feat(lib): answer FolderContentsRequest with the folder's listing

"Download folder" in SoulseekQt and Nicotine+ asks the sharer for that
one folder (peer code 36) instead of the whole share, and a sharer that
never answers leaves the folder download hanging. The requested folder
now comes back as a FolderContentsResponse (code 37), which carries a
directory listing in the same form a browse does, so the two share one
codec. The browse and folder replies also share one reply helper.
This commit is contained in:
Michel de Graaf
2026-09-07 15:21:42 +02:00
parent 53b32e0201
commit 4839e36be8
7 changed files with 266 additions and 55 deletions
@@ -30,20 +30,7 @@ impl MessageHandler<PeerMessage> for SharedFileListResponseHandler {
#[must_use]
pub fn build_shared_file_list(dirs: &[SharedDirectory]) -> Message {
let mut payload = Message::new();
payload.write_int32(dirs.len() as u32);
for dir in dirs {
payload
.write_string(&dir.name)
.write_int32(dir.files.len() as u32);
for (name, size) in &dir.files {
payload
.write_int8(1)
.write_string(name)
.write_int64(*size)
.write_string("") // extension
.write_int32(0); // attribute count
}
}
write_directories(&mut payload, dirs);
payload.write_int32(0); // unknown
payload.write_int32(0); // number of private directories
@@ -60,19 +47,35 @@ pub fn build_shared_file_list(dirs: &[SharedDirectory]) -> Message {
/// Returns an empty listing if the payload is malformed.
#[must_use]
pub fn parse_shared_file_list(message: &mut Message) -> Vec<SharedDirectory> {
let pointer = message.get_pointer();
let size = message.get_size();
let compressed = message.get_slice(pointer, size);
let Ok(data) = deflate(&compressed) else {
return Vec::new();
};
decompress_body(message)
.map_or_else(Vec::new, |mut body| read_directories(&mut body))
}
let mut body = Message::new_with_data(data);
/// Write a directory listing in the form both code 5 and code 37 carry.
pub fn write_directories(payload: &mut Message, dirs: &[SharedDirectory]) {
payload.write_int32(dirs.len() as u32);
for dir in dirs {
payload
.write_string(&dir.name)
.write_int32(dir.files.len() as u32);
for (name, size) in &dir.files {
payload
.write_int8(1)
.write_string(name)
.write_int64(*size)
.write_string("") // extension
.write_int32(0); // attribute count
}
}
}
/// Read back what [`write_directories`] wrote, stopping early when a hostile
/// count outruns the payload so a bogus length cannot spin into a huge
/// allocation loop.
pub fn read_directories(body: &mut Message) -> Vec<SharedDirectory> {
let dir_count = body.read_int32();
let mut dirs = Vec::new();
for _ in 0..dir_count {
// Stop if a hostile count outruns the (decompressed) payload, so a
// bogus length can't spin us into a huge allocation loop.
if body.get_pointer() >= body.get_size() {
break;
}
@@ -104,6 +107,15 @@ pub fn parse_shared_file_list(message: &mut Message) -> Vec<SharedDirectory> {
dirs
}
/// Inflate a compressed peer payload positioned at the blob.
pub fn decompress_body(message: &mut Message) -> Option<Message> {
let pointer = message.get_pointer();
let size = message.get_size();
deflate(&message.get_slice(pointer, size))
.ok()
.map(Message::new_with_data)
}
#[test]
fn hostile_dir_count_does_not_hang() {
// A compressed body claiming ~4 billion directories with no data must