From f4e614acce7b51684d7c719dfb59f43e5ffb0224 Mon Sep 17 00:00:00 2001 From: Ben_Kosytorz Date: Thu, 24 Sep 2026 14:04:03 +0200 Subject: [PATCH] Deny unwrap/expect/panic via workspace lints; fix pedantic warnings --- server/Cargo.toml | 10 +++++ server/README.md | 13 ++++++ server/crates/sn-fetch/Cargo.toml | 3 ++ server/crates/sn-fetch/src/exec.rs | 2 +- server/crates/sn-fetch/src/exit.rs | 2 +- server/crates/sn-fetch/src/lib.rs | 10 +++-- server/crates/sn-fetch/src/main.rs | 24 ++++++---- server/crates/sn-fetch/src/plan.rs | 52 +++++++++------------- server/crates/sn-fetch/src/probe.rs | 25 +++++------ server/crates/sn-fetch/src/runner.rs | 6 ++- server/crates/sn-fetch/tests/common/mod.rs | 3 ++ server/crates/sn-fetch/tests/roundtrip.rs | 3 ++ 12 files changed, 93 insertions(+), 60 deletions(-) diff --git a/server/Cargo.toml b/server/Cargo.toml index 88279da..f39d75d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,6 +7,16 @@ version = "0.1.0" edition = "2021" license = "AGPL-3.0-only" +[workspace.lints.rust] +unsafe_code = "deny" # also #![forbid(unsafe_code)] in every crate root + +[workspace.lints.clippy] +all = { level = "deny", priority = -1 } +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" +unreachable = "deny" + [workspace.dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } diff --git a/server/README.md b/server/README.md index 4eb47ee..61a4a8a 100644 --- a/server/README.md +++ b/server/README.md @@ -86,6 +86,19 @@ Kleinster brauchbarer Schnitt: Der Code soll so sicher sein, dass ganze Fehlerklassen schon beim Kompilieren unmöglich werden. Verbindliche Regeln für jede Crate in `server/`: +**Maschinell erzwungen** (Workspace-Lints in `server/Cargo.toml`, in jeder +Crate via `[lints] workspace = true` aktiviert): + +- `unsafe_code = "deny"` + `#![forbid(unsafe_code)]` im Crate-Root +- `clippy::unwrap_used = "deny"` — kein `unwrap()` (Panic-Quelle, kein Test) +- `clippy::expect_used = "deny"` — kein `expect()` (Panic-Quelle, kein Test) +- `clippy::panic = "deny"`, `clippy::unreachable = "deny"` +- `clippy::all = "deny"`, `clippy::pedantic = "warn"` (Deny-Ziel vor Release) + +Verifiziert: ein eingebautes `unwrap()` in `lib.rs` wurde von Clippy mit +`error: used unwrap() ... #unwrap_used` abgelehnt. Tests dürfen +`unwrap()`/`expect()` (explizites `#![allow(...)]` pro Test-Crate). + 1. **`#![forbid(unsafe_code)]`** in jedem Crate-Root (und `unwrap`, `expect` nur in `#[cfg(test)]`-Kontext). 2. **Newtypes statt roher Primitive** für alles, was Bedeutung hat: diff --git a/server/crates/sn-fetch/Cargo.toml b/server/crates/sn-fetch/Cargo.toml index da3cfa0..bdb12ef 100644 --- a/server/crates/sn-fetch/Cargo.toml +++ b/server/crates/sn-fetch/Cargo.toml @@ -15,3 +15,6 @@ tempfile = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } clap = { workspace = true } + +[lints] +workspace = true diff --git a/server/crates/sn-fetch/src/exec.rs b/server/crates/sn-fetch/src/exec.rs index ea8da01..711726d 100644 --- a/server/crates/sn-fetch/src/exec.rs +++ b/server/crates/sn-fetch/src/exec.rs @@ -145,7 +145,7 @@ pub async fn assemble( for part_path in &part_paths { let mut part = tokio::fs::File::open(part_path).await?; - let mut buffer = [0u8; 64 * 1024]; + let mut buffer = vec![0u8; 64 * 1024]; loop { let n = tokio::io::AsyncReadExt::read(&mut part, &mut buffer).await?; if n == 0 { diff --git a/server/crates/sn-fetch/src/exit.rs b/server/crates/sn-fetch/src/exit.rs index 58f291a..92ebe14 100644 --- a/server/crates/sn-fetch/src/exit.rs +++ b/server/crates/sn-fetch/src/exit.rs @@ -1,7 +1,7 @@ //! Exit abstraction: an "exit" is one internet uplink through which //! segment requests can be sent. //! -//! PoC v0 ships only [`LocalExit`] (the household's own connection, no +//! `PoC` v0 ships only [`LocalExit`] (the household's own connection, no //! detour). The registry/trait structure is deliberately ready so a //! future exit implementation can route HTTP through a neighbour over //! the mesh (e.g. via a per-exit proxy or tunnel) without changing the diff --git a/server/crates/sn-fetch/src/lib.rs b/server/crates/sn-fetch/src/lib.rs index 197ce2c..346eeb9 100644 --- a/server/crates/sn-fetch/src/lib.rs +++ b/server/crates/sn-fetch/src/lib.rs @@ -1,13 +1,17 @@ -//! sn-fetch — segmented multi-exit downloader for SpiderNet. +//! sn-fetch — segmented multi-exit downloader for `SpiderNet`. //! -//! PoC v0: probes a URL for HTTP range support, plans weighted byte-range +//! `PoC` v0: probes a URL for HTTP range support, plans weighted byte-range //! segments across exits, downloads segments in parallel and reassembles //! the target file with SHA-256 verification. //! -//! SpiderNet software carries no warranty and accepts no responsibility +//! `SpiderNet` software carries no warranty and accepts no responsibility //! for its usage or for transported content (AGPL-3.0, sections 15/16). #![forbid(unsafe_code)] +#![warn(clippy::pedantic)] +// deny is the goal before the first release +// PoC scope: doc-annotation noise, not correctness. Revisit at release. +#![allow(clippy::must_use_candidate, clippy::missing_errors_doc)] pub mod error; pub mod exec; diff --git a/server/crates/sn-fetch/src/main.rs b/server/crates/sn-fetch/src/main.rs index fd323cb..be24782 100644 --- a/server/crates/sn-fetch/src/main.rs +++ b/server/crates/sn-fetch/src/main.rs @@ -1,10 +1,13 @@ -//! CLI entry point for the sn-fetch PoC. +//! CLI entry point for the sn-fetch `PoC`. //! //! KEINE GARANTIE — this software is provided "as is" without warranty of //! any kind (AGPL-3.0, sections 15/16). Users are solely responsible for //! what they transmit or receive with it. #![forbid(unsafe_code)] +#![warn(clippy::pedantic)] +// PoC scope: doc-annotation noise, not correctness. Revisit at release. +#![allow(clippy::must_use_candidate, clippy::missing_errors_doc)] use std::num::{NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}; use std::path::PathBuf; @@ -26,7 +29,7 @@ struct Args { output: Option, /// Exits as `id=weight` pairs, comma separated, e.g. `local=1,local2=2`. - /// PoC: all ids map to plain local connections; the weights only shape + /// `PoC`: all ids map to plain local connections; the weights only shape /// the plan. Multi-exit routing comes with the mesh integration. #[arg(short = 'e', long = "exit", default_value = "local=1")] exits: String, @@ -79,8 +82,7 @@ fn default_output(url: &TargetUrl) -> PathBuf { let name = segments .next_back() .filter(|name| !name.is_empty()) - .map(str::to_string) - .unwrap_or_else(|| "download.bin".to_string()); + .map_or_else(|| "download.bin".to_string(), str::to_string); PathBuf::from(name) } @@ -165,14 +167,12 @@ async fn run() -> Result<(), String> { println!( "time: {:.2?} s → effective {:.1} MiB/s", report.wall_time.as_secs_f64(), - report.file_size.value() as f64 / 1024.0 / 1024.0 / report.wall_time.as_secs_f64() + bytes_to_mib(report.file_size.value()) / report.wall_time.as_secs_f64() ); println!("segments:"); for segment in &report.segments { - let mib_per_s = segment.bytes as f64 - / 1024.0 - / 1024.0 - / segment.duration.as_secs_f64().max(f64::EPSILON); + let mib_per_s = + bytes_to_mib(segment.bytes) / segment.duration.as_secs_f64().max(f64::EPSILON); println!( " #{}: {} bytes in {:.2?} s ({mib_per_s:.1} MiB/s)", segment.index, segment.bytes, segment.duration @@ -180,3 +180,9 @@ async fn run() -> Result<(), String> { } Ok(()) } + +/// Bytes to MiB (display-only; precision loss is irrelevant here). +#[allow(clippy::cast_precision_loss)] +fn bytes_to_mib(bytes: u64) -> f64 { + bytes as f64 / (1024.0 * 1024.0) +} diff --git a/server/crates/sn-fetch/src/plan.rs b/server/crates/sn-fetch/src/plan.rs index 0195afc..f5ffc08 100644 --- a/server/crates/sn-fetch/src/plan.rs +++ b/server/crates/sn-fetch/src/plan.rs @@ -57,8 +57,7 @@ impl FetchPlan { .iter() .enumerate() .min_by_key(|(_, (id, weight))| (weight.value(), std::cmp::Reverse(id.value()))) - .map(|(pos, _)| pos) - .unwrap_or(0); + .map_or(0, |(pos, _)| pos); exits.remove(weakest); } // Deterministic boundary assignment order. @@ -120,15 +119,11 @@ impl FetchPlan { // The last exit covers to the end, so nothing can be left // over; stay honest and extend the last assignment anyway. if start < file_size.value() { - match assignments.last_mut() { - Some((last_range, _)) => { - *last_range = - ByteRange::try_new(last_range.start(), file_size.value() - 1)?; - } - None => { - let only = ByteRange::try_new(0, file_size.value() - 1)?; - assignments.push((only, exits[0].0)); - } + if let Some((last_range, _)) = assignments.last_mut() { + *last_range = ByteRange::try_new(last_range.start(), file_size.value() - 1)?; + } else { + let only = ByteRange::try_new(0, file_size.value() - 1)?; + assignments.push((only, exits[0].0)); } } } @@ -212,7 +207,7 @@ fn even_subsplit(start: u64, end_inclusive: u64, parts: usize) -> Vec let mut out = Vec::with_capacity(parts); let mut cursor = start; for i in 0..parts { - let len = base + if (i as u64) < remainder { 1 } else { 0 }; + let len = base + u64::from((i as u64) < remainder); let range_end = cursor + len - 1; match ByteRange::try_new(cursor, range_end) { Ok(range) => out.push(range), @@ -243,36 +238,31 @@ fn merge_tiny_segments( let merged: (ByteRange, ExitId) = if pos + 1 < assignments.len() { // Merge into the next segment, keeping its exit. let next = &assignments[pos + 1]; - let range = match ByteRange::try_new(assignments[pos].0.start(), next.0.end_inclusive()) - { - Ok(range) => range, + let Ok(range) = ByteRange::try_new(assignments[pos].0.start(), next.0.end_inclusive()) + else { // Unreachable: next.start == pos.end + 1, so the merged - // range is always well-formed. Drop both segments instead - // of panicking; validate() would reject a broken plan. - Err(_) => break, + // range is always well-formed. Stop instead of panicking; + // validate() would reject a broken plan. + break; }; (range, next.1) } else { // Last segment is too small: merge into the previous one. let prev_pos = pos - 1; let prev = &assignments[prev_pos]; - let range = match ByteRange::try_new(prev.0.start(), assignments[pos].0.end_inclusive()) - { - Ok(range) => range, - Err(_) => break, + let Ok(range) = ByteRange::try_new(prev.0.start(), assignments[pos].0.end_inclusive()) + else { + break; }; (range, prev.1) }; - match pos + 1 < assignments.len() { - true => { - assignments[pos + 1] = merged; - assignments.remove(pos); - } - false => { - assignments[pos - 1] = merged; - assignments.remove(pos); - } + if pos + 1 < assignments.len() { + assignments[pos + 1] = merged; + assignments.remove(pos); + } else { + assignments[pos - 1] = merged; + assignments.remove(pos); } } assignments diff --git a/server/crates/sn-fetch/src/probe.rs b/server/crates/sn-fetch/src/probe.rs index 6747a1f..f323d70 100644 --- a/server/crates/sn-fetch/src/probe.rs +++ b/server/crates/sn-fetch/src/probe.rs @@ -41,21 +41,20 @@ pub async fn probe(client: &reqwest::Client, url: &TargetUrl) -> Result None, }; - let range_support = match advertised { - Some(support) => support, + let range_support = if let Some(support) = advertised { + support + } else { // HEAD was inconclusive: ask directly with a 1-byte range. - None => { - let resp = client - .get(url.inner().clone()) - .header(reqwest::header::RANGE, "bytes=0-0") - .send() - .await?; + let resp = client + .get(url.inner().clone()) + .header(reqwest::header::RANGE, "bytes=0-0") + .send() + .await?; - match resp.status() { - StatusCode::PARTIAL_CONTENT => RangeSupport::Supported, - StatusCode::OK => RangeSupport::Unsupported, - _ => RangeSupport::Unknown, - } + match resp.status() { + StatusCode::PARTIAL_CONTENT => RangeSupport::Supported, + StatusCode::OK => RangeSupport::Unsupported, + _ => RangeSupport::Unknown, } }; diff --git a/server/crates/sn-fetch/src/runner.rs b/server/crates/sn-fetch/src/runner.rs index 2e73ab6..3e2eb2d 100644 --- a/server/crates/sn-fetch/src/runner.rs +++ b/server/crates/sn-fetch/src/runner.rs @@ -41,13 +41,15 @@ pub async fn run( let info = probe::probe(&probe_client, target).await?; match info.range_support { - RangeSupport::Supported => run_ranged(target, options, exits, parts_dir, info).await, + RangeSupport::Supported => { + Box::pin(run_ranged(target, options, exits, parts_dir, info)).await + } RangeSupport::Unsupported | RangeSupport::Unknown => { tracing::warn!( "server does not (confirmably) support range requests; \ falling back to single-stream download without pooling" ); - run_whole(target, options, exits, parts_dir, info).await + Box::pin(run_whole(target, options, exits, parts_dir, info)).await } } } diff --git a/server/crates/sn-fetch/tests/common/mod.rs b/server/crates/sn-fetch/tests/common/mod.rs index d607876..57efbc0 100644 --- a/server/crates/sn-fetch/tests/common/mod.rs +++ b/server/crates/sn-fetch/tests/common/mod.rs @@ -2,6 +2,9 @@ //! optional range handling (configurable), `Connection: close` semantics. #![forbid(unsafe_code)] +// Tests are allowed to unwrap/expect; production code denies them via +// workspace lints (server/Cargo.toml). +#![allow(clippy::unwrap_used, clippy::expect_used)] use std::io::{BufRead, BufReader, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; diff --git a/server/crates/sn-fetch/tests/roundtrip.rs b/server/crates/sn-fetch/tests/roundtrip.rs index 533fae0..e629bcb 100644 --- a/server/crates/sn-fetch/tests/roundtrip.rs +++ b/server/crates/sn-fetch/tests/roundtrip.rs @@ -3,6 +3,9 @@ //! the fallback path for range-hostile servers. #![forbid(unsafe_code)] +// Tests are allowed to unwrap/expect; production code denies them via +// workspace lints (server/Cargo.toml). +#![allow(clippy::unwrap_used, clippy::expect_used)] mod common;