Deny unwrap/expect/panic via workspace lints; fix pedantic warnings

This commit is contained in:
2026-09-24 14:04:03 +02:00
parent 9a61452931
commit f4e614acce
12 changed files with 93 additions and 60 deletions
+3
View File
@@ -15,3 +15,6 @@ tempfile = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
clap = { workspace = true }
[lints]
workspace = true
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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
+7 -3
View File
@@ -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;
+15 -9
View File
@@ -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<PathBuf>,
/// 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)
}
+21 -31
View File
@@ -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<ByteRange>
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
+12 -13
View File
@@ -41,21 +41,20 @@ pub async fn probe(client: &reqwest::Client, url: &TargetUrl) -> Result<ProbeInf
_ => 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,
}
};
+4 -2
View File
@@ -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
}
}
}
@@ -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};
@@ -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;