Seven region audits, every finding adversarially verified against the whole workspace before being applied; 80 landed and the two that failed in application were refuted honestly (ParseError has a from_str_radix producer the audit missed; NatPmpError's Display is the only reader of its diagnostic payloads). The bigger cuts: - The NewPeer/PierceFireWall cluster: an operation variant nothing ever sent, its 77-line arm, a protocol-wrong Client::pierce_firewall that wrote a peer-init frame down the server socket, and the struct only they used. The live paths in peer/listen.rs are untouched. - The ops loop reads `for operation in reader` instead of hand-rolling recv/break, de-indenting the whole dispatch body. - Dead lib surface: Message::print_hex and four unused accessors, UploadFailed, registry and actor getters, build_watch_user, strip_ansi_codes, delegating shims on ClientContext. Removing public items makes this a semver major, which the release pipeline enforces. - Never-used flexibility: the state-migration framework that never held a migration, border-type helpers that all answered Rounded, the TUI's max_concurrent_downloads field nothing read. - Convergence: six poll loops into poll_until, three wrap-around navigation blocks into cycle, hit tests into Rect::contains, char walkers into floor/ceil_char_boundary, five Download literals into one helper, the suites' shared scaffolding into tests/common. Deliberately not taken: swapping the hand-rolled zlib inflate and MD5 for crates (they replace wire-facing machinery whose tolerance of real-world peer quirks no local test exercises) and pulling chrono into the lib for a log timestamp. Recorded for their own decision. Net -1002 lines. Both e2e modes stay green: 94 CLI tests each way, 16 protocol, 33 lib, unit suites, clippy pedantic clean, aislop 100/100 with the four scanner false positives suppressed inline where they fire.
124 lines
3.6 KiB
Rust
124 lines
3.6 KiB
Rust
use crate::{
|
|
actor::server_actor::ServerMessage,
|
|
message::{Message, MessageHandler},
|
|
types::RoomInfo,
|
|
};
|
|
use std::sync::mpsc::Sender;
|
|
|
|
pub struct RoomListHandler;
|
|
|
|
impl MessageHandler<ServerMessage> for RoomListHandler {
|
|
fn get_code(&self) -> u8 {
|
|
64
|
|
}
|
|
|
|
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
|
let rooms = parse_room_list(message);
|
|
let _ = sender.send(ServerMessage::RoomListReceived(rooms));
|
|
}
|
|
}
|
|
|
|
/// Parse the public rooms out of a `RoomList` (code 64) message: a vector of
|
|
/// room names followed by a vector of user counts. The remaining private-room
|
|
/// sections are ignored. `message` must be positioned at the payload (the
|
|
/// dispatcher sets pointer 8).
|
|
#[must_use]
|
|
pub fn parse_room_list(message: &mut Message) -> Vec<RoomInfo> {
|
|
let name_count = message.read_int32();
|
|
let mut names = Vec::new();
|
|
for _ in 0..name_count {
|
|
// Stop once the payload can't hold another field: a bogus (possibly
|
|
// hostile) count must not spin us into an OOM allocation loop. A
|
|
// 4-byte floor also avoids read_int32 stalling on a 1-3 byte tail.
|
|
if message.get_pointer() + 4 > message.get_size() {
|
|
break;
|
|
}
|
|
names.push(message.read_string());
|
|
}
|
|
let count_count = message.read_int32();
|
|
let mut counts = Vec::new();
|
|
for _ in 0..count_count {
|
|
if message.get_pointer() + 4 > message.get_size() {
|
|
break;
|
|
}
|
|
counts.push(message.read_int32());
|
|
}
|
|
names
|
|
.into_iter()
|
|
.zip(counts)
|
|
.map(|(name, user_count)| RoomInfo { name, user_count })
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::message::framed;
|
|
|
|
#[test]
|
|
fn parses_names_zipped_with_counts() {
|
|
let mut message = framed(|m| {
|
|
m.write_int32(2);
|
|
m.write_string("nicotine");
|
|
m.write_string("jazz");
|
|
m.write_int32(2);
|
|
m.write_int32(42);
|
|
m.write_int32(7);
|
|
});
|
|
let rooms = parse_room_list(&mut message);
|
|
assert_eq!(
|
|
rooms,
|
|
vec![
|
|
RoomInfo {
|
|
name: "nicotine".to_string(),
|
|
user_count: 42
|
|
},
|
|
RoomInfo {
|
|
name: "jazz".to_string(),
|
|
user_count: 7
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_room_list_parses_to_empty() {
|
|
let mut message = framed(|m| {
|
|
m.write_int32(0);
|
|
m.write_int32(0);
|
|
});
|
|
assert!(parse_room_list(&mut message).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_counts_do_not_hang_or_overallocate() {
|
|
// A tiny frame claiming ~4 billion names/counts must return promptly
|
|
// (bounded by the payload) rather than looping into an OOM.
|
|
let mut message = framed(|m| {
|
|
m.write_int32(u32::MAX);
|
|
});
|
|
let rooms = parse_room_list(&mut message);
|
|
assert!(rooms.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn handler_forwards_parsed_rooms() {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_int32(1);
|
|
m.write_string("room");
|
|
m.write_int32(1);
|
|
m.write_int32(5);
|
|
});
|
|
RoomListHandler.handle(&mut message, tx);
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::RoomListReceived(rooms)) => {
|
|
assert_eq!(rooms.len(), 1);
|
|
assert_eq!(rooms[0].name, "room");
|
|
assert_eq!(rooms[0].user_count, 5);
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
}
|