Files
soulseek-rs-modded/soulseek-rs-lib/src/message/peer/folder_contents.rs
T
Michel de Graaf 9bca573bc0 fix(uploads): finish a transfer when the file lands, not when the peer closes
The stress harness found this: 38 completed uploads logged as errors, and a
run that took 30s to move what should take 6.

After the last byte we waited up to 30 seconds for the downloader to close
the file connection. Soulseek does leave that close to the downloader, but a
peer that simply leaves the socket open — or whose FIN a NAT swallows — is
not a failure: it has every byte. We were marking those uploads Failed,
skipping the SendUploadSpeed report that sets our advertised speed, and
holding the upload slot the next peer was queued for.

Cutting the wait to five seconds was not enough, and stress said so: with
two slots and sixty-four queued leechers, five seconds apiece is a hundred
and sixty seconds of waiting, and ten uploads never got served inside the
run. The slot has to be free the moment the file is delivered, so the wait
is gone — Nicotine+ finishes the transfer there too and closes from its own
side. That profile went from 146s with ten uploads lost to 9.7s with none;
the default profile from 30.7s and 25 MiB/s to 5.8s and 133 MiB/s.

Also: a decompression ceiling on peer payloads, the numbers Nicotine+ uses
(128 MiB for a search or folder reply, 2 GiB for a browse listing). Deflate
reaches roughly 1000:1, so a few kilobytes from an unknown peer could
otherwise expand until the process died.

And the stress harness counted threads with `ps -M`, which is macOS
spelling: on Linux it returns one unrelated line, so the guard against
thread explosions has been reporting a single thread on CI all along.
/proc/self/status has the real number.
2026-09-09 19:35:58 +00:00

113 lines
3.5 KiB
Rust

//! `FolderContentsRequest` (peer code 36) and its reply (code 37): one folder
//! of a share in the listing form code 5 uses, which is what "download
//! folder" in other clients asks for.
use crate::message::peer::SharedDirectory;
#[cfg(test)]
use crate::message::peer::SharedFileEntry;
use crate::message::peer::shared_file_list::{
decompress_body, read_directories, write_directories,
};
use crate::message::{Message, MessageHandler};
use crate::peer::PeerMessage;
use crate::utils::zlib::deflate;
use std::sync::mpsc::Sender;
pub struct FolderContentsRequest;
impl MessageHandler<PeerMessage> for FolderContentsRequest {
fn get_code(&self) -> u32 {
36
}
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
let token = message.read_int32();
let folder = message.read_string();
let _ =
sender.send(PeerMessage::FolderContentsRequested { token, folder });
}
}
/// `FolderContentsResponse` (peer code 37): the reply to a folder we asked
/// for. The token is the one we quoted, so a client with two folders in
/// flight can tell the answers apart.
pub struct FolderContentsResponseHandler;
impl MessageHandler<PeerMessage> for FolderContentsResponseHandler {
fn get_code(&self) -> u32 {
37
}
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
let Some((token, folder, directories)) = parse_folder_contents(message)
else {
return;
};
let _ = sender.send(PeerMessage::FolderContentsReceived {
token,
folder,
directories,
});
}
}
/// Build a `FolderContentsRequest` (peer code 36).
#[must_use]
pub fn build_folder_contents_request(token: u32, folder: &str) -> Message {
Message::new()
.write_int32(36)
.write_int32(token)
.write_string(folder)
.clone()
}
/// Build a `FolderContentsResponse` (peer code 37).
#[must_use]
pub fn build_folder_contents(
token: u32,
folder: &str,
dirs: &[SharedDirectory],
) -> Message {
let mut payload = Message::new();
payload.write_int32(token).write_string(folder);
write_directories(&mut payload, dirs);
Message::new()
.write_int32(37)
.write_raw_bytes(deflate(&payload.get_data()))
.clone()
}
/// Parse a `FolderContentsResponse` positioned at its compressed blob, or
/// `None` when it does not inflate.
#[must_use]
pub fn parse_folder_contents(
message: &mut Message,
) -> Option<(u32, String, Vec<SharedDirectory>)> {
let mut body = decompress_body(message, crate::utils::zlib::MAX_INFLATED)?;
let token = body.read_int32();
let folder = body.read_string();
Some((token, folder, read_directories(&mut body)))
}
#[test]
fn a_folder_listing_round_trips() {
let dirs = vec![SharedDirectory {
name: "music\\album".to_string(),
files: vec![
SharedFileEntry {
name: "one.flac".to_string(),
size: 40,
attributes: vec![(0, 992)],
},
SharedFileEntry {
name: "two.flac".to_string(),
size: 50,
attributes: Vec::new(),
},
],
}];
let built = build_folder_contents(7, "music\\album", &dirs);
let mut message = Message::new_with_data(built.get_data());
message.set_pointer(4);
assert_eq!(
parse_folder_contents(&mut message),
Some((7, "music\\album".to_string(), dirs))
);
}