refactor: cut what a whole-repo audit proved dead, padded, or hand-rolled

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.
This commit is contained in:
Michel de Graaf
2026-07-28 12:52:18 +02:00
parent cd56fb3b39
commit f1b667f7f9
62 changed files with 1367 additions and 2319 deletions
+47 -21
View File
@@ -22,6 +22,7 @@ use crate::remote::{Endpoint, RemoteSession};
use soulseek_rs::types::{RoomEvent, RoomInfo};
use soulseek_rs::{Client, ClientSettings, SessionLoss};
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant};
@@ -223,6 +224,26 @@ fn login(settings: ClientSettings, address: &str) -> CliResult<Client> {
}
}
/// Ask `f` every `poll` until it answers or `timeout` runs out.
///
/// The library reports what the network sent by handing it over on the next
/// call, so a command that wants one answer has to keep asking. Checking first
/// and sleeping after means an answer already waiting costs nothing.
fn poll_until<T>(
timeout: Duration,
poll: Duration,
mut f: impl FnMut() -> Option<T>,
) -> Option<T> {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if let Some(answer) = f() {
return Some(answer);
}
std::thread::sleep(poll);
}
None
}
/// Ask for the room list and wait for the server's reply.
///
/// Events already queued are discarded first: servers push a room list at
@@ -235,21 +256,15 @@ pub fn await_room_list(
let _ = client.take_room_events();
client.request_room_list().ok()?;
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let listed =
client.take_room_events().into_iter().find_map(
|event| match event {
RoomEvent::List(rooms) => Some(rooms),
_ => None,
},
);
if listed.is_some() {
return listed;
}
std::thread::sleep(poll);
}
None
poll_until(timeout, poll, || {
client
.take_room_events()
.into_iter()
.find_map(|event| match event {
RoomEvent::List(rooms) => Some(rooms),
_ => None,
})
})
}
/// Wait until the server has answered something we asked for after `send`.
@@ -295,12 +310,10 @@ fn dispatch(ctx: &Ctx, command: Commands) -> CliResult {
}
Commands::User(args) => peer::user(ctx, &args),
Commands::Whoami => peer::whoami(ctx),
// These need no account, so main.rs answers them before asking for
// credentials; the arms exist only to keep the match exhaustive.
Commands::Portmap => portmap(&ctx.out, ctx.settings.listen_port),
// These need the config file as well as a session, so main.rs runs
// them where the store is in scope.
Commands::Serve(_)
// main.rs runs these itself: some need no account, the rest need the
// config store in scope. The arms only keep the match exhaustive.
Commands::Portmap
| Commands::Serve(_)
| Commands::Daemon(_)
| Commands::Wish(_)
| Commands::Config(_)
@@ -317,6 +330,19 @@ fn listen_span(duration: u64, follow: bool) -> Option<Duration> {
(!follow).then(|| Duration::from_secs(duration))
}
/// Where shells and coding agents keep their configuration: the XDG layout on
/// every platform, unlike this client's own config, which follows each
/// platform's convention (see `persist::paths`).
fn xdg(variable: &str, home: &Path, fallback: &str) -> PathBuf {
std::env::var_os(variable)
.filter(|dir| !dir.is_empty())
.map_or_else(|| home.join(fallback), PathBuf::from)
}
fn unwritable(path: &Path, error: &std::io::Error) -> CliError {
CliError::usage(format!("cannot write {}: {error}", path.display()))
}
/// Probe automatic port mapping. A router that does not answer is a negative
/// answer, not a crash, so it exits 4 the way `grep` exits 1 on no match.
pub fn portmap(out: &Out, port: u16) -> CliResult {