feat(shares): reply to browse requests with a SharedFileListResponse (code 5)

GetShareFileList (peer code 4) previously replied with a malformed message (a
server SharedFolders stats message over a peer connection). It now emits
ShareListRequested, and the client builds a real SharedFileListResponse
(code 5) from its shares grouped by directory. Adds a std-only code-5
encoder/decoder (build_shared_file_list / parse_shared_file_list) with a
roundtrip test, and Shares::directories() to group files. The decoder is the
foundation for the Browse feature.
This commit is contained in:
michel
2026-07-19 18:51:55 +02:00
parent 54d7d8f8a3
commit f3b24f34c0
6 changed files with 177 additions and 4 deletions
+11
View File
@@ -38,6 +38,8 @@ pub enum PeerMessage {
RequestTransfer(Download),
/// A peer queued one of our shared files for download (they sent us code 43).
IncomingQueueUpload(String),
/// A peer asked to browse our shared files (they sent us code 4).
ShareListRequested,
/// Offer the queued file to that peer: send an upload TransferRequest.
ServeUpload {
token: u32,
@@ -347,6 +349,15 @@ impl PeerActor {
);
self.send_message(message);
}
PeerMessage::ShareListRequested => {
let requester_key = self.peer_username();
if let Err(e) = self
.client_channel
.send(ClientOperation::ShareListRequested { requester_key })
{
error!("[peer_actor] forward ShareListRequested: {}", e);
}
}
PeerMessage::RequestTransfer(download) => {
let message = MessageFactory::build_transfer_request_message(
&download.filename,
+37
View File
@@ -173,6 +173,10 @@ pub enum ClientOperation {
StartUpload {
token: u32,
},
/// A peer asked to browse our shared files; send our SharedFileListResponse.
ShareListRequested {
requester_key: String,
},
/// A direct outbound connection to this peer failed before it was
/// established — the peer is likely firewalled, so fall back to asking the
/// server to broker the connection. Carries the reporting actor's id.
@@ -1784,6 +1788,39 @@ impl Client {
}
}
}
ClientOperation::ShareListRequested {
requester_key,
} => {
// Reply with our full shared-file listing.
let (registry, message) = match client_context
.read_safe()
{
Ok(ctx) => {
let dirs = ctx
.shares
.directories()
.into_iter()
.map(|(name, files)| {
crate::message::peer::SharedDirectory {
name,
files,
}
})
.collect::<Vec<_>>();
(
ctx.peer_registry.clone(),
crate::message::peer::build_shared_file_list(&dirs),
)
}
Err(_) => continue,
};
if let Some(registry) = registry {
let _ = registry.send_to_peer(
&requester_key,
PeerMessage::SendMessage(message),
);
}
}
ClientOperation::PeerConnectFailed(
id,
username,
@@ -1,17 +1,17 @@
use crate::{
message::{Message, MessageHandler, server::MessageFactory},
message::{Message, MessageHandler},
peer::PeerMessage,
};
use std::sync::mpsc::Sender;
/// A peer asking to browse our shared files (peer code 4). The client (which
/// owns the shares) builds the real SharedFileListResponse in reply.
pub struct GetShareFileList;
impl MessageHandler<PeerMessage> for GetShareFileList {
fn get_code(&self) -> u8 {
4
}
fn handle(&self, _message: &mut Message, sender: Sender<PeerMessage>) {
let message = MessageFactory::build_shared_folders_message(100, 800);
sender.send(PeerMessage::SendMessage(message)).unwrap();
let _ = sender.send(PeerMessage::ShareListRequested);
}
}
+4
View File
@@ -3,6 +3,7 @@ mod get_share_file_list;
mod peer_init;
mod place_in_queue_response;
mod queue_upload;
mod shared_file_list;
mod transfer_request;
mod transfer_response;
mod upload_failed;
@@ -15,6 +16,9 @@ pub use get_share_file_list::GetShareFileList;
pub use peer_init::PeerInit;
pub use place_in_queue_response::PlaceInQueueResponse;
pub use queue_upload::QueueUploadHandler;
pub use shared_file_list::{
SharedDirectory, build_shared_file_list, parse_shared_file_list,
};
pub use transfer_request::TransferRequest;
pub use transfer_response::TransferResponse;
pub use upload_failed::UploadFailedHandler;
@@ -0,0 +1,101 @@
//! `SharedFileListResponse` (peer code 5): a peer's full shared-file listing,
//! sent in reply to `GetShareFileList` (code 4). The payload is zlib-compressed
//! and groups files by their virtual directory.
use crate::message::Message;
use crate::utils::zlib::{compress_stored, deflate};
/// One shared directory and the files directly in it (basename + size).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SharedDirectory {
pub name: String,
pub files: Vec<(String, u64)>,
}
/// Build a `SharedFileListResponse` (peer code 5) from the directory listing.
#[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
}
}
payload.write_int32(0); // unknown
payload.write_int32(0); // number of private directories
let compressed = compress_stored(&payload.get_data());
Message::new()
.write_int32(5)
.write_raw_bytes(compressed)
.clone()
}
/// Parse the (zlib-compressed) `SharedFileListResponse` payload. `message` must
/// be positioned at the compressed blob (the dispatcher sets pointer 8).
///
/// 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();
};
let mut body = Message::new_with_data(data);
let dir_count = body.read_int32();
let mut dirs = Vec::new();
for _ in 0..dir_count {
let name = body.read_string();
let file_count = body.read_int32();
let mut files = Vec::new();
for _ in 0..file_count {
body.read_int8(); // code
let filename = body.read_string();
let file_size = body.read_int64();
body.read_string(); // extension
let attr_count = body.read_int32();
for _ in 0..attr_count {
body.read_int32();
body.read_int32();
}
files.push((filename, file_size));
}
dirs.push(SharedDirectory { name, files });
}
dirs
}
#[test]
fn shared_file_list_roundtrips() {
let dirs = vec![
SharedDirectory {
name: "music\\album".to_string(),
files: vec![
("song one.flac".to_string(), 123),
("song two.flac".to_string(), 456),
],
},
SharedDirectory {
name: "music".to_string(),
files: vec![("top.mp3".to_string(), 789)],
},
];
let message = build_shared_file_list(&dirs);
// Decode via the same offset the dispatcher would use.
let mut decoded = Message::new_with_data(message.get_buffer());
decoded.set_pointer(8);
assert_eq!(parse_shared_file_list(&mut decoded), dirs);
}
+20
View File
@@ -128,6 +128,26 @@ impl Shares {
&self.files
}
/// Files grouped by their virtual directory (everything before the final
/// backslash), as `(directory, [(basename, size)])`. Used to build a
/// SharedFileListResponse.
#[must_use]
pub fn directories(&self) -> Vec<(String, Vec<(String, u64)>)> {
let mut by_dir: std::collections::BTreeMap<String, Vec<(String, u64)>> =
std::collections::BTreeMap::new();
for file in &self.files {
let (dir, base) = file
.virtual_path
.rsplit_once('\\')
.unwrap_or(("", file.virtual_path.as_str()));
by_dir
.entry(dir.to_string())
.or_default()
.push((base.to_string(), file.size));
}
by_dir.into_iter().collect()
}
#[must_use]
pub const fn file_count(&self) -> u32 {
self.files.len() as u32