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:
@@ -6,6 +6,7 @@
|
||||
//! get one `source` line appended to their rc file, marked so `uninstall` can
|
||||
//! take back exactly what was added and nothing else.
|
||||
|
||||
use super::{unwritable, xdg};
|
||||
use crate::cli::{Cli, CompletionsArgs, CompletionsCommand, Shell};
|
||||
use crate::output::{CliError, CliResult, CompletionRecord, Out};
|
||||
use clap::CommandFactory;
|
||||
@@ -39,7 +40,7 @@ struct Target {
|
||||
impl Target {
|
||||
fn record(&self, action: &str) -> CompletionRecord {
|
||||
CompletionRecord {
|
||||
shell: self.shell.name().to_string(),
|
||||
shell: self.shell.generator().to_string(),
|
||||
path: self.script.display().to_string(),
|
||||
action: action.to_string(),
|
||||
}
|
||||
@@ -51,14 +52,6 @@ impl Target {
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Bash => "bash",
|
||||
Self::Zsh => "zsh",
|
||||
Self::Fish => "fish",
|
||||
}
|
||||
}
|
||||
|
||||
const fn generator(self) -> clap_complete::Shell {
|
||||
match self {
|
||||
Self::Bash => clap_complete::Shell::Bash,
|
||||
@@ -213,14 +206,6 @@ impl Dirs {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shells follow 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 write_script(path: &Path, script: &[u8]) -> CliResult<bool> {
|
||||
if std::fs::read(path).is_ok_and(|found| found == script) {
|
||||
return Ok(false);
|
||||
@@ -288,10 +273,6 @@ fn create_parent(path: &Path) -> CliResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn unwritable(path: &Path, error: &std::io::Error) -> CliError {
|
||||
CliError::usage(format!("cannot write {}: {error}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -47,14 +47,9 @@ fn own_privileges(client: &dyn crate::api::SessionApi) -> Option<u32> {
|
||||
if client.check_privileges().is_err() {
|
||||
return None;
|
||||
}
|
||||
let deadline = Instant::now() + PRIVILEGE_TIMEOUT;
|
||||
while Instant::now() < deadline {
|
||||
if let Some(seconds) = client.own_privilege_seconds() {
|
||||
return Some(seconds);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
None
|
||||
super::poll_until(PRIVILEGE_TIMEOUT, Duration::from_millis(50), || {
|
||||
client.own_privilege_seconds()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn user(ctx: &Ctx, args: &UserArgs) -> CliResult {
|
||||
@@ -141,7 +136,7 @@ pub fn serve(
|
||||
.warn("the share index is empty — peers will find nothing");
|
||||
}
|
||||
|
||||
let span = (!args.follow).then(|| Duration::from_secs(args.duration));
|
||||
let span = super::listen_span(args.duration, args.follow);
|
||||
ctx.out.status(&match span {
|
||||
Some(span) => format!("online for {}s…", span.as_secs()),
|
||||
None => "online until interrupted…".to_string(),
|
||||
@@ -155,20 +150,10 @@ pub fn serve(
|
||||
// served well inside one poll, and "it had to wait" is precisely what a
|
||||
// transfer log exists to report.
|
||||
for queued in session.client.take_upload_events() {
|
||||
let record = upload_record(&queued);
|
||||
let key = (record.user.clone(), record.path.clone());
|
||||
if reported.get(&key) != Some(&record) {
|
||||
ctx.out.emit(&record);
|
||||
reported.insert(key, record);
|
||||
}
|
||||
emit_changed(&mut reported, upload_record(&queued), &ctx.out);
|
||||
}
|
||||
for upload in session.client.uploads() {
|
||||
let record = upload_record(&upload);
|
||||
let key = (record.user.clone(), record.path.clone());
|
||||
if reported.get(&key) != Some(&record) {
|
||||
ctx.out.emit(&record);
|
||||
reported.insert(key, record);
|
||||
}
|
||||
emit_changed(&mut reported, upload_record(&upload), &ctx.out);
|
||||
}
|
||||
if let Some(sweeper) = sweeper.as_mut() {
|
||||
sweeper.tick(ctx, session.client.as_ref());
|
||||
@@ -211,8 +196,7 @@ fn upload_record(upload: &UploadInfo) -> UploadRecord {
|
||||
}
|
||||
|
||||
/// Emit `record` only when it differs from the last one reported for that
|
||||
/// (user, file). Exposed for tests; the serve loop inlines the same rule.
|
||||
#[cfg(test)]
|
||||
/// (user, file).
|
||||
fn emit_changed(
|
||||
reported: &mut HashMap<(String, String), UploadRecord>,
|
||||
record: UploadRecord,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! inside it and is written out on demand. Installing over an older copy is
|
||||
//! what updating means, which is why there is no separate verb for it.
|
||||
|
||||
use super::{unwritable, xdg};
|
||||
use crate::cli::{SkillsArgs, SkillsCommand};
|
||||
use crate::output::{CliError, CliResult, Out, SkillRecord};
|
||||
use directories::UserDirs;
|
||||
@@ -122,7 +123,7 @@ fn targets(args: &SkillsArgs) -> CliResult<Vec<Target>> {
|
||||
)
|
||||
})?;
|
||||
|
||||
let found = detected(&home, &config_home(&home));
|
||||
let found = detected(&home, &xdg("XDG_CONFIG_HOME", &home, ".config"));
|
||||
if found.is_empty() {
|
||||
return Err(CliError::no_results(
|
||||
"no coding agent found in your home directory; name a skills \
|
||||
@@ -149,15 +150,6 @@ fn detected(home: &Path, config_home: &Path) -> Vec<Target> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where an agent keeps its own configuration, which is the XDG location on
|
||||
/// every platform — unlike this client's config, which follows each platform's
|
||||
/// own convention (see `persist::paths`).
|
||||
fn config_home(home: &Path) -> PathBuf {
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.filter(|dir| !dir.is_empty())
|
||||
.map_or_else(|| home.join(".config"), PathBuf::from)
|
||||
}
|
||||
|
||||
fn current(file: &Path) -> bool {
|
||||
std::fs::read_to_string(file).is_ok_and(|found| found == SKILL)
|
||||
}
|
||||
@@ -167,10 +159,6 @@ fn ours(file: &Path) -> bool {
|
||||
.is_ok_and(|found| found.contains(FRONTMATTER_NAME))
|
||||
}
|
||||
|
||||
fn unwritable(path: &Path, error: &std::io::Error) -> CliError {
|
||||
CliError::usage(format!("cannot write {}: {error}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -39,17 +39,13 @@ pub fn fetch_listing(
|
||||
CliError::connection(format!("cannot ask {user} for a listing: {e}"))
|
||||
})?;
|
||||
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
if let Some(directories) = client.take_browse_result(user) {
|
||||
return Ok(directories);
|
||||
}
|
||||
std::thread::sleep(POLL);
|
||||
}
|
||||
Err(CliError::timeout(format!(
|
||||
"{user} did not send a file list within {}s",
|
||||
timeout.as_secs()
|
||||
)))
|
||||
super::poll_until(timeout, POLL, || client.take_browse_result(user))
|
||||
.ok_or_else(|| {
|
||||
CliError::timeout(format!(
|
||||
"{user} did not send a file list within {}s",
|
||||
timeout.as_secs()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn browse(ctx: &Ctx, args: &BrowseArgs) -> CliResult {
|
||||
@@ -143,22 +139,18 @@ pub fn room_users(ctx: &Ctx, room: &str, timeout: Duration) -> CliResult {
|
||||
ctx.out
|
||||
.status(&format!("waiting for {room}'s member list…"));
|
||||
|
||||
let deadline = Instant::now() + timeout;
|
||||
let mut members = Vec::new();
|
||||
while Instant::now() < deadline && members.is_empty() {
|
||||
members = session.client.room_members(room);
|
||||
if members.is_empty() {
|
||||
std::thread::sleep(POLL);
|
||||
}
|
||||
}
|
||||
let listed = super::poll_until(timeout, POLL, || {
|
||||
let members = session.client.room_members(room);
|
||||
(!members.is_empty()).then_some(members)
|
||||
});
|
||||
let _ = session.client.leave_room(room);
|
||||
|
||||
if members.is_empty() {
|
||||
let Some(members) = listed else {
|
||||
return Err(CliError::timeout(format!(
|
||||
"{room} sent no member list within {}s",
|
||||
timeout.as_secs()
|
||||
)));
|
||||
}
|
||||
};
|
||||
for user in members {
|
||||
ctx.out.emit(&RoomMemberRecord {
|
||||
room: room.to_string(),
|
||||
|
||||
@@ -310,13 +310,9 @@ fn collect(
|
||||
})
|
||||
};
|
||||
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
if client.get_search_results_count(query) > 0 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
super::poll_until(timeout, Duration::from_millis(100), || {
|
||||
(client.get_search_results_count(query) > 0).then_some(())
|
||||
});
|
||||
cancel.store(true, Ordering::Relaxed);
|
||||
let _ = worker.join();
|
||||
Ok(client.get_search_results(query))
|
||||
@@ -334,29 +330,26 @@ fn dedupe(
|
||||
download_dir: &str,
|
||||
out: &Out,
|
||||
) -> Vec<Request> {
|
||||
let mut paths: HashMap<String, String> = HashMap::new();
|
||||
let mut targets: HashMap<PathBuf, String> = HashMap::new();
|
||||
let mut targets: HashMap<PathBuf, Request> = HashMap::new();
|
||||
let mut kept = Vec::new();
|
||||
for request in requests {
|
||||
if let Some(user) = paths.get(&request.path) {
|
||||
out.warn(&format!(
|
||||
"skipping {} from {}: same remote path already queued from \
|
||||
{user}",
|
||||
request.path, request.user
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let target = local_path(download_dir, &request.path);
|
||||
if let Some(other) = targets.get(&target) {
|
||||
if let Some(queued) = targets.get(&target) {
|
||||
let collision = if queued.path == request.path {
|
||||
format!("same remote path already queued from {}", queued.user)
|
||||
} else {
|
||||
format!(
|
||||
"would overwrite the file already queued from {}",
|
||||
queued.user
|
||||
)
|
||||
};
|
||||
out.warn(&format!(
|
||||
"skipping {} from {}: would overwrite the file already queued \
|
||||
from {other}",
|
||||
"skipping {} from {}: {collision}",
|
||||
request.path, request.user
|
||||
));
|
||||
continue;
|
||||
}
|
||||
paths.insert(request.path.clone(), request.user.clone());
|
||||
targets.insert(target, request.user.clone());
|
||||
targets.insert(target, request.clone());
|
||||
kept.push(request);
|
||||
}
|
||||
kept
|
||||
@@ -617,9 +610,8 @@ fn read_requests(
|
||||
}
|
||||
|
||||
/// `Ok(None)` for a blank line, `Err` for something unusable.
|
||||
// aislop-ignore-next-line complexity/function-too-long -- scanner bug, not a long function: the `'{'` char literal below reads as an opening brace, so aislop's counter never closes and measures to EOF. It is 34 lines.
|
||||
// aislop-ignore-next-line complexity/function-too-long -- scanner bug, not a long function: the `'{'` char literal below reads as an opening brace, so aislop's counter never closes and measures to EOF. It is 33 lines.
|
||||
fn parse_request(line: &str) -> Result<Option<Request>, String> {
|
||||
let line = line.trim_end_matches(['\r', '\n']);
|
||||
if line.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -68,22 +68,13 @@ pub fn run(ctx: &Ctx, store: &Store, args: &WishRunArgs) -> CliResult {
|
||||
/// How to narrow and order what a sweep prints — the parts of `wish run`'s flags
|
||||
/// that outlive the command, so `serve --wishlist` can sweep without inventing
|
||||
/// a set of command-line arguments it never received.
|
||||
#[derive(Default)]
|
||||
struct Sweep {
|
||||
filter: transfer::Filter,
|
||||
sort: SortKey,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl Default for Sweep {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter: transfer::Filter::default(),
|
||||
sort: SortKey::Best,
|
||||
limit: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WishRunArgs> for Sweep {
|
||||
fn from(args: &WishRunArgs) -> Self {
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user