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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user