Deny unwrap/expect/panic via workspace lints; fix pedantic warnings
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -15,3 +15,6 @@ tempfile = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user