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.
This commit is contained in:
KeelrLeek committed 2026-07-22 17:57:00 -05:00
commit a667b4b28e
161 files changed
+53170

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
[package]
name = "soulseek-proto"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
bytes.workspace = true
thiserror.workspace = true
flate2 = "1"
md-5 = "0.10"
+137
View File
@@ -0,0 +1,137 @@
//! Distributed network messages (connection type "D"), single-byte codes.
//! We join as a child: connect to a parent, receive relayed searches, and
//! report our branch level/root.
use bytes::Bytes;
use crate::wire::{Reader, Writer};
use crate::Result;
pub mod code {
pub const PING: u8 = 0;
pub const SEARCH_REQUEST: u8 = 3;
pub const BRANCH_LEVEL: u8 = 4;
pub const BRANCH_ROOT: u8 = 5;
pub const CHILD_DEPTH: u8 = 7;
pub const EMBEDDED_MESSAGE: u8 = 93;
}
// ---- outgoing (to parent / children) ----
pub fn branch_level(level: u32) -> Vec<u8> {
Writer::new().u32(level).finish_u8(code::BRANCH_LEVEL)
}
pub fn branch_root(root_username: &str) -> Vec<u8> {
Writer::new()
.string(root_username)
.finish_u8(code::BRANCH_ROOT)
}
pub fn ping() -> Vec<u8> {
Writer::new().finish_u8(code::PING)
}
/// Relay a search to our children.
pub fn search_request(username: &str, token: u32, query: &str) -> Vec<u8> {
Writer::new()
.u32(0)
.string(username)
.u32(token)
.string(query)
.finish_u8(code::SEARCH_REQUEST)
}
// ---- incoming ----
#[derive(Debug, Clone)]
pub enum DistributedEvent {
/// A search to match against our shares and reply to `username`.
SearchRequest {
username: String,
token: u32,
query: String,
},
BranchLevel(u32),
BranchRoot(String),
Ping,
Unhandled {
code: u8,
},
}
/// Decode a distributed frame (code byte + payload). Handles the
/// `EmbeddedMessage` (93) wrapper by unwrapping to its inner distributed
/// message.
pub fn decode(frame: Bytes) -> Result<DistributedEvent> {
let mut r = Reader::new(frame);
let code = r.u8()?;
decode_inner(code, &mut r)
}
fn decode_inner(code: u8, r: &mut Reader) -> Result<DistributedEvent> {
Ok(match code {
code::EMBEDDED_MESSAGE => {
// [u8 inner_code][inner payload]
let inner = r.u8()?;
return decode_inner(inner, r);
}
code::SEARCH_REQUEST => {
let _unknown = r.u32()?;
let username = r.string()?;
let token = r.u32()?;
let query = r.string()?;
DistributedEvent::SearchRequest {
username,
token,
query,
}
}
code::BRANCH_LEVEL => DistributedEvent::BranchLevel(r.u32()?),
code::BRANCH_ROOT => DistributedEvent::BranchRoot(r.string()?),
code::PING => DistributedEvent::Ping,
other => DistributedEvent::Unhandled { code: other },
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_search_request() {
let msg = search_request("alice", 42, "the beatles");
let ev = decode(Bytes::copy_from_slice(&msg[4..])).unwrap();
match ev {
DistributedEvent::SearchRequest {
username,
token,
query,
} => {
assert_eq!(
(username.as_str(), token, query.as_str()),
("alice", 42, "the beatles")
);
}
other => panic!("wrong: {other:?}"),
}
}
#[test]
fn decode_embedded_wrapper() {
// Server EmbeddedMessage delivers [93][3][search payload].
let inner = search_request("bob", 7, "query");
// inner[4] is the code byte (3); inner[5..] is the payload.
let mut framed = vec![code::EMBEDDED_MESSAGE];
framed.extend_from_slice(&inner[4..]); // code 3 + payload
let ev = decode(Bytes::from(framed)).unwrap();
match ev {
DistributedEvent::SearchRequest {
username, query, ..
} => {
assert_eq!((username.as_str(), query.as_str()), ("bob", "query"));
}
other => panic!("wrong: {other:?}"),
}
}
}
+31
View File
@@ -0,0 +1,31 @@
//! Soulseek protocol messages and binary codecs.
//!
//! Wire format: every message is `[u32 length][code][payload]`, little-endian
//! throughout. Server and peer messages use a `u32` code; connection
//! initialization messages (PeerInit / PierceFirewall) use a single-byte code.
//! Strings are `[u32 len][bytes]`, UTF-8 with Latin-1 fallback on decode.
//! Search responses and browse payloads are zlib-compressed after the code.
//!
//! Reference: Soulseek.NET `src/Messaging/**` and the Nicotine+ protocol docs.
pub mod dist;
pub mod peer;
pub mod server;
pub mod types;
pub mod wire;
#[derive(Debug, thiserror::Error)]
pub enum ProtoError {
#[error("message payload ended unexpectedly")]
UnexpectedEof,
#[error("zlib decompression failed: {0}")]
Zlib(#[from] std::io::Error),
#[error("message too large: {0} bytes")]
TooLarge(usize),
}
pub type Result<T> = std::result::Result<T, ProtoError>;
/// Sanity cap on a single message frame (search responses from big shares
/// run a few hundred KB; browse responses can reach tens of MB).
pub const MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024;
+435
View File
@@ -0,0 +1,435 @@
//! 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);
}
}
+290
View File
@@ -0,0 +1,290 @@
//! Server messages (`server.slsknet.org:2242`), u32 codes.
use bytes::Bytes;
use md5::{Digest, Md5};
use std::net::{Ipv4Addr, SocketAddrV4};
use crate::wire::{Reader, Writer};
use crate::Result;
/// Client version fields sent at login. 160 is the protocol version spoken
/// by current clients (Nicotine+ uses the same).
pub const VERSION: u32 = 160;
pub const MINOR_VERSION: u32 = 1;
pub mod code {
pub const LOGIN: u32 = 1;
pub const SET_LISTEN_PORT: u32 = 2;
pub const GET_PEER_ADDRESS: u32 = 3;
pub const WATCH_USER: u32 = 5;
pub const CONNECT_TO_PEER: u32 = 18;
pub const FILE_SEARCH: u32 = 26;
pub const PING: u32 = 32;
pub const SHARED_FOLDERS_AND_FILES: u32 = 35;
pub const HAVE_NO_PARENTS: u32 = 71;
pub const PARENT_MIN_SPEED: u32 = 83;
pub const PARENT_SPEED_RATIO: u32 = 84;
pub const EMBEDDED_MESSAGE: u32 = 93; // wraps a distributed search
pub const ACCEPT_CHILDREN: u32 = 100;
pub const NET_INFO: u32 = 102;
pub const WISHLIST_INTERVAL: u32 = 104;
pub const BRANCH_LEVEL: u32 = 126;
pub const BRANCH_ROOT: u32 = 127;
pub const EXCLUDED_SEARCH_PHRASES: u32 = 160;
pub const CANNOT_CONNECT: u32 = 1001;
}
// ---- outgoing ----
pub fn login(username: &str, password: &str) -> Vec<u8> {
let hash = hex(Md5::digest(format!("{username}{password}").as_bytes()));
Writer::new()
.string(username)
.string(password)
.u32(VERSION)
.string(&hash)
.u32(MINOR_VERSION)
.finish_u32(code::LOGIN)
}
pub fn set_listen_port(port: u32) -> Vec<u8> {
Writer::new().u32(port).finish_u32(code::SET_LISTEN_PORT)
}
pub fn file_search(token: u32, query: &str) -> Vec<u8> {
Writer::new()
.u32(token)
.string(query)
.finish_u32(code::FILE_SEARCH)
}
pub fn shared_folders_and_files(dirs: u32, files: u32) -> Vec<u8> {
Writer::new()
.u32(dirs)
.u32(files)
.finish_u32(code::SHARED_FOLDERS_AND_FILES)
}
/// Tell the server whether we need a distributed parent.
pub fn accept_children(accept: bool) -> Vec<u8> {
Writer::new().bool(accept).finish_u32(code::ACCEPT_CHILDREN)
}
pub fn branch_level(level: u32) -> Vec<u8> {
Writer::new().u32(level).finish_u32(code::BRANCH_LEVEL)
}
pub fn branch_root(root: &str) -> Vec<u8> {
Writer::new().string(root).finish_u32(code::BRANCH_ROOT)
}
pub fn ping() -> Vec<u8> {
Writer::new().finish_u32(code::PING)
}
pub fn get_peer_address(username: &str) -> Vec<u8> {
Writer::new()
.string(username)
.finish_u32(code::GET_PEER_ADDRESS)
}
/// Ask the server to have `username` connect to us (indirect connection).
pub fn connect_to_peer(token: u32, username: &str, conn_type: &str) -> Vec<u8> {
Writer::new()
.u32(token)
.string(username)
.string(conn_type)
.finish_u32(code::CONNECT_TO_PEER)
}
pub fn have_no_parents(value: bool) -> Vec<u8> {
Writer::new().bool(value).finish_u32(code::HAVE_NO_PARENTS)
}
// ---- incoming ----
#[derive(Debug, Clone)]
pub enum ServerEvent {
LoginOk {
greeting: String,
own_ip: Ipv4Addr,
is_supporter: bool,
},
LoginFailed {
reason: String,
},
/// Another client wants a connection with us (or is answering our
/// indirect request): connect to them and send PierceFirewall(token).
ConnectToPeer {
username: String,
conn_type: String,
addr: SocketAddrV4,
token: u32,
privileged: bool,
},
/// Our indirect connection request to `token` failed.
CannotConnect {
token: u32,
username: Option<String>,
},
/// Answer to GetPeerAddress. A 0.0.0.0/port-0 address means offline.
UserAddress {
username: String,
addr: SocketAddrV4,
},
ExcludedSearchPhrases(Vec<String>),
/// Candidate distributed parents to connect to.
NetInfo(Vec<(String, SocketAddrV4)>),
/// A search relayed directly by the server (older path).
ServerSearch {
username: String,
token: u32,
query: String,
},
/// A distributed message wrapped by the server (code 93). The inner
/// bytes are a distributed frame starting with its code byte — decode
/// with `dist::decode`.
Embedded(Bytes),
Unhandled {
code: u32,
},
}
pub fn decode(code: u32, payload: Bytes) -> Result<ServerEvent> {
let mut r = Reader::new(payload);
Ok(match code {
code::LOGIN => {
if r.bool()? {
let greeting = r.string()?;
let own_ip = r.ipv4()?;
// Trailing hash + supporter flag are absent on some servers.
let _hash = r.string().unwrap_or_default();
let is_supporter = r.bool().unwrap_or(false);
ServerEvent::LoginOk {
greeting,
own_ip,
is_supporter,
}
} else {
ServerEvent::LoginFailed {
reason: r.string()?,
}
}
}
code::CONNECT_TO_PEER => {
let username = r.string()?;
let conn_type = r.string()?;
let ip = r.ipv4()?;
let port = r.u32()?;
let token = r.u32()?;
let privileged = r.bool().unwrap_or(false);
ServerEvent::ConnectToPeer {
username,
conn_type,
addr: SocketAddrV4::new(ip, port as u16),
token,
privileged,
}
}
code::GET_PEER_ADDRESS => {
let username = r.string()?;
let ip = r.ipv4()?;
let port = r.u32()?;
// Trailing obfuscation fields ignored.
ServerEvent::UserAddress {
username,
addr: SocketAddrV4::new(ip, port as u16),
}
}
code::CANNOT_CONNECT => {
let token = r.u32()?;
let username = r.string().ok();
ServerEvent::CannotConnect { token, username }
}
code::NET_INFO => {
let count = r.u32()?.min(1000);
let mut parents = Vec::with_capacity(count as usize);
for _ in 0..count {
let username = r.string()?;
let ip = r.ipv4()?;
let port = r.u32()?;
parents.push((username, SocketAddrV4::new(ip, port as u16)));
}
ServerEvent::NetInfo(parents)
}
code::FILE_SEARCH => {
// Server-relayed search (same code we send): username, token, query.
let username = r.string()?;
let token = r.u32()?;
let query = r.string()?;
ServerEvent::ServerSearch {
username,
token,
query,
}
}
code::EMBEDDED_MESSAGE => ServerEvent::Embedded(r.rest()),
code::EXCLUDED_SEARCH_PHRASES => {
let count = r.u32()?.min(10_000);
let mut phrases = Vec::with_capacity(count as usize);
for _ in 0..count {
phrases.push(r.string()?);
}
ServerEvent::ExcludedSearchPhrases(phrases)
}
other => ServerEvent::Unhandled { code: other },
})
}
fn hex(digest: impl AsRef<[u8]>) -> String {
digest.as_ref().iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn login_message_layout() {
let msg = login("user", "pass");
let mut r = Reader::new(Bytes::copy_from_slice(&msg[8..]));
assert_eq!(&msg[4..8], &1u32.to_le_bytes());
assert_eq!(r.string().unwrap(), "user");
assert_eq!(r.string().unwrap(), "pass");
assert_eq!(r.u32().unwrap(), VERSION);
// md5("userpass")
assert_eq!(r.string().unwrap(), "63e780c3f321d13109c71bf81805476e");
assert_eq!(r.u32().unwrap(), MINOR_VERSION);
}
#[test]
fn decode_connect_to_peer() {
let payload = Writer::new()
.string("bob")
.string("P")
.u8(4)
.u8(3)
.u8(2)
.u8(1) // 1.2.3.4 reversed on wire
.u32(2234)
.u32(99)
.bool(false)
.finish_u32(18);
let ev = decode(18, Bytes::copy_from_slice(&payload[8..])).unwrap();
match ev {
ServerEvent::ConnectToPeer {
username,
conn_type,
addr,
token,
..
} => {
assert_eq!(username, "bob");
assert_eq!(conn_type, "P");
assert_eq!(addr, "1.2.3.4:2234".parse().unwrap());
assert_eq!(token, 99);
}
other => panic!("wrong event: {other:?}"),
}
}
}
+114
View File
@@ -0,0 +1,114 @@
//! Shared domain types used across peer and server messages.
use crate::wire::{Reader, Writer};
use crate::Result;
/// File attribute types (Soulseek.NET `FileAttributeType`).
pub const ATTR_BITRATE: u32 = 0;
pub const ATTR_DURATION: u32 = 1;
pub const ATTR_VBR: u32 = 2;
pub const ATTR_SAMPLE_RATE: u32 = 4;
pub const ATTR_BIT_DEPTH: u32 = 5;
#[derive(Debug, Clone)]
pub struct File {
pub filename: String,
pub size: u64,
pub extension: String,
pub attributes: Vec<(u32, u32)>,
}
impl File {
pub(crate) fn read(r: &mut Reader) -> Result<File> {
let _code = r.u8()?;
let filename = r.string()?;
let mut size = r.u64()?;
// Some clients send a 32-bit size sign-extended to 64 bits.
if size > i64::MAX as u64 && (size >> 32) == 0xFFFF_FFFF {
size &= 0xFFFF_FFFF;
}
let extension = r.string()?;
let attr_count = r.u32()?.min(64);
let mut attributes = Vec::with_capacity(attr_count as usize);
for _ in 0..attr_count {
attributes.push((r.u32()?, r.u32()?));
}
Ok(File {
filename,
size,
extension,
attributes,
})
}
/// Write this file into a search/browse response body. `code` is the
/// leading byte (1 for normal files).
pub fn write(&self, mut w: Writer) -> Writer {
w = w
.u8(1)
.string(&self.filename)
.u64(self.size)
.string(&self.extension);
w = w.u32(self.attributes.len() as u32);
for (ty, val) in &self.attributes {
w = w.u32(*ty).u32(*val);
}
w
}
fn attr(&self, ty: u32) -> Option<u32> {
self.attributes
.iter()
.find(|(t, _)| *t == ty)
.map(|(_, v)| *v)
}
pub fn bitrate(&self) -> Option<u32> {
self.attr(ATTR_BITRATE)
}
pub fn duration_secs(&self) -> Option<u32> {
self.attr(ATTR_DURATION)
}
pub fn is_vbr(&self) -> bool {
self.attr(ATTR_VBR) == Some(1)
}
pub fn sample_rate(&self) -> Option<u32> {
self.attr(ATTR_SAMPLE_RATE)
}
pub fn bit_depth(&self) -> Option<u32> {
self.attr(ATTR_BIT_DEPTH)
}
}
#[derive(Debug, Clone)]
pub struct SearchResponse {
pub username: String,
pub token: u32,
pub files: Vec<File>,
pub has_free_upload_slot: bool,
pub upload_speed: u32,
pub queue_length: u32,
pub locked_files: Vec<File>,
}
/// A shared directory in a browse response: backslash-separated name and
/// its files.
#[derive(Debug, Clone)]
pub struct Directory {
pub name: String,
pub files: Vec<File>,
}
impl Directory {
pub fn write(&self, mut w: Writer) -> Writer {
w = w.string(&self.name).u32(self.files.len() as u32);
for f in &self.files {
w = f.write(w);
}
w
}
}
+208
View File
@@ -0,0 +1,208 @@
//! Low-level message building and reading, mirroring Soulseek.NET's
//! `MessageBuilder` / `MessageReader`.
use bytes::{Buf, BufMut, Bytes, BytesMut};
use flate2::read::ZlibDecoder;
use std::io::Read;
use crate::{ProtoError, Result};
/// Builds one outgoing message. `finish_*` prepends the length header.
pub struct Writer {
buf: BytesMut,
}
impl Default for Writer {
fn default() -> Self {
Self::new()
}
}
impl Writer {
pub fn new() -> Self {
Writer {
buf: BytesMut::with_capacity(64),
}
}
pub fn u8(mut self, v: u8) -> Self {
self.buf.put_u8(v);
self
}
pub fn u32(mut self, v: u32) -> Self {
self.buf.put_u32_le(v);
self
}
pub fn u64(mut self, v: u64) -> Self {
self.buf.put_u64_le(v);
self
}
pub fn bool(self, v: bool) -> Self {
self.u8(v as u8)
}
pub fn string(mut self, s: &str) -> Self {
self.buf.put_u32_le(s.len() as u32);
self.buf.put_slice(s.as_bytes());
self
}
/// Frame with a u32 code: `[len][code:u32][payload]`.
pub fn finish_u32(self, code: u32) -> Vec<u8> {
let mut out = Vec::with_capacity(self.buf.len() + 8);
out.extend_from_slice(&((self.buf.len() as u32 + 4).to_le_bytes()));
out.extend_from_slice(&code.to_le_bytes());
out.extend_from_slice(&self.buf);
out
}
/// Frame with a u32 code and a zlib-compressed payload:
/// `[len][code:u32][zlib(payload)]` (search responses, browse responses).
pub fn finish_u32_compressed(self, code: u32) -> Vec<u8> {
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::Write;
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(&self.buf).expect("zlib write");
let compressed = enc.finish().expect("zlib finish");
let mut out = Vec::with_capacity(compressed.len() + 8);
out.extend_from_slice(&((compressed.len() as u32 + 4).to_le_bytes()));
out.extend_from_slice(&code.to_le_bytes());
out.extend_from_slice(&compressed);
out
}
/// Frame with a single-byte code: `[len][code:u8][payload]`
/// (PeerInit / PierceFirewall only).
pub fn finish_u8(self, code: u8) -> Vec<u8> {
let mut out = Vec::with_capacity(self.buf.len() + 5);
out.extend_from_slice(&((self.buf.len() as u32 + 1).to_le_bytes()));
out.push(code);
out.extend_from_slice(&self.buf);
out
}
}
/// Reads one message payload (the bytes after the code).
pub struct Reader {
buf: Bytes,
}
impl Reader {
pub fn new(payload: Bytes) -> Self {
Reader { buf: payload }
}
pub fn remaining(&self) -> usize {
self.buf.remaining()
}
pub fn has_more(&self) -> bool {
self.buf.has_remaining()
}
/// Take all remaining bytes.
pub fn rest(&mut self) -> Bytes {
let n = self.buf.remaining();
self.buf.copy_to_bytes(n)
}
pub fn u8(&mut self) -> Result<u8> {
if self.buf.remaining() < 1 {
return Err(ProtoError::UnexpectedEof);
}
Ok(self.buf.get_u8())
}
pub fn bool(&mut self) -> Result<bool> {
Ok(self.u8()? != 0)
}
pub fn u32(&mut self) -> Result<u32> {
if self.buf.remaining() < 4 {
return Err(ProtoError::UnexpectedEof);
}
Ok(self.buf.get_u32_le())
}
pub fn u64(&mut self) -> Result<u64> {
if self.buf.remaining() < 8 {
return Err(ProtoError::UnexpectedEof);
}
Ok(self.buf.get_u64_le())
}
/// IPv4 address; wire order is reversed relative to display order.
pub fn ipv4(&mut self) -> Result<std::net::Ipv4Addr> {
if self.buf.remaining() < 4 {
return Err(ProtoError::UnexpectedEof);
}
let mut b = [0u8; 4];
self.buf.copy_to_slice(&mut b);
Ok(std::net::Ipv4Addr::new(b[3], b[2], b[1], b[0]))
}
/// UTF-8 with Latin-1 fallback, matching what real-world peers send.
pub fn string(&mut self) -> Result<String> {
let len = self.u32()? as usize;
if self.buf.remaining() < len {
return Err(ProtoError::UnexpectedEof);
}
let bytes = self.buf.copy_to_bytes(len);
Ok(match std::str::from_utf8(&bytes) {
Ok(s) => s.to_owned(),
Err(_) => bytes.iter().map(|&b| b as char).collect(),
})
}
/// Replace the remaining payload with its zlib-decompressed form.
pub fn decompress(&mut self) -> Result<()> {
let mut out = Vec::new();
ZlibDecoder::new(&self.buf[..]).read_to_end(&mut out)?;
if out.len() > crate::MAX_MESSAGE_SIZE {
return Err(ProtoError::TooLarge(out.len()));
}
self.buf = Bytes::from(out);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_primitives() {
let msg = Writer::new().u32(7).string("héllo").u64(9).finish_u32(26);
// len = 4 (code) + 4 + 4+6 + 8 = 26
assert_eq!(&msg[0..4], &26u32.to_le_bytes());
assert_eq!(&msg[4..8], &26u32.to_le_bytes());
let mut r = Reader::new(Bytes::copy_from_slice(&msg[8..]));
assert_eq!(r.u32().unwrap(), 7);
assert_eq!(r.string().unwrap(), "héllo");
assert_eq!(r.u64().unwrap(), 9);
assert!(!r.has_more());
}
#[test]
fn u8_code_frame() {
let msg = Writer::new().u32(123).finish_u8(0);
assert_eq!(&msg[0..4], &5u32.to_le_bytes());
assert_eq!(msg[4], 0);
}
#[test]
fn latin1_fallback() {
let mut raw = Vec::new();
raw.extend_from_slice(&2u32.to_le_bytes());
raw.extend_from_slice(&[0xE9, 0x21]); // "é!" in Latin-1, invalid UTF-8
let mut r = Reader::new(Bytes::from(raw));
assert_eq!(r.string().unwrap(), "é!");
}
}