From 2d5235f0b794006e2a3d0492122a43966cd85c45 Mon Sep 17 00:00:00 2001 From: Robin B Date: Sun, 13 Sep 2026 09:42:09 +0200 Subject: [PATCH 1/2] fix: validate measurements and bound speed test runs --- Cargo.lock | 71 +++- Cargo.toml | 2 + README.md | 40 +++ src/lib.rs | 1 + src/main.rs | 63 +++- src/measurements.rs | 75 ++++- src/run.rs | 293 +++++++++++++++++ src/speedtest.rs | 756 ++++++++++++++++++++++++++++++++++--------- tests/p1_cli.rs | 341 +++++++++++++++++++ tests/support/mod.rs | 176 ++++++++++ 10 files changed, 1635 insertions(+), 183 deletions(-) create mode 100644 src/run.rs create mode 100644 tests/p1_cli.rs create mode 100644 tests/support/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 99520ff..3ff2173 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -144,7 +153,9 @@ dependencies = [ "clap", "clap_complete", "csv", + "ctrlc", "env_logger", + "httpdate", "indexmap", "jiff", "log", @@ -259,6 +270,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "deranged" version = "0.5.8" @@ -268,6 +290,18 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -459,6 +493,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -713,9 +753,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "litemap" @@ -758,12 +798,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.4" diff --git a/Cargo.toml b/Cargo.toml index fff3793..fcbad34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,5 @@ serde_json = { version = "1.0", features = ["preserve_order"] } indexmap = "2.13" clap_complete = "4.5" jiff = "0.2" +httpdate = "1.0" +ctrlc = { version = "3.5", features = ["termination"] } diff --git a/README.md b/README.md index a05950d..81d26b2 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,12 @@ Options: Test upload speed only --generate-completion Generate shell completion script for the specified shell [possible values: bash, elvish, fish, powershell, zsh] + --server + Base URL of a compatible Cloudflare speed-test service [default: https://speed.cloudflare.com] + --max-duration + Maximum total run duration in seconds [default: 120] + --max-retry-wait + Maximum cumulative retry wait in seconds [default: 30] -h, --help Print help -V, --version @@ -60,6 +66,40 @@ Example usage: Example with json-pretty output: [![asciicast](https://asciinema.org/a/P6IUAADtaCq3bT18GbYVHmksA.svg)](https://asciinema.org/a/P6IUAADtaCq3bT18GbYVHmksA) +### Run limits and failures + +Runs have a shared 120-second measurement deadline and a 30-second cumulative retry-wait budget by default. Both limits cover the entire run, including metadata, latency, and both transfer directions. Each request is limited to the smaller of 30 seconds and the remaining run time. + +```sh +cfspeedtest --max-duration 60 --max-retry-wait 10 -o json +``` + +`--max-duration` and `--max-retry-wait` are in seconds. Setting the retry-wait budget to zero prevents positive retry waits. The client accepts both seconds and HTTP dates in `Retry-After`; when the requested delay exceeds either remaining budget, it stops and retains completed measurements instead of retrying early. `--disable-dynamic-max-payload-size` does not disable these limits. + +Ctrl-C stops new measurements and interrupts retry waits. An in-flight blocking request may take up to its remaining request timeout to finish; completed samples are retained. On Unix, SIGTERM and SIGHUP use the same graceful cancellation path. + +| Exit code | Meaning | +| --- | --- | +| 0 | Complete: every attempted payload reached its successful-sample target and all enabled latency probes succeeded. Payloads omitted by normal adaptive stopping do not make a run partial. | +| 1 | Failed: no valid throughput samples were collected, or CLI initialization failed. | +| 2 | Invalid command-line arguments. | +| 3 | Partial: some valid throughput samples exist, but a sample target was missed or a run limit stopped testing. | +| 130 | Cancelled; any completed measurements are retained. | + +Metadata is optional: an unavailable or invalid trace response is reported as an error, but does not by itself make a successful measurement run fail. Failed samples that are replaced by successful retries remain in the error history without making a completed run partial. Operational diagnostics go to stderr in every CLI output mode. + +JSON includes `status`, `stop_reason` (`deadline`, `retry_budget`, `cancelled`, or null), and structured `errors` with stage, payload size, HTTP status when available, and reason. Missing metadata is null. `latency_measurement` is always present and includes `status`, `attempts`, `successes`, `target_samples`, and `errors`. Its summary values are null when no valid samples exist; human output uses N/A. `--nr-latency-tests 0` explicitly disables latency testing. CSV retains its throughput-only columns; use the exit code and stderr to detect failures, or JSON for the full outcome. + +Downloads must deliver exactly the requested number of bytes without body-read errors. Upload response-body errors also invalidate the sample, while upload timing still stops at response headers. Quartiles use the median of each half, excluding the middle observation for odd sample counts; a singleton uses that observation for every summary statistic. + +`--server ` selects a compatible service exposing `/cdn-cgi/trace`, `/__down?bytes=...`, and `/__up`. It defaults to `https://speed.cloudflare.com` and also supports local HTTP fixtures for testing. + +### Library outcomes + +`speed_test_with_config(client, options, RunConfig)` returns a `SpeedTestReport` with metadata, latency, throughput samples, attempt counts, errors, and an exit-code policy. Set `options.output_format = OutputFormat::None` to consume it without printing. `RunConfig` supplies the endpoint, time budgets, and a shared cancellation flag; library calls never install signal handlers or exit the host process. + +`run_latency_report` and `try_test_latency` provide explicit latency outcomes. The existing `speed_test` and tuple/floating-point functions remain available. Missing latency from `test_latency` or `run_latency_test` now uses NaN instead of the misleading zero fallback. `fetch_metadata` returns `MeasurementError` so it can report invalid metadata as well as HTTP/transport errors. + ### Shell Completion `cfspeedtest` supports generating shell completion scripts. Use the `--generate-completion` flag followed by your shell name (e.g., `bash`, `zsh`, `fish`, `powershell`, `elvish`). diff --git a/src/lib.rs b/src/lib.rs index 531df14..01d7e88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod boxplot; pub mod measurements; pub mod progress; +pub mod run; pub mod speedtest; use std::fmt; use std::fmt::Display; diff --git a/src/main.rs b/src/main.rs index 2905fdd..9736686 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,21 +6,42 @@ use clap_complete::generate; use std::io; use std::net::IpAddr; -use speedtest::speed_test; +use cfspeedtest::run::RunConfig; +use speedtest::speed_test_with_config; +use std::process::ExitCode; +use std::sync::atomic::Ordering; +use std::time::Duration; + +#[derive(Parser)] +#[command(version, about = "Unofficial CLI for speed.cloudflare.com")] +struct CliOptions { + #[command(flatten)] + options: SpeedTestCLIOptions, + /// Base URL of a compatible Cloudflare speed-test service + #[arg(long, default_value = "https://speed.cloudflare.com")] + server: reqwest::Url, + /// Maximum total run duration in seconds + #[arg(long, default_value_t = 120, value_parser = clap::value_parser!(u64).range(1..=86400))] + max_duration: u64, + /// Maximum cumulative retry wait in seconds + #[arg(long, default_value_t = 30, value_parser = clap::value_parser!(u64).range(0..=86400))] + max_retry_wait: u64, +} fn print_completions(gen: G, cmd: &mut clap::Command) { generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); } -fn main() { +fn main() -> ExitCode { env_logger::init(); - let options = SpeedTestCLIOptions::parse(); + let cli = CliOptions::parse(); + let options = cli.options; if let Some(generator) = options.completion { - let mut cmd = SpeedTestCLIOptions::command(); + let mut cmd = CliOptions::command(); eprintln!("Generating completion script for {generator}..."); print_completions(generator, &mut cmd); - return; + return ExitCode::SUCCESS; } if options.output_format == OutputFormat::StdOut { @@ -45,8 +66,38 @@ fn main() { .cookie_store(true) .build(); } - speed_test( + let config = RunConfig { + base_url: cli.server.as_str().trim_end_matches('/').to_string(), + max_duration: Duration::from_secs(cli.max_duration), + max_retry_wait: Duration::from_secs(cli.max_retry_wait), + ..RunConfig::default() + }; + let cancelled = config.cancelled.clone(); + if let Err(error) = ctrlc::set_handler(move || { + cancelled.store(true, Ordering::SeqCst); + }) { + eprintln!("Failed to install cancellation handler: {error}"); + return ExitCode::FAILURE; + } + let report = speed_test_with_config( client.expect("Failed to initialize reqwest client"), options, + config, ); + for error in &report.errors { + let payload = error + .payload_size + .map_or(String::new(), |bytes| format!(" ({bytes} bytes)")); + eprintln!("{}{}: {}", error.stage, payload, error.error); + } + if let Some(reason) = report.stop_reason { + eprintln!("Run stopped: {reason:?}"); + } + if report.exit_code() != 0 { + eprintln!( + "Run status: {:?}; completed measurements are retained in the output", + report.status + ); + } + ExitCode::from(report.exit_code()) } diff --git a/src/measurements.rs b/src/measurements.rs index 0c3c9fa..409ccef 100644 --- a/src/measurements.rs +++ b/src/measurements.rs @@ -1,4 +1,5 @@ use crate::boxplot; +use crate::run::SpeedTestReport; use crate::speedtest::Metadata; use crate::speedtest::TestType; use crate::OutputFormat; @@ -60,14 +61,13 @@ impl Display for Measurement { } pub(crate) fn log_measurements( - measurements: &[Measurement], - payload_attempt_stats: &[PayloadAttemptStats], - latency_measurement: Option<&LatencyMeasurement>, + report: &SpeedTestReport, payload_sizes: Vec, verbose: bool, output_format: OutputFormat, - metadata: Option<&Metadata>, ) { + let measurements = &report.measurements; + let payload_attempt_stats = &report.payload_attempt_stats; if output_format == OutputFormat::StdOut { println!("\nSummary Statistics"); if verbose { @@ -104,12 +104,12 @@ pub(crate) fn log_measurements( wtr.flush().unwrap(); } OutputFormat::Json => { - let output = compose_output_json(&stat_measurements, latency_measurement, metadata); + let output = compose_report_json(&stat_measurements, report); serde_json::to_writer(io::stdout(), &output).unwrap(); println!(); } OutputFormat::JsonPretty => { - let output = compose_output_json(&stat_measurements, latency_measurement, metadata); + let output = compose_report_json(&stat_measurements, report); serde_json::to_writer_pretty(io::stdout(), &output).unwrap(); println!(); } @@ -118,6 +118,34 @@ pub(crate) fn log_measurements( } } +fn compose_report_json( + stat_measurements: &[StatMeasurement], + report: &SpeedTestReport, +) -> serde_json::Map { + let mut output = compose_output_json(stat_measurements, None, report.metadata.as_ref()); + output.insert( + "metadata".into(), + serde_json::to_value(&report.metadata).unwrap(), + ); + output.insert( + "latency_measurement".into(), + serde_json::to_value(&report.latency).unwrap(), + ); + output.insert( + "status".into(), + serde_json::to_value(report.status).unwrap(), + ); + output.insert( + "stop_reason".into(), + serde_json::to_value(report.stop_reason).unwrap(), + ); + output.insert( + "errors".into(), + serde_json::to_value(&report.errors).unwrap(), + ); + output +} + fn compose_output_json( stat_measurements: &[StatMeasurement], latency_measurement: Option<&LatencyMeasurement>, @@ -276,17 +304,9 @@ fn calc_stats(mbit_measurements: Vec) -> Option<(f64, f64, f64, f64, f64, f )); } - let q1 = if length.is_multiple_of(2) { - median(&sorted_data[0..length / 2]) - } else { - median(&sorted_data[0..length.div_ceil(2)]) - }; - - let q3 = if length.is_multiple_of(2) { - median(&sorted_data[length / 2..length]) - } else { - median(&sorted_data[length.div_ceil(2)..length]) - }; + // Median of each half, excluding the middle sample for odd lengths. + let q1 = median(&sorted_data[..length / 2]); + let q3 = median(&sorted_data[length.div_ceil(2)..]); Some(( *sorted_data.first().unwrap(), @@ -457,3 +477,24 @@ mod tests { ); } } + +#[cfg(test)] +mod p1_regressions { + use super::*; + + #[test] + fn p1_quartiles_exclude_middle_sample_consistently() { + let (_, q1, median, q3, _, _) = calc_stats(vec![1., 2., 3., 4., 5.]).unwrap(); + assert_eq!((q1, median, q3), (1.5, 3., 4.5)); + } + + #[test] + fn p1_quartiles_are_symmetric_for_odd_and_even_samples() { + for count in 2..20 { + let values = (1..=count).map(f64::from).collect(); + let (_, q1, median, q3, _, _) = calc_stats(values).unwrap(); + assert_eq!(median - q1, q3 - median, "count={count}"); + } + assert_eq!(calc_stats(vec![7.; 5]).unwrap(), (7., 7., 7., 7., 7., 7.)); + } +} diff --git a/src/run.rs b/src/run.rs new file mode 100644 index 0000000..c9f975c --- /dev/null +++ b/src/run.rs @@ -0,0 +1,293 @@ +//! Run outcomes and shared limits for metadata, latency, transfers, and retries. +use crate::measurements::{Measurement, PayloadAttemptStats}; +use crate::speedtest::Metadata; +use serde::Serialize; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::time::{Duration, Instant}; + +#[derive(Clone, Debug)] +pub struct RunConfig { + pub base_url: String, + pub max_duration: Duration, + pub max_retry_wait: Duration, + /// Set this flag to request cancellation. Library calls never install signal handlers. + pub cancelled: Arc, +} + +impl Default for RunConfig { + fn default() -> Self { + Self { + base_url: "https://speed.cloudflare.com".into(), + max_duration: Duration::from_secs(120), + max_retry_wait: Duration::from_secs(30), + cancelled: Arc::new(AtomicBool::new(false)), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RunStatus { + Complete, + Partial, + Failed, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum StopReason { + Deadline, + RetryBudget, + Cancelled, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum LatencyStatus { + Disabled, + Complete, + Partial, + Failed, +} + +#[derive(Clone, Debug, Serialize)] +pub struct MeasurementError { + pub status_code: Option, + pub reason: String, +} + +impl std::fmt::Display for MeasurementError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.reason) + } +} +impl std::error::Error for MeasurementError {} +impl From for MeasurementError { + fn from(error: reqwest::Error) -> Self { + Self { + status_code: error.status().map(|s| s.as_u16()), + reason: error.to_string(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct RunError { + pub stage: String, + pub payload_size: Option, + #[serde(flatten)] + pub error: MeasurementError, +} + +#[derive(Debug, Serialize)] +pub struct LatencyReport { + pub status: LatencyStatus, + pub attempts: u32, + pub successes: u32, + pub target_samples: u32, + pub avg_latency_ms: Option, + pub min_latency_ms: Option, + pub max_latency_ms: Option, + pub latency_measurements: Vec, + pub errors: Vec, +} + +impl LatencyReport { + pub(crate) fn new(target_samples: u32) -> Self { + Self { + status: if target_samples == 0 { + LatencyStatus::Disabled + } else { + LatencyStatus::Failed + }, + attempts: 0, + successes: 0, + target_samples, + avg_latency_ms: None, + min_latency_ms: None, + max_latency_ms: None, + latency_measurements: vec![], + errors: vec![], + } + } + + pub(crate) fn finish(&mut self) { + self.successes = self.latency_measurements.len() as u32; + if self.successes > 0 { + self.status = if self.successes == self.target_samples { + LatencyStatus::Complete + } else { + LatencyStatus::Partial + }; + self.avg_latency_ms = + Some(self.latency_measurements.iter().sum::() / f64::from(self.successes)); + self.min_latency_ms = self.latency_measurements.iter().copied().reduce(f64::min); + self.max_latency_ms = self.latency_measurements.iter().copied().reduce(f64::max); + } + } +} + +pub struct SpeedTestReport { + pub status: RunStatus, + pub stop_reason: Option, + pub metadata: Option, + pub latency: LatencyReport, + pub measurements: Vec, + pub payload_attempt_stats: Vec, + pub errors: Vec, +} + +impl SpeedTestReport { + /// Complete=0, failed=1, partial=3, cancelled=130. Metadata is optional. + pub fn exit_code(&self) -> u8 { + if self.stop_reason == Some(StopReason::Cancelled) { + return 130; + } + match self.status { + RunStatus::Complete => 0, + RunStatus::Failed => 1, + RunStatus::Partial => 3, + } + } +} + +pub(crate) struct RunControl { + pub config: RunConfig, + deadline: Instant, + retry_wait: Duration, + pub stop_reason: Option, + pub errors: Vec, +} + +impl RunControl { + pub fn new(config: RunConfig) -> Self { + Self::new_at(config, Instant::now()) + } + + fn new_at(config: RunConfig, now: Instant) -> Self { + Self { + deadline: now.checked_add(config.max_duration).unwrap_or(now), + config, + retry_wait: Duration::ZERO, + stop_reason: None, + errors: vec![], + } + } + + fn remaining_at(&mut self, now: Instant) -> Option { + if self.config.cancelled.load(Ordering::SeqCst) { + self.stop_reason = Some(StopReason::Cancelled); + } + if self.stop_reason.is_some() { + return None; + } + let remaining = self.deadline.saturating_duration_since(now); + if remaining.is_zero() { + self.stop_reason = Some(StopReason::Deadline); + None + } else { + Some(remaining) + } + } + + pub fn remaining(&mut self) -> Option { + self.remaining_at(Instant::now()) + } + + pub fn request_timeout(&mut self) -> Option { + self.remaining().map(|d| d.min(Duration::from_secs(30))) + } + + fn reserve_retry_at(&mut self, delay: Duration, now: Instant) -> bool { + let Some(remaining) = self.remaining_at(now) else { + return false; + }; + if delay > self.config.max_retry_wait.saturating_sub(self.retry_wait) { + self.stop_reason = Some(StopReason::RetryBudget); + return false; + } + if delay >= remaining { + self.stop_reason = Some(StopReason::Deadline); + return false; + } + self.retry_wait += delay; + true + } + + pub fn reserve_retry(&mut self, delay: Duration) -> bool { + self.reserve_retry_at(delay, Instant::now()) + } + + pub fn record(&mut self, stage: &str, payload_size: Option, error: MeasurementError) { + self.errors.push(RunError { + stage: stage.into(), + payload_size, + error, + }); + } +} + +pub(crate) fn interruptible_sleep(delay: Duration, cancelled: &AtomicBool) { + let start = Instant::now(); + while !cancelled.load(Ordering::SeqCst) { + let remaining = delay.saturating_sub(start.elapsed()); + if remaining.is_zero() { + break; + } + std::thread::sleep(remaining.min(Duration::from_millis(20))); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p1_retry_budget_is_cumulative_and_accepts_zero_without_waiting() { + let now = Instant::now(); + let mut run = RunControl::new_at( + RunConfig { + max_retry_wait: Duration::from_secs(3), + ..RunConfig::default() + }, + now, + ); + assert!(run.reserve_retry_at(Duration::from_secs(2), now)); + assert!(run.reserve_retry_at(Duration::from_secs(1), now + Duration::from_secs(2))); + assert!(run.reserve_retry_at(Duration::ZERO, now + Duration::from_secs(3))); + assert!(!run.reserve_retry_at(Duration::from_millis(1), now + Duration::from_secs(3))); + assert_eq!(run.stop_reason, Some(StopReason::RetryBudget)); + } + + #[test] + fn p1_deadline_does_not_reset_between_stages_or_waits() { + let now = Instant::now(); + let mut run = RunControl::new_at( + RunConfig { + max_duration: Duration::from_secs(5), + ..RunConfig::default() + }, + now, + ); + assert_eq!( + run.remaining_at(now + Duration::from_secs(4)), + Some(Duration::from_secs(1)) + ); + assert!(!run.reserve_retry_at(Duration::from_secs(1), now + Duration::from_secs(4))); + assert_eq!(run.stop_reason, Some(StopReason::Deadline)); + assert!(run.remaining_at(now + Duration::from_secs(5)).is_none()); + } + + #[test] + fn p1_cancellation_prevents_new_requests_and_retries() { + let now = Instant::now(); + let mut run = RunControl::new_at(RunConfig::default(), now); + run.config.cancelled.store(true, Ordering::SeqCst); + assert!(run.request_timeout().is_none()); + assert!(!run.reserve_retry_at(Duration::ZERO, now)); + assert_eq!(run.stop_reason, Some(StopReason::Cancelled)); + } +} diff --git a/src/speedtest.rs b/src/speedtest.rs index 9e1e8f1..e48febb 100644 --- a/src/speedtest.rs +++ b/src/speedtest.rs @@ -1,9 +1,12 @@ use crate::measurements::format_bytes; use crate::measurements::log_measurements; -use crate::measurements::LatencyMeasurement; use crate::measurements::Measurement; use crate::measurements::PayloadAttemptStats; use crate::progress::print_progress; +use crate::run::{ + interruptible_sleep, LatencyReport, LatencyStatus, MeasurementError, RunConfig, RunControl, + RunStatus, SpeedTestReport, +}; use crate::OutputFormat; use crate::SpeedTestCLIOptions; use jiff::Zoned; @@ -18,8 +21,7 @@ use std::{ atomic::{AtomicBool, Ordering}, LazyLock, }, - thread, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime}, }; const BASE_URL: &str = "https://speed.cloudflare.com"; @@ -106,102 +108,175 @@ impl Display for Metadata { } } +/// Compatibility wrapper. Returns successful throughput samples and never exits the process. +/// Use `speed_test_with_config` for explicit outcomes, latency, errors, and limits. pub fn speed_test(client: Client, options: SpeedTestCLIOptions) -> Vec { - let metadata = match fetch_metadata(&client) { - Ok(metadata) => metadata, - Err(e) => { - eprintln!("Error fetching metadata: {e}"); - std::process::exit(1); + speed_test_with_config(client, options, RunConfig::default()).measurements +} + +/// Run with a single deadline and retry-wait budget shared by every stage. +/// Output follows `options.output_format`; the returned report is available in every format. +pub fn speed_test_with_config( + client: Client, + options: SpeedTestCLIOptions, + config: RunConfig, +) -> SpeedTestReport { + let mut control = RunControl::new(config); + let base_url = control.config.base_url.trim_end_matches('/').to_string(); + let metadata = if let Some(timeout) = control.request_timeout() { + match fetch_metadata_request(&client, &base_url, Some(timeout)) { + Ok(metadata) => Some(metadata), + Err(error) => { + control.record("metadata", None, error); + None + } } - }; - if options.output_format == OutputFormat::StdOut { - println!("{metadata}"); - } - let (latency_measurements, avg_latency) = - run_latency_test(&client, options.nr_latency_tests, options.output_format); - let latency_measurement = if !latency_measurements.is_empty() { - Some(LatencyMeasurement { - avg_latency_ms: avg_latency, - min_latency_ms: latency_measurements - .iter() - .copied() - .fold(f64::INFINITY, f64::min), - max_latency_ms: latency_measurements - .iter() - .copied() - .fold(f64::NEG_INFINITY, f64::max), - latency_measurements, - }) } else { None }; - + if options.output_format == OutputFormat::StdOut { + if let Some(metadata) = &metadata { + println!("{metadata}"); + } + } + let latency = run_latency_with_control( + &client, + options.nr_latency_tests, + options.output_format, + &mut control, + ); let payload_sizes = PayloadSize::sizes_from_max(options.max_payload_size.clone()); + let retry_options = RetryRunOptions { + nr_tests: options.nr_tests, + output_format: options.output_format, + disable_dynamic_max_payload_size: options.disable_dynamic_max_payload_size, + }; let mut measurements = Vec::new(); let mut payload_attempt_stats = Vec::new(); - - if options.should_download() { - let (download_measurements, download_attempt_stats) = run_tests_with_retries( - &client, - TestType::Download, - payload_sizes.clone(), - options.nr_tests, - options.output_format, - options.disable_dynamic_max_payload_size, - ); - measurements.extend(download_measurements); - payload_attempt_stats.extend(download_attempt_stats); - } - - if options.should_upload() { - let (upload_measurements, upload_attempt_stats) = run_tests_with_retries( - &client, - TestType::Upload, - payload_sizes.clone(), - options.nr_tests, - options.output_format, - options.disable_dynamic_max_payload_size, - ); - measurements.extend(upload_measurements); - payload_attempt_stats.extend(upload_attempt_stats); + let cancelled = control.config.cancelled.clone(); + for (enabled, test_type) in [ + (options.should_download(), TestType::Download), + (options.should_upload(), TestType::Upload), + ] { + if enabled { + let (samples, attempts) = run_tests_with_control( + &client, + test_type, + payload_sizes.clone(), + retry_options, + &mut control, + |delay| interruptible_sleep(delay, &cancelled), + ); + measurements.extend(samples); + payload_attempt_stats.extend(attempts); + } } - + control.remaining(); + let status = if measurements.is_empty() { + RunStatus::Failed + } else if control.stop_reason.is_some() + || matches!( + latency.status, + LatencyStatus::Failed | LatencyStatus::Partial + ) + || payload_attempt_stats + .iter() + .any(|s| s.successes < s.target_successes) + { + RunStatus::Partial + } else { + RunStatus::Complete + }; + let report = SpeedTestReport { + status, + stop_reason: control.stop_reason, + metadata, + latency, + measurements, + payload_attempt_stats, + errors: control.errors, + }; log_measurements( - &measurements, - &payload_attempt_stats, - latency_measurement.as_ref(), + &report, payload_sizes, options.verbose, options.output_format, - Some(&metadata), ); - measurements + report } +/// Compatibility wrapper. The average is NaN when there are no valid samples. +/// Use `run_latency_report` to distinguish disabled, partial, and failed measurements. pub fn run_latency_test( client: &Client, nr_latency_tests: u32, output_format: OutputFormat, ) -> (Vec, f64) { - let mut measurements: Vec = Vec::new(); + let report = run_latency_report( + client, + nr_latency_tests, + output_format, + RunConfig::default(), + ); + ( + report.latency_measurements, + report.avg_latency_ms.unwrap_or(f64::NAN), + ) +} + +pub fn run_latency_report( + client: &Client, + nr_latency_tests: u32, + output_format: OutputFormat, + config: RunConfig, +) -> LatencyReport { + run_latency_with_control( + client, + nr_latency_tests, + output_format, + &mut RunControl::new(config), + ) +} + +fn run_latency_with_control( + client: &Client, + nr_latency_tests: u32, + output_format: OutputFormat, + control: &mut RunControl, +) -> LatencyReport { + let mut report = LatencyReport::new(nr_latency_tests); + let base_url = control.config.base_url.trim_end_matches('/').to_string(); for i in 0..nr_latency_tests { + let Some(timeout) = control.request_timeout() else { + break; + }; if output_format == OutputFormat::StdOut { print_progress("latency test", i + 1, nr_latency_tests); } - if let Some(latency) = try_test_latency(client) { - measurements.push(latency); + report.attempts += 1; + match test_latency_request(client, &base_url, Some(timeout)) { + Ok(latency) => report.latency_measurements.push(latency), + Err(error) => { + control.record("latency", None, error.clone()); + report.errors.push(error); + } } } - let avg_latency = if measurements.is_empty() { - 0.0 - } else { - measurements.iter().sum::() / measurements.len() as f64 - }; - - if output_format == OutputFormat::StdOut { - println!("\nAvg GET request latency {avg_latency:.2} ms\n"); + report.finish(); + if output_format == OutputFormat::StdOut && nr_latency_tests > 0 { + if let Some(avg) = report.avg_latency_ms { + println!( + "\nAvg GET request latency {avg:.2} ms ({}/{} valid samples)\n", + report.successes, report.target_samples + ); + } else { + println!( + "\nAvg GET request latency N/A (0/{} valid samples)\n", + report.target_samples + ); + } } - (measurements, avg_latency) + report } // Parse latency from a Server-Timing header value. Supports the legacy @@ -233,49 +308,68 @@ fn parse_latency_from_server_timing(header: &str, total_ms: f64) -> Option None } -fn try_test_latency(client: &Client) -> Option { - let url = &format!("{}/{}{}", BASE_URL, DOWNLOAD_URL, 0); - let req_builder = client.get(url); +/// Measure one valid latency response, retaining the cause of a failed measurement. +pub fn try_test_latency(client: &Client) -> Result { + test_latency_request(client, BASE_URL, None) +} +fn test_latency_request( + client: &Client, + base_url: &str, + timeout: Option, +) -> Result { + let url = format!("{base_url}/{DOWNLOAD_URL}0"); + let mut request = client.get(url); + if let Some(timeout) = timeout { + request = request.timeout(timeout); + } let start = Instant::now(); - let mut response = match req_builder.send() { - Ok(resp) => resp, - Err(e) => { - log::debug!("Latency test request failed: {e}"); - return None; - } - }; - let _status_code = response.status(); - let _ = std::io::copy(&mut response, &mut std::io::sink()); + let mut response = request.send()?.error_for_status()?; + let status_code = response.status().as_u16(); + if status_code != 200 { + return Err(MeasurementError { + status_code: Some(status_code), + reason: "unexpected latency response status".into(), + }); + } + let received = + std::io::copy(&mut response, &mut std::io::sink()).map_err(|error| MeasurementError { + status_code: Some(status_code), + reason: format!("failed to read latency body: {error}"), + })?; + if received != 0 { + return Err(MeasurementError { + status_code: Some(status_code), + reason: format!("expected empty latency body, received {received} bytes"), + }); + } let total_ms = start.elapsed().as_secs_f64() * 1_000.0; - let server_timing = response .headers() .get("Server-Timing") .and_then(|v| v.to_str().ok()); - if let Some(header) = server_timing { if let Some(latency) = parse_latency_from_server_timing(header, total_ms) { - log::debug!("latency: total_ms={total_ms:.3} parsed={latency:.3}"); - return Some(latency); + if latency.is_finite() && latency >= 0.0 { + return Ok(latency); + } + return Err(MeasurementError { + status_code: Some(status_code), + reason: "non-finite latency in Server-Timing".into(), + }); } if !WARNED_UNKNOWN_HEADER.swap(true, Ordering::Relaxed) { log::warn!("Server-Timing header format not recognized, falling back to raw RTT"); } - } else { - if !WARNED_NO_HEADER.swap(true, Ordering::Relaxed) { - log::warn!("No Server-Timing header in response, falling back to raw RTT"); - } + } else if !WARNED_NO_HEADER.swap(true, Ordering::Relaxed) { + log::warn!("No Server-Timing header in response, falling back to raw RTT"); } - log::debug!("latency fallback: total_ms={total_ms:.3}"); - Some(total_ms) + Ok(total_ms) } +/// Compatibility wrapper. Returns NaN on failure; prefer `try_test_latency`. pub fn test_latency(client: &Client) -> f64 { - try_test_latency(client).unwrap_or_else(|| { - log::debug!("Latency measurement failed, returning 0.0"); - 0.0 - }) + try_test_latency(client).unwrap_or(f64::NAN) } #[derive(Debug)] @@ -352,36 +446,61 @@ pub fn run_tests_with_retries( output_format: OutputFormat, disable_dynamic_max_payload_size: bool, ) -> (Vec, Vec) { - let options = RetryRunOptions { - nr_tests, - output_format, - disable_dynamic_max_payload_size, - }; - run_tests_with_sleep( + let mut control = RunControl::new(RunConfig::default()); + let cancelled = control.config.cancelled.clone(); + run_tests_with_control( client, test_type, payload_sizes, - options, - BASE_URL, - thread::sleep, + RetryRunOptions { + nr_tests, + output_format, + disable_dynamic_max_payload_size, + }, + &mut control, + |delay| interruptible_sleep(delay, &cancelled), ) } -fn run_tests_with_sleep( +#[cfg(test)] +fn run_tests_with_sleep( client: &Client, test_type: TestType, payload_sizes: Vec, options: RetryRunOptions, base_url: &str, sleep_fn: S, -) -> (Vec, Vec) -where - S: Fn(Duration), -{ +) -> (Vec, Vec) { + let mut control = RunControl::new(RunConfig { + base_url: base_url.into(), + ..RunConfig::default() + }); + run_tests_with_control( + client, + test_type, + payload_sizes, + options, + &mut control, + sleep_fn, + ) +} + +fn run_tests_with_control( + client: &Client, + test_type: TestType, + payload_sizes: Vec, + options: RetryRunOptions, + control: &mut RunControl, + sleep_fn: S, +) -> (Vec, Vec) { + let base_url = control.config.base_url.trim_end_matches('/').to_string(); let mut measurements: Vec = Vec::new(); let mut payload_attempt_stats = Vec::new(); for payload_size in payload_sizes { + if control.remaining().is_none() { + break; + } let label = format!("{:?} {:<5}", test_type, format_bytes(payload_size)); log::debug!("running tests for payload_size {payload_size}"); let start = Instant::now(); @@ -396,21 +515,29 @@ where .max(options.nr_tests); while successes < options.nr_tests && attempts < max_attempts { + let Some(timeout) = control.request_timeout() else { + break; + }; if options.output_format == OutputFormat::StdOut { print_progress(&label, successes, options.nr_tests); } attempts += 1; let sample_outcome = match test_type { - TestType::Download => test_download_with_base_url( + TestType::Download => test_download_request( client, payload_size, options.output_format, - base_url, + &base_url, + Some(timeout), + ), + TestType::Upload => test_upload_request( + client, + payload_size, + options.output_format, + &base_url, + Some(timeout), ), - TestType::Upload => { - test_upload_with_base_url(client, payload_size, options.output_format, base_url) - } }; match sample_outcome { @@ -440,9 +567,20 @@ where reason, } => { skipped += 1; + control.record( + &format!("{test_type:?}").to_lowercase(), + Some(payload_size), + MeasurementError { + status_code: status_code.map(|s| s.as_u16()), + reason: reason.clone(), + }, + ); retry_streak += 1; if attempts < max_attempts { let delay = compute_retry_delay(retry_streak, retry_after); + if !control.reserve_retry(delay) { + break; + } let status = status_code .map(|code| code.to_string()) .unwrap_or_else(|| "transport error".to_string()); @@ -464,6 +602,14 @@ where reason, } => { skipped += 1; + control.record( + &format!("{test_type:?}").to_lowercase(), + Some(payload_size), + MeasurementError { + status_code: status_code.map(|s| s.as_u16()), + reason: reason.clone(), + }, + ); let status = status_code .map(|code| code.to_string()) .unwrap_or_else(|| "transport error".to_string()); @@ -532,10 +678,23 @@ fn test_upload_with_base_url( payload_size_bytes: usize, output_format: OutputFormat, base_url: &str, +) -> SampleOutcome { + test_upload_request(client, payload_size_bytes, output_format, base_url, None) +} + +fn test_upload_request( + client: &Client, + payload_size_bytes: usize, + output_format: OutputFormat, + base_url: &str, + timeout: Option, ) -> SampleOutcome { let url = format!("{base_url}/{UPLOAD_URL}"); let payload: Vec = vec![1; payload_size_bytes]; - let req_builder = client.post(&url).body(payload); + let mut req_builder = client.post(&url).body(payload); + if let Some(timeout) = timeout { + req_builder = req_builder.timeout(timeout); + } let start = Instant::now(); let mut response = match req_builder.send() { @@ -565,8 +724,8 @@ fn test_upload_with_base_url( let retry_after = parse_retry_after(response.headers().get(RETRY_AFTER)); // Measure upload duration once response headers are available. let duration = start.elapsed(); - // Drain response after timing so we don't skew upload measurement. - let _ = std::io::copy(&mut response, &mut std::io::sink()); + // Validate completion after timing so response download time does not skew upload speed. + let body_result = std::io::copy(&mut response, &mut std::io::sink()); if !status_code.is_success() { if output_format == OutputFormat::StdOut { print_skipped_sample(duration, status_code, payload_size_bytes); @@ -587,7 +746,22 @@ fn test_upload_with_base_url( }; } + if let Err(error) = body_result { + return SampleOutcome::RetryableFailure { + duration, + status_code: Some(status_code), + retry_after: None, + reason: format!("failed to read upload response: {error}"), + }; + } let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64(); + if !mbits.is_finite() || mbits <= 0.0 { + return SampleOutcome::Failed { + duration, + status_code: Some(status_code), + reason: "invalid upload throughput".into(), + }; + } if output_format == OutputFormat::StdOut { print_current_speed(mbits, duration, payload_size_bytes); } @@ -603,9 +777,22 @@ fn test_download_with_base_url( payload_size_bytes: usize, output_format: OutputFormat, base_url: &str, +) -> SampleOutcome { + test_download_request(client, payload_size_bytes, output_format, base_url, None) +} + +fn test_download_request( + client: &Client, + payload_size_bytes: usize, + output_format: OutputFormat, + base_url: &str, + timeout: Option, ) -> SampleOutcome { let url = format!("{base_url}/{DOWNLOAD_URL}{payload_size_bytes}"); - let req_builder = client.get(&url); + let mut req_builder = client.get(&url); + if let Some(timeout) = timeout { + req_builder = req_builder.timeout(timeout); + } let start = Instant::now(); let mut response = match req_builder.send() { @@ -632,8 +819,8 @@ fn test_download_with_base_url( }; let status_code = response.status(); - // Stream the body to avoid buffering the full payload in memory. - let _ = std::io::copy(&mut response, &mut std::io::sink()); + // Retain body errors and byte counts: a successful status alone is not a sample. + let body_result = std::io::copy(&mut response, &mut std::io::sink()); let duration = start.elapsed(); if !status_code.is_success() { if output_format == OutputFormat::StdOut { @@ -656,7 +843,34 @@ fn test_download_with_base_url( }; } - let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64(); + let received = match body_result { + Ok(received) => received, + Err(error) => { + return SampleOutcome::RetryableFailure { + duration, + status_code: Some(status_code), + retry_after: None, + reason: format!("failed to read download body: {error}"), + } + } + }; + if received != payload_size_bytes as u64 { + return SampleOutcome::Failed { + duration, + status_code: Some(status_code), + reason: format!( + "incorrect download size: expected {payload_size_bytes} bytes, received {received}" + ), + }; + } + let mbits = (received as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64(); + if !mbits.is_finite() || mbits <= 0.0 { + return SampleOutcome::Failed { + duration, + status_code: Some(status_code), + reason: "invalid download throughput".to_string(), + }; + } if output_format == OutputFormat::StdOut { print_current_speed(mbits, duration, payload_size_bytes); } @@ -675,10 +889,23 @@ fn is_retryable_status(status_code: StatusCode) -> bool { } fn parse_retry_after(retry_after: Option<&reqwest::header::HeaderValue>) -> Option { - retry_after - .and_then(|header| header.to_str().ok()) - .and_then(|value| value.trim().parse::().ok()) - .map(Duration::from_secs) + parse_retry_after_at(retry_after, SystemTime::now()) +} + +fn parse_retry_after_at( + retry_after: Option<&reqwest::header::HeaderValue>, + now: SystemTime, +) -> Option { + let value = retry_after?.to_str().ok()?.trim(); + if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) { + // Overflow must not turn an enormous server delay into a short retry. + return Some(Duration::from_secs( + value.parse::().unwrap_or(u64::MAX), + )); + } + httpdate::parse_http_date(value) + .ok() + .map(|time| time.duration_since(now).unwrap_or(Duration::ZERO)) } fn compute_retry_delay(retry_count: u32, retry_after: Option) -> Duration { @@ -773,29 +1000,43 @@ fn flush_stdout() { let _ = std::io::stdout().flush(); } -pub fn fetch_metadata(client: &Client) -> Result { - const TRACE_URL: &str = "https://speed.cloudflare.com/cdn-cgi/trace"; +pub fn fetch_metadata(client: &Client) -> Result { + fetch_metadata_request(client, BASE_URL, None) +} - let response = client.get(TRACE_URL).send()?; +fn fetch_metadata_request( + client: &Client, + base_url: &str, + timeout: Option, +) -> Result { + let mut request = client.get(format!("{base_url}/cdn-cgi/trace")); + if let Some(timeout) = timeout { + request = request.timeout(timeout); + } + let response = request.send()?.error_for_status()?; + let status_code = response.status().as_u16(); let body = response.text()?; - - // Parse key=value pairs from response body let trace_data = parse_trace_response(&body); - - Ok(Metadata { - country: trace_data - .get("loc") - .unwrap_or(&"N/A".to_string()) - .to_owned(), - ip: trace_data - .get("ip") - .unwrap_or(&"N/A".to_string()) - .to_owned(), - colo: trace_data - .get("colo") - .unwrap_or(&"N/A".to_string()) - .to_owned(), - }) + let ip = trace_data + .get("ip") + .filter(|ip| ip.parse::().is_ok()); + let country = trace_data + .get("loc") + .filter(|s| s.len() == 2 && s.bytes().all(|b| b.is_ascii_uppercase())); + let colo = trace_data + .get("colo") + .filter(|s| s.len() == 3 && s.bytes().all(|b| b.is_ascii_uppercase())); + match (ip, country, colo) { + (Some(ip), Some(country), Some(colo)) => Ok(Metadata { + ip: ip.clone(), + country: country.clone(), + colo: colo.clone(), + }), + _ => Err(MeasurementError { + status_code: Some(status_code), + reason: "metadata response has missing or invalid ip, loc, or colo fields".into(), + }), + } } /// Parses the Cloudflare trace response body into a key-value map @@ -855,8 +1096,27 @@ mod tests { while idx < responses.len() { match listener.accept() { Ok((mut stream, _)) => { - let mut buf = [0_u8; 1024]; - let _ = stream.read(&mut buf); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let mut request = Vec::new(); + let mut buf = [0_u8; 4096]; + loop { + let count = stream.read(&mut buf).unwrap(); + assert!(count > 0, "incomplete mock request"); + request.extend_from_slice(&buf[..count]); + if let Some(end) = request.windows(4).position(|b| b == b"\r\n\r\n") { + let headers = String::from_utf8_lossy(&request[..end]); + let length = headers + .lines() + .filter_map(|line| line.split_once(':')) + .find(|(key, _)| key.eq_ignore_ascii_case("content-length")) + .map_or(0, |(_, value)| value.trim().parse::().unwrap()); + if request.len() >= end + 4 + length { + break; + } + } + } let response = &responses[idx]; let mut response_head = format!( @@ -1122,7 +1382,7 @@ mod tests { let (measurements, payload_stats) = run_tests_with_sleep( &client, TestType::Download, - vec![100_000], + vec![2], RetryRunOptions { nr_tests: 1, output_format: OutputFormat::None, @@ -1176,7 +1436,7 @@ mod tests { let (measurements, payload_stats) = run_tests_with_sleep( &client, TestType::Download, - vec![100_000], + vec![2], RetryRunOptions { nr_tests: 6, output_format: OutputFormat::None, @@ -1247,7 +1507,7 @@ mod tests { let (measurements, payload_stats) = run_tests_with_sleep( &client, TestType::Download, - vec![100_000], + vec![2], RetryRunOptions { nr_tests: 2, output_format: OutputFormat::None, @@ -1297,7 +1557,7 @@ mod tests { let (measurements, payload_stats) = run_tests_with_sleep( &client, TestType::Download, - vec![100_000], + vec![2], RetryRunOptions { nr_tests: 2, output_format: OutputFormat::None, @@ -1334,7 +1594,7 @@ mod tests { let (measurements, payload_stats) = run_tests_with_sleep( &client, TestType::Download, - vec![100_000], + vec![2], RetryRunOptions { nr_tests: 2, output_format: OutputFormat::None, @@ -1354,6 +1614,99 @@ mod tests { assert_eq!(served_counter.load(AtomicOrdering::SeqCst), 1); } + #[test] + fn p1_retry_budget_is_shared_across_download_and_upload() { + let responses = vec![ + MockHttpResponse { + status_code: 429, + reason: "Retry", + headers: vec![("Retry-After", "1")], + body: "", + }, + MockHttpResponse { + status_code: 200, + reason: "OK", + headers: vec![], + body: "ok", + }, + MockHttpResponse { + status_code: 429, + reason: "Retry", + headers: vec![("Retry-After", "1")], + body: "", + }, + ]; + let (base_url, served, server) = spawn_mock_http_server(responses); + let client = Client::builder().no_proxy().build().unwrap(); + let mut control = RunControl::new(RunConfig { + base_url, + max_retry_wait: Duration::from_secs(1), + ..RunConfig::default() + }); + let options = RetryRunOptions { + nr_tests: 1, + output_format: OutputFormat::None, + disable_dynamic_max_payload_size: true, + }; + let delays = std::cell::RefCell::new(Vec::new()); + let sleep = |delay| delays.borrow_mut().push(delay); + let (downloads, _) = run_tests_with_control( + &client, + TestType::Download, + vec![2], + options, + &mut control, + sleep, + ); + let (uploads, _) = run_tests_with_control( + &client, + TestType::Upload, + vec![2], + options, + &mut control, + sleep, + ); + server.join().unwrap(); + assert_eq!(served.load(AtomicOrdering::SeqCst), 3); + assert_eq!(downloads.len(), 1); + assert!(uploads.is_empty()); + assert_eq!(*delays.borrow(), vec![Duration::from_secs(1)]); + assert_eq!( + control.stop_reason, + Some(crate::run::StopReason::RetryBudget) + ); + } + + #[test] + fn p1_retry_after_supports_dates_and_handles_invalid_and_huge_delays() { + let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000); + let future = httpdate::fmt_http_date(now + Duration::from_secs(300)); + let past = httpdate::fmt_http_date(now - Duration::from_secs(300)); + for (input, expected) in [ + (future.as_str(), Some(Duration::from_secs(300))), + (past.as_str(), Some(Duration::ZERO)), + ("0", Some(Duration::ZERO)), + (" 42 ", Some(Duration::from_secs(42))), + ( + "999999999999999999999999999999999999", + Some(Duration::from_secs(u64::MAX)), + ), + ("-1", None), + ("1.5", None), + ("nonsense", None), + ("", None), + ] { + let header = reqwest::header::HeaderValue::from_str(input).unwrap(); + assert_eq!( + parse_retry_after_at(Some(&header), now), + expected, + "{input}" + ); + } + assert_eq!(parse_retry_after_at(None, now), None); + assert_eq!(compute_retry_delay(999, None), Duration::from_millis(2400)); + } + #[test] fn test_upload_duration_excludes_delayed_response_body() { let responses = vec![MockHttpResponse { @@ -1591,13 +1944,13 @@ mod tests { .proxy(reqwest::Proxy::all("http://127.0.0.1:1").unwrap()) .build() .unwrap(); - // Must not panic. Returns 0.0 as the fallback for request failure. + // Must not panic or return a plausible latency on failure. let result = test_latency(&client); - assert!((result - 0.0).abs() < 0.001); + assert!(result.is_nan()); } #[test] - fn test_run_latency_test_all_failures_returns_zero_avg() { + fn test_run_latency_test_all_failures_returns_nan_avg() { // Every request fails immediately - failed samples are skipped let client = reqwest::blocking::Client::builder() .proxy(reqwest::Proxy::all("http://127.0.0.1:1").unwrap()) @@ -1605,6 +1958,93 @@ mod tests { .unwrap(); let (measurements, avg) = run_latency_test(&client, 3, OutputFormat::Json); assert!(measurements.is_empty(), "Failed requests should be skipped"); - assert!((avg - 0.0).abs() < 0.001, "Average should be 0.0"); + assert!( + avg.is_nan(), + "Missing average must not be a zero measurement" + ); + } +} + +#[cfg(test)] +mod p1_regressions { + use super::*; + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::thread; + + fn download_response(response: &'static [u8], pause: Duration) -> SampleOutcome { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let url = format!("http://{}", listener.local_addr().unwrap()); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let mut request = Vec::new(); + let mut buffer = [0; 4096]; + while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") { + let received = stream.read(&mut buffer).unwrap(); + assert!(received > 0, "incomplete mock request headers"); + request.extend_from_slice(&buffer[..received]); + } + stream.write_all(response).unwrap(); + thread::sleep(pause); + }); + let client = Client::builder() + .no_proxy() + .timeout(Duration::from_millis(100)) + .build() + .unwrap(); + let outcome = test_download_with_base_url(&client, 4, OutputFormat::None, &url); + server.join().unwrap(); + outcome + } + + #[test] + fn p1_download_rejects_truncated_body() { + let result = download_response( + b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\nConnection: close\r\n\r\nok", + Duration::ZERO, + ); + assert!( + !matches!(result, SampleOutcome::Success { .. }), + "{result:?}" + ); + } + + #[test] + fn p1_download_rejects_incorrect_size() { + for response in [ + b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok".as_slice(), + b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n".as_slice(), + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\n12345".as_slice(), + ] { + let result = download_response(response, Duration::ZERO); + assert!( + !matches!(result, SampleOutcome::Success { .. }), + "{result:?}" + ); + } + } + + #[test] + fn p1_download_rejects_body_timeout() { + let result = download_response( + b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\nConnection: close\r\n\r\n", + Duration::from_millis(250), + ); + assert!( + !matches!(result, SampleOutcome::Success { .. }), + "{result:?}" + ); + } + + #[test] + fn p1_download_accepts_complete_chunked_body() { + let result = download_response(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n2\r\nab\r\n2\r\ncd\r\n0\r\n\r\n", Duration::ZERO); + assert!( + matches!(result, SampleOutcome::Success { mbits, .. } if mbits.is_finite() && mbits > 0.0), + "{result:?}" + ); } } diff --git a/tests/p1_cli.rs b/tests/p1_cli.rs new file mode 100644 index 0000000..f6cd9c8 --- /dev/null +++ b/tests/p1_cli.rs @@ -0,0 +1,341 @@ +mod support; + +use serde_json::Value; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use support::{Response, Server}; + +fn json(output: &std::process::Output) -> Value { + serde_json::from_slice(&output.stdout).unwrap_or_else(|error| { + panic!( + "{error}: stdout={}, stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + }) +} + +#[test] +fn p1_cli_all_speed_samples_failed_is_nonzero() { + let server = Server::new(|path| { + if path.starts_with("/__down") { + Response::new(403, "forbidden") + } else { + Response::normal(path) + } + }); + let output = server.run(&[]); + assert_eq!(output.status.code(), Some(1)); + let report = json(&output); + assert_eq!(report["status"], "failed"); + assert_eq!(report["speed_measurements"][0]["successes"], 0); + assert_eq!(report["errors"][0]["status_code"], 403); + assert!(!output.stderr.is_empty()); +} + +#[test] +fn p1_cli_latency_http_errors_are_not_samples() { + let server = Server::new(|path| { + if path == "/__down?bytes=0" { + Response::new(503, "unavailable") + } else { + Response::normal(path) + } + }); + let output = server.run(&["--nr-latency-tests", "2"]); + let report = json(&output); + assert_eq!(report["latency_measurement"]["successes"], 0); + assert_eq!(report["latency_measurement"]["attempts"], 2); + assert_eq!(report["latency_measurement"]["status"], "failed"); + assert!(report["latency_measurement"]["avg_latency_ms"].is_null()); + assert_eq!(report["status"], "partial"); + assert_eq!(output.status.code(), Some(3)); +} + +#[test] +fn p1_cli_failed_latency_is_na_in_human_output() { + let server = Server::new(|path| { + let mut response = Response::normal(path); + response.disconnect = path == "/__down?bytes=0"; + response + }); + let output = server.run(&["--nr-latency-tests", "2", "-o", "stdout"]); + let text = String::from_utf8(output.stdout).unwrap(); + assert!(text.contains("latency N/A"), "{text}"); + assert!(!text.contains("latency 0.00")); + assert_eq!(output.status.code(), Some(3)); +} + +#[test] +fn p1_cli_metadata_errors_are_optional_and_explicit() { + for (status, body, disconnect) in [ + (503, "unavailable", false), + (200, "ip=not-an-ip\ncolo=\n", false), + (200, "", true), + ] { + let server = Server::new(move |path| { + if path == "/cdn-cgi/trace" { + let mut r = Response::new(status, body); + r.disconnect = disconnect; + r + } else { + Response::normal(path) + } + }); + let output = server.run(&[]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report = json(&output); + assert!(report["metadata"].is_null(), "{report}"); + assert_eq!(report["status"], "complete"); + assert_eq!(report["errors"][0]["stage"], "metadata"); + assert!(!output.stderr.is_empty()); + } +} + +#[test] +fn p1_cli_partial_samples_have_distinct_exit_and_keep_results() { + let count = AtomicUsize::new(0); + let server = Server::new(move |path| { + if path.starts_with("/__down") && count.fetch_add(1, Ordering::SeqCst) > 0 { + Response::new(403, "forbidden") + } else { + Response::normal(path) + } + }); + let output = server.run(&["-n", "2"]); + assert_eq!(output.status.code(), Some(3)); + let report = json(&output); + assert_eq!(report["status"], "partial"); + assert_eq!(report["speed_measurements"][0]["successes"], 1); + assert!(report["speed_measurements"][0]["avg"].as_f64().unwrap() > 0.0); +} + +#[test] +fn p1_cli_success_and_disabled_latency_are_explicit() { + let server = Server::new(Response::normal); + let output = server.run(&[]); + assert_eq!(output.status.code(), Some(0)); + let report = json(&output); + assert_eq!(report["status"], "complete"); + assert_eq!(report["latency_measurement"]["status"], "disabled"); + assert_eq!(report["latency_measurement"]["attempts"], 0); +} + +#[test] +fn p1_cli_valid_zero_latency_is_preserved() { + let server = Server::new(|path| { + let mut response = Response::normal(path); + if path == "/__down?bytes=0" { + response + .headers + .push(("Server-Timing".into(), "cfL4;desc=\"?rtt=0\"".into())); + } + response + }); + let output = server.run(&["--nr-latency-tests", "1"]); + let report = json(&output); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(report["latency_measurement"]["status"], "complete"); + assert_eq!(report["latency_measurement"]["successes"], 1); + assert_eq!(report["latency_measurement"]["avg_latency_ms"], 0.0); +} + +#[test] +fn p1_cli_refuses_retry_after_beyond_budget() { + let count = Arc::new(AtomicUsize::new(0)); + let observed = count.clone(); + let server = Server::new(move |path| { + if path.starts_with("/__down") { + observed.fetch_add(1, Ordering::SeqCst); + let mut r = Response::new(429, "retry later"); + r.headers.push(("Retry-After".into(), "86400".into())); + r + } else { + Response::normal(path) + } + }); + let output = server.run(&["--max-retry-wait", "1"]); + assert_eq!(output.status.code(), Some(1)); + assert_eq!(json(&output)["stop_reason"], "retry_budget"); + assert_eq!(count.load(Ordering::SeqCst), 1); +} + +#[test] +fn p1_cli_deadline_bounds_body_reads() { + let server = Server::new(|path| { + let mut r = Response::normal(path); + if path.starts_with("/__down") { + r.delay = Duration::from_secs(2); + } + r + }); + let start = Instant::now(); + let output = server.run(&["--max-duration", "1"]); + assert!( + start.elapsed() < Duration::from_millis(1600), + "elapsed={:?}", + start.elapsed() + ); + assert_eq!(output.status.code(), Some(1)); + let report = json(&output); + assert_eq!(report["stop_reason"], "deadline"); + assert_eq!(report["speed_measurements"][0]["successes"], 0); +} + +#[test] +fn p1_cli_upload_body_timeout_is_not_success() { + let server = Server::new(|path| { + let mut response = Response::normal(path); + if path == "/__up" { + response.delay = Duration::from_secs(2); + } + response + }); + let output = server.run(&["--upload-only", "--max-duration", "1"]); + let report = json(&output); + assert_eq!(report["speed_measurements"][0]["successes"], 0, "{report}"); + assert_eq!(report["stop_reason"], "deadline"); + assert_eq!(output.status.code(), Some(1)); +} + +#[test] +fn p1_cli_metadata_body_is_covered_by_overall_deadline() { + let requests = Arc::new(AtomicUsize::new(0)); + let observed = requests.clone(); + let server = Server::new(move |path| { + observed.fetch_add(1, Ordering::SeqCst); + let mut r = Response::normal(path); + if path == "/cdn-cgi/trace" { + r.delay = Duration::from_secs(2); + } + r + }); + let start = Instant::now(); + let output = server.run(&["--max-duration", "1"]); + assert!(start.elapsed() < Duration::from_millis(1600)); + assert_eq!(output.status.code(), Some(1)); + let report = json(&output); + assert_eq!(report["stop_reason"], "deadline"); + assert!(report["metadata"].is_null()); + assert_eq!(requests.load(Ordering::SeqCst), 1); +} + +#[test] +fn p1_cli_latency_rejects_truncated_body_and_retains_valid_samples() { + let latency_count = AtomicUsize::new(0); + let server = Server::new(move |path| { + let mut response = Response::normal(path); + if path == "/__down?bytes=0" && latency_count.fetch_add(1, Ordering::SeqCst) == 0 { + response.body = b"x".to_vec(); + response.declared_length = Some(4); + } + response + }); + let output = server.run(&["--nr-latency-tests", "2"]); + let report = json(&output); + assert_eq!(output.status.code(), Some(3)); + assert_eq!(report["latency_measurement"]["attempts"], 2); + assert_eq!(report["latency_measurement"]["successes"], 1); + assert_eq!( + report["latency_measurement"]["errors"] + .as_array() + .unwrap() + .len(), + 1 + ); + assert_eq!(report["latency_measurement"]["status"], "partial"); + assert!( + report["latency_measurement"]["avg_latency_ms"] + .as_f64() + .unwrap() + >= 0.0 + ); +} + +#[test] +fn p1_cli_recovered_retry_is_complete_and_keeps_error_history() { + let count = AtomicUsize::new(0); + let server = Server::new(move |path| { + if path.starts_with("/__down") && count.fetch_add(1, Ordering::SeqCst) == 0 { + let mut response = Response::new(429, "retry"); + response.headers.push(("Retry-After".into(), "0".into())); + response + } else { + Response::normal(path) + } + }); + let output = server.run(&[]); + let report = json(&output); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(report["status"], "complete"); + assert_eq!(report["speed_measurements"][0]["attempts"], 2); + assert_eq!(report["speed_measurements"][0]["successes"], 1); + assert_eq!(report["errors"][0]["status_code"], 429); +} + +#[test] +fn p1_cli_csv_failures_keep_stdout_parseable() { + let server = Server::new(|path| { + if path.starts_with("/__down") { + Response::new(403, "forbidden") + } else { + Response::normal(path) + } + }); + let output = server.run(&["-o", "csv"]); + assert_eq!(output.status.code(), Some(1)); + let mut reader = csv::Reader::from_reader(output.stdout.as_slice()); + assert_eq!(reader.headers().unwrap().get(0), Some("test_type")); + let success_column = reader + .headers() + .unwrap() + .iter() + .position(|name| name == "successes") + .unwrap(); + let records: Vec<_> = reader.records().collect::>().unwrap(); + assert_eq!(records.len(), 1); + assert_eq!(records[0].get(success_column), Some("0")); + assert!(!output.stderr.is_empty()); +} + +#[cfg(unix)] +#[test] +fn p1_cli_cancellation_preserves_completed_samples() { + let count = Arc::new(AtomicUsize::new(0)); + let observed = count.clone(); + let server = Server::new(move |path| { + if path.starts_with("/__down") && observed.fetch_add(1, Ordering::SeqCst) > 0 { + let mut r = Response::new(429, "retry later"); + r.headers.push(("Retry-After".into(), "2".into())); + r + } else { + Response::normal(path) + } + }); + let mut child = server.command(&["-n", "2"]).spawn().unwrap(); + let start = Instant::now(); + while count.load(Ordering::SeqCst) < 2 { + if start.elapsed() > Duration::from_secs(2) { + child.kill().unwrap(); + child.wait().unwrap(); + panic!("CLI did not reach retry"); + } + std::thread::sleep(Duration::from_millis(5)); + } + assert!(std::process::Command::new("kill") + .args(["-INT", &child.id().to_string()]) + .status() + .unwrap() + .success()); + let output = support::finish(child, Duration::from_secs(1)); + assert_eq!(output.status.code(), Some(130)); + let report = json(&output); + assert_eq!(report["stop_reason"], "cancelled"); + assert_eq!(report["speed_measurements"][0]["successes"], 1); +} diff --git a/tests/support/mod.rs b/tests/support/mod.rs new file mode 100644 index 0000000..89ce4d7 --- /dev/null +++ b/tests/support/mod.rs @@ -0,0 +1,176 @@ +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::process::{Child, Command, Output, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +pub struct Response { + pub status: u16, + pub body: Vec, + pub headers: Vec<(String, String)>, + pub delay: Duration, + pub disconnect: bool, + pub declared_length: Option, +} + +impl Response { + pub fn new(status: u16, body: impl Into>) -> Self { + Self { + status, + body: body.into(), + headers: vec![], + delay: Duration::ZERO, + disconnect: false, + declared_length: None, + } + } + + pub fn normal(path: &str) -> Self { + match path { + "/cdn-cgi/trace" => Self::new(200, "ip=127.0.0.1\nloc=CH\ncolo=ZRH\n"), + "/__down?bytes=0" => Self::new(200, ""), + _ => Self::new(200, vec![1; 100_000]), + } + } +} + +pub struct Server { + pub url: String, + running: Arc, + handle: Option>, +} + +impl Server { + pub fn new(handler: impl Fn(&str) -> Response + Send + 'static) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + listener.set_nonblocking(true).unwrap(); + let url = format!("http://{}", listener.local_addr().unwrap()); + let running = Arc::new(AtomicBool::new(true)); + let active = running.clone(); + let handle = thread::spawn(move || { + while active.load(Ordering::SeqCst) { + match listener.accept() { + Ok((mut stream, _)) => { + if let Some(path) = read_request(&mut stream) { + let response = handler(&path); + if response.disconnect { + continue; + } + let mut head = format!( + "HTTP/1.1 {} Test\r\nContent-Length: {}\r\nConnection: close\r\n", + response.status, + response.declared_length.unwrap_or(response.body.len()) + ); + for (name, value) in response.headers { + head.push_str(&format!("{name}: {value}\r\n")); + } + head.push_str("\r\n"); + if stream.write_all(head.as_bytes()).is_err() { + continue; + } + let start = Instant::now(); + while start.elapsed() < response.delay && active.load(Ordering::SeqCst) + { + thread::sleep(Duration::from_millis(5)); + } + let _ = stream.write_all(&response.body); + } + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(2)) + } + Err(error) => panic!("mock accept: {error}"), + } + } + }); + Self { + url, + running, + handle: Some(handle), + } + } + + pub fn command(&self, args: &[&str]) -> Command { + let mut command = Command::new(env!("CARGO_BIN_EXE_cfspeedtest")); + command.args(["--server", &self.url]); + if !args.contains(&"--upload-only") { + command.arg("--download-only"); + } + for (flag, value) in [ + ("-n", "1"), + ("--nr-latency-tests", "0"), + ("-m", "100k"), + ("-o", "json"), + ] { + if !args.contains(&flag) { + command.args([flag, value]); + } + } + command.args(args).env("NO_PROXY", "*").env("no_proxy", "*"); + command.stdout(Stdio::piped()).stderr(Stdio::piped()); + command + } + + pub fn run(&self, args: &[&str]) -> Output { + finish(self.command(args).spawn().unwrap(), Duration::from_secs(4)) + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.running.store(false, Ordering::SeqCst); + self.handle.take().unwrap().join().unwrap(); + } +} + +pub fn finish(mut child: Child, timeout: Duration) -> Output { + let start = Instant::now(); + while child.try_wait().unwrap().is_none() { + if start.elapsed() > timeout { + child.kill().unwrap(); + let output = child.wait_with_output().unwrap(); + panic!( + "CLI exceeded {timeout:?}; stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + thread::sleep(Duration::from_millis(5)); + } + child.wait_with_output().unwrap() +} + +fn read_request(stream: &mut TcpStream) -> Option { + stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + stream + .set_write_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let mut bytes = Vec::new(); + let mut buf = [0; 4096]; + loop { + let count = stream.read(&mut buf).ok()?; + if count == 0 { + return None; + } + bytes.extend_from_slice(&buf[..count]); + if let Some(end) = bytes.windows(4).position(|b| b == b"\r\n\r\n") { + let headers = String::from_utf8_lossy(&bytes[..end]).into_owned(); + let length = headers + .lines() + .filter_map(|l| l.split_once(':')) + .find(|(k, _)| k.eq_ignore_ascii_case("content-length")) + .map_or(0, |(_, v)| v.trim().parse::().unwrap()); + while bytes.len() < end + 4 + length { + let count = stream.read(&mut buf).ok()?; + if count == 0 { + return None; + } + bytes.extend_from_slice(&buf[..count]); + } + return headers.split_whitespace().nth(1).map(str::to_owned); + } + } +} From cba4b1cf01d075d08afa604fd1dca48b2a0de3a4 Mon Sep 17 00:00:00 2001 From: Robin B Date: Mon, 14 Sep 2026 17:51:49 +0200 Subject: [PATCH 2/2] update deps --- Cargo.lock | 753 +++++++++++++++++++++++------------------------------ 1 file changed, 329 insertions(+), 424 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ff2173..08ae9fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,28 +4,13 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse 0.2.7", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - [[package]] name = "anstream" version = "1.0.0" @@ -33,7 +18,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", - "anstyle-parse 1.0.0", + "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -47,15 +32,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - [[package]] name = "anstyle-parse" version = "1.0.0" @@ -99,9 +75,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block2" @@ -114,21 +96,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.57" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "shlex", @@ -142,9 +124,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "cfspeedtest" @@ -165,11 +147,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "clap" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -177,11 +170,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ - "anstream 1.0.0", + "anstream", "anstyle", "clap_lex", "strsim", @@ -189,23 +182,23 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.0" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] @@ -222,9 +215,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "cookie" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ "percent-encoding", "time", @@ -249,6 +242,15 @@ dependencies = [ "url", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "csv" version = "1.4.0" @@ -281,14 +283,42 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + [[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "dispatch2" @@ -296,7 +326,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -304,13 +334,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] @@ -324,9 +354,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -334,11 +364,11 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ - "anstream 0.6.21", + "anstream", "anstyle", "env_filter", "jiff", @@ -353,9 +383,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "form_urlencoded" @@ -368,9 +398,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -378,33 +408,33 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-io", @@ -430,23 +460,23 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasip2", + "rand_core", "wasm-bindgen", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -456,9 +486,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -466,9 +496,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -476,9 +506,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -501,9 +531,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -514,7 +544,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -522,15 +551,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -562,12 +590,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -575,9 +604,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -588,9 +617,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -602,16 +631,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -622,15 +652,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -654,9 +684,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -664,9 +694,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown", @@ -674,19 +704,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.10" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is_terminal_polyfill" @@ -696,41 +716,54 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ + "defmt", + "jiff-core", "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "jiff-tzdb" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" [[package]] name = "jiff-tzdb-platform" @@ -743,11 +776,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -759,9 +793,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -771,27 +805,27 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru-slab" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", @@ -804,7 +838,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -812,9 +846,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "objc2" @@ -855,32 +889,26 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -891,20 +919,11 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -927,9 +946,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ "bytes", "cfg_aliases", @@ -947,14 +966,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", "rand", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -968,67 +988,64 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "rand_chacha", + "chacha20", + "getrandom 0.4.3", "rand_core", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rand_core" -version = "0.9.5" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "getrandom 0.3.4", + "rand_core", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -1038,9 +1055,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -1049,9 +1066,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -1111,15 +1128,15 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -1131,9 +1148,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -1141,9 +1158,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -1152,9 +1169,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -1164,9 +1181,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1174,29 +1191,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap", "itoa", @@ -1220,9 +1237,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "slab" @@ -1232,15 +1249,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -1266,9 +1283,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -1292,37 +1320,36 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -1332,15 +1359,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -1348,9 +1375,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -1358,24 +1385,15 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.50.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -1387,9 +1405,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -1412,20 +1430,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.2", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -1522,20 +1540,11 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -1546,23 +1555,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1570,31 +1575,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -1612,9 +1617,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -1631,16 +1636,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -1658,31 +1654,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -1691,113 +1670,59 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1806,68 +1731,48 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -1876,9 +1781,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -1887,17 +1792,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"