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
@@ -0,0 +1,68 @@
//! `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;
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::compress_stored;
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 });
}
}
/// 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(compress_stored(&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)?;
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![("one.flac".to_string(), 40), ("two.flac".to_string(), 50)],
}];
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))
);
}