Files
SoulStream/crates/soulseek-proto/src/peer.rs
T
KeelrLeek a667b4b28e Baseline: full tree snapshot after git repository loss
The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.
2026-07-22 17:57:00 -05:00

436 lines
12 KiB
Rust

//! Peer connection messages: single-byte-code initialization handshake,
//! then u32-code peer messages.
use bytes::Bytes;
use crate::types::{Directory, File, SearchResponse};
use crate::wire::{Reader, Writer};
use crate::{ProtoError, Result};
pub mod init_code {
pub const PIERCE_FIREWALL: u8 = 0;
pub const PEER_INIT: u8 = 1;
}
pub mod code {
pub const BROWSE_REQUEST: u32 = 4;
pub const BROWSE_RESPONSE: u32 = 5;
pub const SEARCH_RESPONSE: u32 = 9;
pub const USER_INFO_REQUEST: u32 = 15;
pub const USER_INFO_RESPONSE: u32 = 16;
pub const FOLDER_CONTENTS_REQUEST: u32 = 36;
pub const FOLDER_CONTENTS_RESPONSE: u32 = 37;
pub const TRANSFER_REQUEST: u32 = 40;
pub const TRANSFER_RESPONSE: u32 = 41;
pub const QUEUE_DOWNLOAD: u32 = 43;
pub const PLACE_IN_QUEUE_RESPONSE: u32 = 44;
pub const UPLOAD_FAILED: u32 = 46;
pub const UPLOAD_DENIED: u32 = 50;
pub const PLACE_IN_QUEUE_REQUEST: u32 = 51;
}
/// Connection types carried by PeerInit / ConnectToPeer.
pub const CONN_TYPE_PEER: &str = "P";
pub const CONN_TYPE_TRANSFER: &str = "F";
pub const CONN_TYPE_DISTRIBUTED: &str = "D";
/// TransferRequest direction values.
pub const DIRECTION_DOWNLOAD: u32 = 0;
pub const DIRECTION_UPLOAD: u32 = 1;
// ---- initialization (u8 code) ----
#[derive(Debug, Clone)]
pub enum InitMessage {
PierceFirewall {
token: u32,
},
PeerInit {
username: String,
conn_type: String,
token: u32,
},
}
pub fn pierce_firewall(token: u32) -> Vec<u8> {
Writer::new()
.u32(token)
.finish_u8(init_code::PIERCE_FIREWALL)
}
pub fn peer_init(username: &str, conn_type: &str, token: u32) -> Vec<u8> {
Writer::new()
.string(username)
.string(conn_type)
.u32(token)
.finish_u8(init_code::PEER_INIT)
}
/// Decode the first frame on a fresh incoming connection.
/// `frame` is the full frame body (code byte included, length header not).
pub fn decode_init(frame: Bytes) -> Result<InitMessage> {
let mut r = Reader::new(frame);
match r.u8()? {
init_code::PIERCE_FIREWALL => Ok(InitMessage::PierceFirewall { token: r.u32()? }),
init_code::PEER_INIT => Ok(InitMessage::PeerInit {
username: r.string()?,
conn_type: r.string()?,
token: r.u32()?,
}),
_ => Err(ProtoError::UnexpectedEof),
}
}
// ---- peer messages (u32 code) ----
/// Ask a peer to send us a file (modern flow; direction=download, size 0).
pub fn transfer_request_download(token: u32, filename: &str) -> Vec<u8> {
Writer::new()
.u32(DIRECTION_DOWNLOAD)
.u32(token)
.string(filename)
.u64(0)
.finish_u32(code::TRANSFER_REQUEST)
}
/// Offer to upload one of our files to a peer (direction=upload).
pub fn transfer_request_upload(token: u32, filename: &str, size: u64) -> Vec<u8> {
Writer::new()
.u32(DIRECTION_UPLOAD)
.u32(token)
.string(filename)
.u64(size)
.finish_u32(code::TRANSFER_REQUEST)
}
/// Accept a peer's upload offer (their TransferRequest with direction=upload).
pub fn transfer_response_allowed(token: u32, size: u64) -> Vec<u8> {
Writer::new()
.u32(token)
.u8(1)
.u64(size)
.finish_u32(code::TRANSFER_RESPONSE)
}
pub fn transfer_response_rejected(token: u32, reason: &str) -> Vec<u8> {
Writer::new()
.u32(token)
.u8(0)
.string(reason)
.finish_u32(code::TRANSFER_RESPONSE)
}
/// Legacy enqueue message; some clients expect it before TransferRequest.
pub fn queue_download(filename: &str) -> Vec<u8> {
Writer::new()
.string(filename)
.finish_u32(code::QUEUE_DOWNLOAD)
}
pub fn place_in_queue_request(filename: &str) -> Vec<u8> {
Writer::new()
.string(filename)
.finish_u32(code::PLACE_IN_QUEUE_REQUEST)
}
pub fn place_in_queue_response(filename: &str, place: u32) -> Vec<u8> {
Writer::new()
.string(filename)
.u32(place)
.finish_u32(code::PLACE_IN_QUEUE_RESPONSE)
}
pub fn upload_denied(filename: &str, reason: &str) -> Vec<u8> {
Writer::new()
.string(filename)
.string(reason)
.finish_u32(code::UPLOAD_DENIED)
}
// ---- responder-side encoders (we answer other peers' requests) ----
/// A search reply we send to a searcher (code 9, zlib-compressed).
pub fn search_response(
username: &str,
token: u32,
files: &[File],
has_free_upload_slot: bool,
upload_speed: u32,
queue_length: u32,
) -> Vec<u8> {
let mut w = Writer::new()
.string(username)
.u32(token)
.u32(files.len() as u32);
for f in files {
w = f.write(w);
}
w = w
.bool(has_free_upload_slot)
.u32(upload_speed)
.u32(queue_length)
.u32(0) // unknown, for compatibility
.u32(0); // locked file count
w.finish_u32_compressed(code::SEARCH_RESPONSE)
}
/// Our full shared directory listing (code 5, zlib-compressed).
pub fn browse_response(dirs: &[Directory]) -> Vec<u8> {
let mut w = Writer::new().u32(dirs.len() as u32);
for d in dirs {
w = d.write(w);
}
w = w.u32(0).u32(0); // unknown + locked directory count
w.finish_u32_compressed(code::BROWSE_RESPONSE)
}
/// Contents of a single requested folder (code 37, zlib-compressed).
pub fn folder_contents_response(token: u32, folder: &str, dir: &Directory) -> Vec<u8> {
let w = Writer::new().u32(token).string(folder).u32(1);
let w = dir.write(w);
w.finish_u32_compressed(code::FOLDER_CONTENTS_RESPONSE)
}
/// Our user-info card (code 16).
pub fn user_info_response(
description: &str,
upload_slots: u32,
queue_length: u32,
has_free_upload_slot: bool,
) -> Vec<u8> {
Writer::new()
.string(description)
.u8(0) // has picture = false
.u32(upload_slots)
.u32(queue_length)
.bool(has_free_upload_slot)
.finish_u32(code::USER_INFO_RESPONSE)
}
#[derive(Debug, Clone)]
pub enum PeerEvent {
SearchResponse(SearchResponse),
/// Peer is ready to send us a file (direction=upload) or wants one
/// from us (direction=download → an upload request we service).
TransferRequest {
direction: u32,
token: u32,
filename: String,
size: u64,
},
/// Answer to our TransferRequest.
TransferResponse {
token: u32,
allowed: bool,
size: u64,
message: String,
},
PlaceInQueueResponse {
filename: String,
place: u32,
},
UploadDenied {
filename: String,
reason: String,
},
UploadFailed {
filename: String,
},
// Requests other peers make of us (M5 responder):
BrowseRequest,
UserInfoRequest,
FolderContentsRequest {
token: u32,
folder: String,
},
/// Peer asks us to queue an upload of one of our files to them.
QueueDownload {
filename: String,
},
PlaceInQueueRequest {
filename: String,
},
Unhandled {
code: u32,
},
}
pub fn decode(code: u32, payload: Bytes) -> Result<PeerEvent> {
// Search responses carry a zlib payload; handle before Reader dispatch.
if code == code::SEARCH_RESPONSE {
return Ok(PeerEvent::SearchResponse(decode_search_response(payload)?));
}
let mut r = Reader::new(payload);
Ok(match code {
code::TRANSFER_REQUEST => {
let direction = r.u32()?;
let token = r.u32()?;
let filename = r.string()?;
let size = if r.remaining() >= 8 { r.u64()? } else { 0 };
PeerEvent::TransferRequest {
direction,
token,
filename,
size,
}
}
code::TRANSFER_RESPONSE => {
let token = r.u32()?;
let allowed = r.bool()?;
if allowed {
let size = if r.remaining() >= 8 { r.u64()? } else { 0 };
PeerEvent::TransferResponse {
token,
allowed,
size,
message: String::new(),
}
} else {
let message = r.string().unwrap_or_default();
PeerEvent::TransferResponse {
token,
allowed,
size: 0,
message,
}
}
}
code::PLACE_IN_QUEUE_RESPONSE => PeerEvent::PlaceInQueueResponse {
filename: r.string()?,
place: r.u32()?,
},
code::UPLOAD_DENIED => PeerEvent::UploadDenied {
filename: r.string()?,
reason: r.string().unwrap_or_default(),
},
code::UPLOAD_FAILED => PeerEvent::UploadFailed {
filename: r.string()?,
},
code::BROWSE_REQUEST => PeerEvent::BrowseRequest,
code::USER_INFO_REQUEST => PeerEvent::UserInfoRequest,
code::FOLDER_CONTENTS_REQUEST => PeerEvent::FolderContentsRequest {
token: r.u32()?,
folder: r.string()?,
},
code::QUEUE_DOWNLOAD => PeerEvent::QueueDownload {
filename: r.string()?,
},
code::PLACE_IN_QUEUE_REQUEST => PeerEvent::PlaceInQueueRequest {
filename: r.string()?,
},
other => PeerEvent::Unhandled { code: other },
})
}
/// Payload is zlib-compressed: username, token, files, slot/speed/queue,
/// optional unknown u32, optional locked file list.
fn decode_search_response(payload: Bytes) -> Result<SearchResponse> {
let mut r = Reader::new(payload);
r.decompress()?;
let username = r.string()?;
let token = r.u32()?;
let file_count = r.u32()?.min(100_000);
let mut files = Vec::with_capacity(file_count.min(4096) as usize);
for _ in 0..file_count {
files.push(File::read(&mut r)?);
}
let has_free_upload_slot = r.bool()?;
let upload_speed = r.u32()?;
let queue_length = r.u32()?;
if r.has_more() {
let _unknown = r.u32()?;
}
let mut locked_files = Vec::new();
if r.has_more() {
let locked_count = r.u32()?.min(100_000);
for _ in 0..locked_count {
locked_files.push(File::read(&mut r)?);
}
}
Ok(SearchResponse {
username,
token,
files,
has_free_upload_slot,
upload_speed,
queue_length,
locked_files,
})
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::Write as _;
fn zlib(data: &[u8]) -> Bytes {
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(data).unwrap();
Bytes::from(enc.finish().unwrap())
}
#[test]
fn init_roundtrip() {
let msg = peer_init("alice", "P", 42);
let init = decode_init(Bytes::copy_from_slice(&msg[4..])).unwrap();
match init {
InitMessage::PeerInit {
username,
conn_type,
token,
} => {
assert_eq!(
(username.as_str(), conn_type.as_str(), token),
("alice", "P", 42)
);
}
other => panic!("wrong init: {other:?}"),
}
let msg = pierce_firewall(7);
match decode_init(Bytes::copy_from_slice(&msg[4..])).unwrap() {
InitMessage::PierceFirewall { token } => assert_eq!(token, 7),
other => panic!("wrong init: {other:?}"),
}
}
#[test]
fn search_response_decode() {
// Build the uncompressed body by hand.
let body = Writer::new()
.string("bob")
.u32(555)
.u32(1)
// file entry
.u8(1)
.string("Music\\Artist\\Album\\01 - Song.flac")
.u64(31_337_000)
.string("flac")
.u32(2)
.u32(0)
.u32(1024) // bitrate
.u32(1)
.u32(213) // duration
// trailer
.bool(true)
.u32(500_000)
.u32(0)
.finish_u32(0);
let compressed = zlib(&body[8..]); // strip len+code, compress payload only
let resp = decode_search_response(compressed).unwrap();
assert_eq!(resp.username, "bob");
assert_eq!(resp.token, 555);
assert_eq!(resp.files.len(), 1);
let f = &resp.files[0];
assert_eq!(f.size, 31_337_000);
assert_eq!(f.bitrate(), Some(1024));
assert_eq!(f.duration_secs(), Some(213));
assert!(resp.has_free_upload_slot);
}
}