├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── pre-commit.yml │ ├── rust-clippy.yml │ ├── security-checks.yml │ ├── CI.yml │ └── release.yaml ├── examples ├── download_test.rs ├── latency_test.rs └── simple_speedtest.rs ├── Dockerfile ├── src ├── progress.rs ├── main.rs ├── boxplot.rs ├── lib.rs ├── measurements.rs └── speedtest.rs ├── .pre-commit-config.yaml ├── Cargo.toml ├── LICENSE.txt ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .aider* 3 | CLAUDE.md 4 | GEMINI.md 5 | 6 | # Local agent instructions 7 | AGENTS.md 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | assignees: 8 | - "code-inflation" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | pre-commit: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: actions/setup-python@v6 17 | - uses: pre-commit/action@v3.0.1 18 | -------------------------------------------------------------------------------- /examples/download_test.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest::test_download; 2 | use cfspeedtest::OutputFormat; 3 | 4 | fn main() { 5 | println!("Testing download speed with 10MB of payload"); 6 | 7 | let download_speed = test_download( 8 | &reqwest::blocking::Client::new(), 9 | 10_000_000, 10 | OutputFormat::None, // don't write to stdout while running the test 11 | ); 12 | 13 | println!("download speed in mbit: {download_speed}") 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim-bullseye as builder 2 | WORKDIR /usr/src/cfspeedtest 3 | COPY Cargo.toml Cargo.lock ./ 4 | COPY src ./src 5 | RUN cargo install --path . 6 | 7 | FROM debian:bullseye-slim 8 | RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* 9 | COPY --from=builder /usr/local/cargo/bin/cfspeedtest /usr/local/bin/cfspeedtest 10 | 11 | # tini will be PID 1 and handle signal forwarding and process reaping 12 | ENTRYPOINT ["/usr/bin/tini", "--", "cfspeedtest"] 13 | -------------------------------------------------------------------------------- /src/progress.rs: -------------------------------------------------------------------------------- 1 | use std::io::stdout; 2 | use std::io::Write; 3 | 4 | pub fn print_progress(name: &str, curr: u32, max: u32) { 5 | const BAR_LEN: u32 = 30; 6 | let progress_line = ((curr as f32 / max as f32) * BAR_LEN as f32) as u32; 7 | let remaining_line = BAR_LEN - progress_line; 8 | print!( 9 | "\r{:<15} [{}{}]", 10 | name, 11 | (0..progress_line).map(|_| "=").collect::(), 12 | (0..remaining_line).map(|_| "-").collect::(), 13 | ); 14 | stdout().flush().expect("error printing progress bar"); 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/rust-clippy.yml: -------------------------------------------------------------------------------- 1 | # source: https://doc.rust-lang.org/nightly/clippy/continuous_integration/github_actions.html 2 | name: Clippy check 3 | 4 | on: 5 | push: 6 | branches: [ "master" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | schedule: 10 | - cron: '0 3 * * 1' 11 | 12 | # Make sure CI fails on all warnings, including Clippy lints 13 | env: 14 | RUSTFLAGS: "-Dwarnings" 15 | 16 | jobs: 17 | clippy_check: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | - name: Run Clippy 22 | run: cargo clippy --all-targets --all-features -------------------------------------------------------------------------------- /examples/latency_test.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest::run_latency_test; 2 | use cfspeedtest::OutputFormat; 3 | 4 | fn main() { 5 | println!("Testing latency"); 6 | 7 | let (latency_results, avg_latency) = run_latency_test( 8 | &reqwest::blocking::Client::new(), 9 | 25, 10 | OutputFormat::None, // don't write to stdout while running the test 11 | ); 12 | 13 | println!("average latancy in ms: {avg_latency}"); 14 | 15 | println!("all latency test results"); 16 | for latency_result in latency_results { 17 | println!("latency in ms: {latency_result}"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | stages: [pre-commit] 7 | - id: check-json 8 | stages: [pre-commit] 9 | - id: check-toml 10 | stages: [pre-commit] 11 | - id: check-merge-conflict 12 | stages: [pre-commit] 13 | - id: check-case-conflict 14 | stages: [pre-commit] 15 | - id: detect-private-key 16 | stages: [pre-commit] 17 | - repo: https://github.com/crate-ci/typos 18 | rev: v1.16.20 19 | hooks: 20 | - id: typos 21 | stages: [pre-commit] 22 | -------------------------------------------------------------------------------- /.github/workflows/security-checks.yml: -------------------------------------------------------------------------------- 1 | name: Rust Security Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: '0 3 * * 1' 12 | 13 | jobs: 14 | cargo_audit: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v6 18 | - name: Cache cargo-audit 19 | uses: actions/cache@v4 20 | with: 21 | path: ~/.cargo/bin/cargo-audit 22 | key: cargo-audit-${{ runner.os }}-v0.21.2 23 | - name: Install cargo audit 24 | run: | 25 | if ! command -v cargo-audit &> /dev/null; then 26 | cargo install cargo-audit 27 | fi 28 | - name: Run Cargo Audit 29 | run: cargo audit 30 | continue-on-error: true 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cfspeedtest" 3 | version = "2.0.2" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Unofficial CLI for speed.cloudflare.com" 7 | readme = "README.md" 8 | repository = "https://github.com/code-inflation/cfspeedtest/" 9 | keywords = ["cli", "speedtest", "speed-test", "cloudflare"] 10 | categories = ["command-line-utilities"] 11 | exclude = [".github/"] 12 | 13 | [dependencies] 14 | log = "0.4" 15 | env_logger = "0.11" 16 | regex = "1.12" 17 | reqwest = { version = "0.12.25", default-features = false, features = ["blocking", "rustls-tls"] } 18 | clap = { version = "4.5", features = ["derive"] } 19 | serde = { version = "1.0", features = ["derive"] } 20 | csv = "1.3.0" 21 | serde_json = { version = "1.0", features = ["preserve_order"] } 22 | indexmap = "2.12" 23 | clap_complete = "4.5" 24 | -------------------------------------------------------------------------------- /examples/simple_speedtest.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest::speed_test; 2 | use cfspeedtest::speedtest::PayloadSize; 3 | use cfspeedtest::OutputFormat; 4 | use cfspeedtest::SpeedTestCLIOptions; 5 | 6 | fn main() { 7 | // define speedtest options 8 | let options = SpeedTestCLIOptions { 9 | output_format: OutputFormat::None, // don't write to stdout 10 | ipv4: None, // don't force ipv4 usage 11 | ipv6: None, // don't force ipv6 usage 12 | verbose: false, 13 | upload_only: false, 14 | download_only: false, 15 | nr_tests: 5, 16 | nr_latency_tests: 20, 17 | max_payload_size: PayloadSize::M10, 18 | disable_dynamic_max_payload_size: false, 19 | completion: None, 20 | }; 21 | 22 | let measurements = speed_test(reqwest::blocking::Client::new(), options); 23 | measurements 24 | .iter() 25 | .for_each(|measurement| println!("{measurement}")); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | - name: Ensure 'cargo fmt' 19 | run: cargo fmt -- --check 20 | - name: Build 21 | run: cargo build --verbose 22 | - name: Run tests 23 | run: cargo test --verbose 24 | - name: Run example - simple_speedtest 25 | run: cargo run --example simple_speedtest 26 | - name: Run example - download_test 27 | run: cargo run --example download_test 28 | - name: Run example - latency_test 29 | run: cargo run --example latency_test 30 | - name: Run CLI 31 | run: cargo run 32 | 33 | docker-build-amd64: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v6 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | - name: Build AMD64 Docker image 40 | uses: docker/build-push-action@v5 41 | with: 42 | platforms: linux/amd64 43 | push: false 44 | provenance: false 45 | tags: | 46 | cybuerg/cfspeedtest:${{ github.sha }}-amd64 47 | 48 | docker-build-arm64: 49 | runs-on: ubuntu-24.04-arm 50 | steps: 51 | - uses: actions/checkout@v6 52 | - name: Build ARM64 Docker image 53 | run: docker build --platform linux/arm64 -t cybuerg/cfspeedtest:${{ github.sha }}-arm64 . 54 | 55 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use cfspeedtest::speedtest; 2 | use cfspeedtest::OutputFormat; 3 | use cfspeedtest::SpeedTestCLIOptions; 4 | use clap::{CommandFactory, Parser}; 5 | use clap_complete::generate; 6 | use std::io; 7 | use std::net::IpAddr; 8 | 9 | use speedtest::speed_test; 10 | 11 | fn print_completions(gen: G, cmd: &mut clap::Command) { 12 | generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); 13 | } 14 | 15 | fn main() { 16 | env_logger::init(); 17 | let options = SpeedTestCLIOptions::parse(); 18 | 19 | if let Some(generator) = options.completion { 20 | let mut cmd = SpeedTestCLIOptions::command(); 21 | eprintln!("Generating completion script for {generator}..."); 22 | print_completions(generator, &mut cmd); 23 | return; 24 | } 25 | 26 | if options.output_format == OutputFormat::StdOut { 27 | println!("Starting Cloudflare speed test"); 28 | } 29 | let client; 30 | if let Some(ref ip) = options.ipv4 { 31 | client = reqwest::blocking::Client::builder() 32 | .local_address(ip.parse::().expect("Invalid IPv4 address")) 33 | .timeout(std::time::Duration::from_secs(30)) 34 | .build(); 35 | } else if let Some(ref ip) = options.ipv6 { 36 | client = reqwest::blocking::Client::builder() 37 | .local_address(ip.parse::().expect("Invalid IPv6 address")) 38 | .timeout(std::time::Duration::from_secs(30)) 39 | .build(); 40 | } else { 41 | client = reqwest::blocking::Client::builder() 42 | .timeout(std::time::Duration::from_secs(30)) 43 | .build(); 44 | } 45 | speed_test( 46 | client.expect("Failed to initialize reqwest client"), 47 | options, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | upload-assets: 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: 25 | - windows-latest 26 | include: 27 | - target: aarch64-unknown-linux-gnu 28 | os: ubuntu-latest 29 | - target: aarch64-apple-darwin 30 | os: macos-latest 31 | - target: x86_64-unknown-linux-gnu 32 | os: ubuntu-latest 33 | - target: x86_64-apple-darwin 34 | os: macos-latest 35 | - target: universal-apple-darwin 36 | os: macos-latest 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - uses: actions/checkout@v6 40 | - uses: taiki-e/upload-rust-binary-action@v1 41 | with: 42 | bin: cfspeedtest 43 | target: ${{ matrix.target }} 44 | tar: unix 45 | zip: windows 46 | token: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | docker-build-amd64: 49 | needs: [create-release, upload-assets] 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v6 53 | - name: Set up Docker Buildx 54 | uses: docker/setup-buildx-action@v3 55 | - name: Log in to DockerHub 56 | uses: docker/login-action@v3 57 | with: 58 | username: ${{ secrets.DOCKERHUB_USERNAME }} 59 | password: ${{ secrets.DOCKERHUB_TOKEN }} 60 | - name: Build and push AMD64 Docker image 61 | uses: docker/build-push-action@v5 62 | with: 63 | platforms: linux/amd64 64 | push: true 65 | provenance: false 66 | tags: | 67 | cybuerg/cfspeedtest:${{ github.ref_name }}-amd64 68 | cybuerg/cfspeedtest:latest-amd64 69 | 70 | docker-build-arm64: 71 | needs: [create-release, upload-assets] 72 | runs-on: ubuntu-24.04-arm 73 | steps: 74 | - uses: actions/checkout@v6 75 | - name: Log in to DockerHub 76 | uses: docker/login-action@v3 77 | with: 78 | username: ${{ secrets.DOCKERHUB_USERNAME }} 79 | password: ${{ secrets.DOCKERHUB_TOKEN }} 80 | - name: Build and push ARM64 Docker image 81 | run: | 82 | docker build --platform linux/arm64 -t cybuerg/cfspeedtest:${{ github.ref_name }}-arm64 -t cybuerg/cfspeedtest:latest-arm64 . 83 | docker push cybuerg/cfspeedtest:${{ github.ref_name }}-arm64 84 | docker push cybuerg/cfspeedtest:latest-arm64 85 | 86 | docker-create-manifest: 87 | needs: [docker-build-amd64, docker-build-arm64] 88 | runs-on: ubuntu-latest 89 | steps: 90 | - name: Log in to DockerHub 91 | uses: docker/login-action@v3 92 | with: 93 | username: ${{ secrets.DOCKERHUB_USERNAME }} 94 | password: ${{ secrets.DOCKERHUB_TOKEN }} 95 | - name: Create and push multi-arch manifest 96 | run: | 97 | # Create manifest for versioned tag 98 | docker manifest create cybuerg/cfspeedtest:${{ github.ref_name }} \ 99 | cybuerg/cfspeedtest:${{ github.ref_name }}-amd64 \ 100 | cybuerg/cfspeedtest:${{ github.ref_name }}-arm64 101 | docker manifest push cybuerg/cfspeedtest:${{ github.ref_name }} 102 | 103 | # Create manifest for latest tag 104 | docker manifest create cybuerg/cfspeedtest:latest \ 105 | cybuerg/cfspeedtest:latest-amd64 \ 106 | cybuerg/cfspeedtest:latest-arm64 107 | docker manifest push cybuerg/cfspeedtest:latest 108 | -------------------------------------------------------------------------------- /src/boxplot.rs: -------------------------------------------------------------------------------- 1 | use log; 2 | use std::fmt::Write; 3 | 4 | const PLOT_WIDTH: usize = 80; 5 | 6 | fn generate_axis_labels(minima: f64, maxima: f64) -> String { 7 | let mut labels = String::new(); 8 | write!(labels, "{minima:<10.2}").unwrap(); 9 | write!( 10 | labels, 11 | "{:^width$.2}", 12 | (minima + maxima) / 2.0, 13 | width = PLOT_WIDTH - 20 14 | ) 15 | .unwrap(); 16 | write!(labels, "{maxima:>10.2}").unwrap(); 17 | labels 18 | } 19 | 20 | pub(crate) fn render_plot(minima: f64, q1: f64, median: f64, q3: f64, maxima: f64) -> String { 21 | let value_range = maxima - minima; 22 | let quartile_0 = q1 - minima; 23 | let quartile_1 = median - q1; 24 | let quartile_2 = q3 - median; 25 | let quartile_3 = maxima - q3; 26 | 27 | let scale_factor = PLOT_WIDTH as f64 / value_range; 28 | 29 | let mut plot = String::with_capacity(PLOT_WIDTH); 30 | plot.push('|'); 31 | plot.push_str("-".repeat((quartile_0 * scale_factor) as usize).as_str()); 32 | plot.push_str("=".repeat((quartile_1 * scale_factor) as usize).as_str()); 33 | plot.push(':'); 34 | plot.push_str("=".repeat((quartile_2 * scale_factor) as usize).as_str()); 35 | plot.push_str("-".repeat((quartile_3 * scale_factor) as usize).as_str()); 36 | plot.push('|'); 37 | 38 | let axis_labels = generate_axis_labels(minima, maxima); 39 | plot.push('\n'); 40 | plot.push_str(&axis_labels); 41 | 42 | log::debug!("fn input: {minima}, {q1}, {median}, {q3}, {maxima}"); 43 | log::debug!("quartiles: {quartile_0}, {quartile_1}, {quartile_2}, {quartile_3}"); 44 | log::debug!("value range: {value_range}"); 45 | log::debug!("len of the plot: {}", plot.len()); 46 | 47 | plot 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | 54 | #[test] 55 | fn test_generate_axis_labels() { 56 | let labels = generate_axis_labels(0.0, 100.0); 57 | assert!(labels.starts_with("0.00")); 58 | assert!(labels.ends_with("100.00")); 59 | assert!(labels.contains("50.00")); 60 | assert_eq!(labels.len(), PLOT_WIDTH); 61 | } 62 | 63 | #[test] 64 | fn test_generate_axis_labels_negative() { 65 | let labels = generate_axis_labels(-50.0, 50.0); 66 | assert!(labels.starts_with("-50.00")); 67 | assert!(labels.ends_with("50.00")); 68 | assert!(labels.contains("0.00")); 69 | } 70 | 71 | #[test] 72 | fn test_render_plot_basic() { 73 | let plot = render_plot(0.0, 25.0, 50.0, 75.0, 100.0); 74 | 75 | // Should contain boxplot characters 76 | assert!(plot.contains('|')); 77 | assert!(plot.contains('-')); 78 | assert!(plot.contains('=')); 79 | assert!(plot.contains(':')); 80 | 81 | // Should contain axis labels 82 | assert!(plot.contains("0.00")); 83 | assert!(plot.contains("100.00")); 84 | assert!(plot.contains("50.00")); 85 | 86 | // Should have newline separating plot from labels 87 | assert!(plot.contains('\n')); 88 | } 89 | 90 | #[test] 91 | fn test_render_plot_same_values() { 92 | let plot = render_plot(50.0, 50.0, 50.0, 50.0, 50.0); 93 | 94 | // When all values are the same, should still render 95 | assert!(plot.contains('|')); 96 | assert!(plot.contains(':')); 97 | assert!(plot.contains("50.00")); 98 | } 99 | 100 | #[test] 101 | fn test_render_plot_structure() { 102 | let plot = render_plot(10.0, 30.0, 50.0, 70.0, 90.0); 103 | let lines: Vec<&str> = plot.split('\n').collect(); 104 | 105 | // Should have exactly 2 lines: plot and axis labels 106 | assert_eq!(lines.len(), 2); 107 | 108 | // First line should be the boxplot 109 | assert!(lines[0].starts_with('|')); 110 | assert!(lines[0].ends_with('|')); 111 | 112 | // Second line should be the axis labels 113 | assert!(lines[1].contains("10.00")); 114 | assert!(lines[1].contains("90.00")); 115 | } 116 | 117 | #[test] 118 | fn test_render_plot_quartile_ordering() { 119 | let plot = render_plot(0.0, 20.0, 50.0, 80.0, 100.0); 120 | 121 | // Find the positions of key characters 122 | let colon_pos = plot.find(':').unwrap(); 123 | let first_pipe = plot.find('|').unwrap(); 124 | let last_pipe = plot.rfind('|').unwrap(); 125 | 126 | // Colon (median) should be between the pipes 127 | assert!(colon_pos > first_pipe); 128 | assert!(colon_pos < last_pipe); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cfspeedtest - Unofficial CLI for [speed.cloudflare.com](https://speed.cloudflare.com) 2 | ![CI](https://github.com/code-inflation/cfspeedtest/actions/workflows/CI.yml/badge.svg) 3 | ![Release](https://github.com/code-inflation/cfspeedtest/actions/workflows/release.yaml/badge.svg) 4 | ![Crates.io Version](https://img.shields.io/crates/v/cfspeedtest) 5 | ![Crates.io Downloads](https://img.shields.io/crates/d/cfspeedtest?label=Crates.io%20downloads) 6 | 7 | 8 | ## Installation 9 | Install using `cargo`: 10 | ```sh 11 | cargo install cfspeedtest 12 | ``` 13 | 14 | Or download the latest binary release here: [cfspeedtest/releases/latest](https://github.com/code-inflation/cfspeedtest/releases/latest) 15 | 16 | Alternatively there is also a [docker image available on dockerhub](https://hub.docker.com/r/cybuerg/cfspeedtest) 17 | ```sh 18 | docker run cybuerg/cfspeedtest 19 | ``` 20 | 21 | ## Usage 22 | ``` 23 | > cfspeedtest --help 24 | Unofficial CLI for speed.cloudflare.com 25 | 26 | Usage: cfspeedtest [OPTIONS] 27 | 28 | Options: 29 | -n, --nr-tests 30 | Number of test runs per payload size [default: 10] 31 | --nr-latency-tests 32 | Number of latency tests to run [default: 25] 33 | -m, --max-payload-size 34 | The max payload size in bytes to use [100k, 1m, 10m, 25m or 100m] [default: 25MB] 35 | -o, --output-format 36 | Set the output format [csv, json or json-pretty] > This silences all other output to stdout [default: StdOut] 37 | -v, --verbose 38 | Enable verbose output i.e. print boxplots of the measurements 39 | --ipv4 [] 40 | Force IPv4 with provided source IPv4 address or the default IPv4 address bound to the main interface 41 | --ipv6 [] 42 | Force IPv6 with provided source IPv6 address or the default IPv6 address bound to the main interface 43 | -d, --disable-dynamic-max-payload-size 44 | Disables dynamically skipping tests with larger payload sizes if the tests for the previous payload size took longer than 5 seconds 45 | --download-only 46 | Test download speed only 47 | --upload-only 48 | Test upload speed only 49 | --generate-completion 50 | Generate shell completion script for the specified shell [possible values: bash, elvish, fish, powershell, zsh] 51 | -h, --help 52 | Print help 53 | -V, --version 54 | Print version 55 | ``` 56 | 57 | Example usage: 58 | [![asciicast](https://asciinema.org/a/Moun5mFB1sm1VFkkFljG9UGyz.svg)](https://asciinema.org/a/Moun5mFB1sm1VFkkFljG9UGyz) 59 | 60 | Example with json-pretty output: 61 | [![asciicast](https://asciinema.org/a/P6IUAADtaCq3bT18GbYVHmksA.svg)](https://asciinema.org/a/P6IUAADtaCq3bT18GbYVHmksA) 62 | 63 | ### Shell Completion 64 | 65 | `cfspeedtest` supports generating shell completion scripts. Use the `--generate-completion` flag followed by your shell name (e.g., `bash`, `zsh`, `fish`, `powershell`, `elvish`). 66 | 67 | Example for bash (add to `~/.bashrc` or similar): 68 | ```sh 69 | cfspeedtest --generate-completion bash > ~/.local/share/bash-completion/completions/cfspeedtest 70 | # Or, if you don't have a completions directory set up: 71 | # source <(cfspeedtest --generate-completion bash) 72 | ``` 73 | 74 | Example for zsh (add to `~/.zshrc` or similar): 75 | ```sh 76 | # Ensure your fpath includes a directory for completions, e.g., ~/.zfunc 77 | # mkdir -p ~/.zfunc 78 | # echo 'fpath=(~/.zfunc $fpath)' >> ~/.zshrc 79 | cfspeedtest --generate-completion zsh > ~/.zfunc/_cfspeedtest 80 | # You may need to run compinit: 81 | # autoload -U compinit && compinit 82 | ``` 83 | 84 | Example for fish: 85 | ```sh 86 | cfspeedtest --generate-completion fish > ~/.config/fish/completions/cfspeedtest.fish 87 | ``` 88 | 89 | 90 | ## Development 91 | 92 | ### Logging 93 | Set the log level using the `RUST_LOG` env var: 94 | ```sh 95 | RUST_LOG=debug cargo run 96 | ``` 97 | ### Release 98 | #### Using `cargo-release` 99 | Install `cargo-release`: 100 | ```sh 101 | cargo install cargo-release 102 | ``` 103 | Create the release (version bump levels are `[patch, minor, major]`): 104 | ```sh 105 | cargo release patch --execute 106 | ``` 107 | This will bump the `cfspeedtest` version in both `Cargo.toml` and `Cargo.lock` and run `cargo publish` to push the release on crates.io. Additionally a version git tag is created and pushed to `master` triggering the GH action that creates the binary releases. 108 | 109 | #### On GitHub 110 | Release builds are published automatically using github actions. They are triggered when a git tag in the format `v[0-9]+.*` is pushed. 111 | ```sh 112 | git tag v1.0.0 113 | git push origin v1.0.0 114 | ``` 115 | #### On crates.io 116 | 1. Update `cfspeedtest` version in `Cargo.toml` 117 | 2. `cargo publish --dry-run` 118 | 3. Verify contents using `cargo package --list` 119 | 4. Upload to crates.io `cargo publish` 120 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod boxplot; 2 | pub mod measurements; 3 | pub mod progress; 4 | pub mod speedtest; 5 | use std::fmt; 6 | use std::fmt::Display; 7 | 8 | use clap::Parser; 9 | use clap_complete::Shell; 10 | use speedtest::PayloadSize; 11 | 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 13 | pub enum OutputFormat { 14 | Csv, 15 | Json, 16 | JsonPretty, 17 | StdOut, 18 | None, 19 | } 20 | 21 | impl Display for OutputFormat { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | write!(f, "{self:?}") 24 | } 25 | } 26 | 27 | impl OutputFormat { 28 | pub fn from(output_format_string: String) -> Result { 29 | match output_format_string.to_lowercase().as_str() { 30 | "csv" => Ok(Self::Csv), 31 | "json" => Ok(Self::Json), 32 | "json_pretty" | "json-pretty" => Ok(Self::JsonPretty), 33 | "stdout" => Ok(Self::StdOut), 34 | _ => Err("Value needs to be one of csv, json or json-pretty".to_string()), 35 | } 36 | } 37 | } 38 | 39 | /// Unofficial CLI for speed.cloudflare.com 40 | #[derive(Parser, Debug)] 41 | #[command(author, version, about, long_about = None)] 42 | pub struct SpeedTestCLIOptions { 43 | /// Number of test runs per payload size. 44 | #[arg(value_parser = clap::value_parser!(u32).range(1..1000), short, long, default_value_t = 10)] 45 | pub nr_tests: u32, 46 | 47 | /// Number of latency tests to run 48 | #[arg(long, default_value_t = 25)] 49 | pub nr_latency_tests: u32, 50 | 51 | /// The max payload size in bytes to use [100k, 1m, 10m, 25m or 100m] 52 | #[arg(value_parser = parse_payload_size, short, long, default_value_t = PayloadSize::M25)] 53 | pub max_payload_size: PayloadSize, 54 | 55 | /// Set the output format [csv, json or json-pretty] > 56 | /// This silences all other output to stdout 57 | #[arg(value_parser = parse_output_format, short, long, default_value_t = OutputFormat::StdOut)] 58 | pub output_format: OutputFormat, 59 | 60 | /// Enable verbose output i.e. print boxplots of the measurements 61 | #[arg(short, long)] 62 | pub verbose: bool, 63 | 64 | /// Force IPv4 with provided source IPv4 address or the default IPv4 address bound to the main interface 65 | #[clap(long, value_name = "IPv4", num_args = 0..=1, default_missing_value = "0.0.0.0", conflicts_with = "ipv6")] 66 | pub ipv4: Option, 67 | 68 | /// Force IPv6 with provided source IPv6 address or the default IPv6 address bound to the main interface 69 | #[clap(long, value_name = "IPv6", num_args = 0..=1, default_missing_value = "::", conflicts_with = "ipv4")] 70 | pub ipv6: Option, 71 | 72 | /// Disables dynamically skipping tests with larger payload sizes if the tests for the previous payload 73 | /// size took longer than 5 seconds 74 | #[arg(short, long)] 75 | pub disable_dynamic_max_payload_size: bool, 76 | 77 | /// Test download speed only 78 | #[arg(long, conflicts_with = "upload_only")] 79 | pub download_only: bool, 80 | 81 | /// Test upload speed only 82 | #[arg(long, conflicts_with = "download_only")] 83 | pub upload_only: bool, 84 | 85 | /// Generate shell completion script for the specified shell 86 | #[arg(long = "generate-completion", value_enum)] 87 | pub completion: Option, 88 | } 89 | 90 | impl SpeedTestCLIOptions { 91 | /// Returns whether download tests should be performed 92 | pub fn should_download(&self) -> bool { 93 | self.download_only || !self.upload_only 94 | } 95 | 96 | /// Returns whether upload tests should be performed 97 | pub fn should_upload(&self) -> bool { 98 | self.upload_only || !self.download_only 99 | } 100 | } 101 | 102 | fn parse_payload_size(input_string: &str) -> Result { 103 | PayloadSize::from(input_string.to_string()) 104 | } 105 | 106 | fn parse_output_format(input_string: &str) -> Result { 107 | OutputFormat::from(input_string.to_string()) 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | 114 | #[test] 115 | fn test_output_format_from_valid_inputs() { 116 | assert_eq!(OutputFormat::from("csv".to_string()), Ok(OutputFormat::Csv)); 117 | assert_eq!(OutputFormat::from("CSV".to_string()), Ok(OutputFormat::Csv)); 118 | assert_eq!( 119 | OutputFormat::from("json".to_string()), 120 | Ok(OutputFormat::Json) 121 | ); 122 | assert_eq!( 123 | OutputFormat::from("JSON".to_string()), 124 | Ok(OutputFormat::Json) 125 | ); 126 | assert_eq!( 127 | OutputFormat::from("json-pretty".to_string()), 128 | Ok(OutputFormat::JsonPretty) 129 | ); 130 | assert_eq!( 131 | OutputFormat::from("json_pretty".to_string()), 132 | Ok(OutputFormat::JsonPretty) 133 | ); 134 | assert_eq!( 135 | OutputFormat::from("JSON-PRETTY".to_string()), 136 | Ok(OutputFormat::JsonPretty) 137 | ); 138 | assert_eq!( 139 | OutputFormat::from("stdout".to_string()), 140 | Ok(OutputFormat::StdOut) 141 | ); 142 | assert_eq!( 143 | OutputFormat::from("STDOUT".to_string()), 144 | Ok(OutputFormat::StdOut) 145 | ); 146 | } 147 | 148 | #[test] 149 | fn test_output_format_from_invalid_inputs() { 150 | assert!(OutputFormat::from("invalid".to_string()).is_err()); 151 | assert!(OutputFormat::from("xml".to_string()).is_err()); 152 | assert!(OutputFormat::from("".to_string()).is_err()); 153 | assert!(OutputFormat::from("json_invalid".to_string()).is_err()); 154 | 155 | let error_msg = OutputFormat::from("invalid".to_string()).unwrap_err(); 156 | assert_eq!( 157 | error_msg, 158 | "Value needs to be one of csv, json or json-pretty" 159 | ); 160 | } 161 | 162 | #[test] 163 | fn test_output_format_display() { 164 | assert_eq!(format!("{}", OutputFormat::Csv), "Csv"); 165 | assert_eq!(format!("{}", OutputFormat::Json), "Json"); 166 | assert_eq!(format!("{}", OutputFormat::JsonPretty), "JsonPretty"); 167 | assert_eq!(format!("{}", OutputFormat::StdOut), "StdOut"); 168 | assert_eq!(format!("{}", OutputFormat::None), "None"); 169 | } 170 | 171 | #[test] 172 | fn test_cli_options_should_download() { 173 | let mut options = SpeedTestCLIOptions { 174 | nr_tests: 10, 175 | nr_latency_tests: 25, 176 | max_payload_size: speedtest::PayloadSize::M25, 177 | output_format: OutputFormat::StdOut, 178 | verbose: false, 179 | ipv4: None, 180 | ipv6: None, 181 | disable_dynamic_max_payload_size: false, 182 | download_only: false, 183 | upload_only: false, 184 | completion: None, 185 | }; 186 | 187 | // Default: both download and upload 188 | assert!(options.should_download()); 189 | assert!(options.should_upload()); 190 | 191 | // Download only 192 | options.download_only = true; 193 | assert!(options.should_download()); 194 | assert!(!options.should_upload()); 195 | 196 | // Upload only 197 | options.download_only = false; 198 | options.upload_only = true; 199 | assert!(!options.should_download()); 200 | assert!(options.should_upload()); 201 | } 202 | 203 | #[test] 204 | fn test_cli_options_should_upload() { 205 | let mut options = SpeedTestCLIOptions { 206 | nr_tests: 10, 207 | nr_latency_tests: 25, 208 | max_payload_size: speedtest::PayloadSize::M25, 209 | output_format: OutputFormat::StdOut, 210 | verbose: false, 211 | ipv4: None, 212 | ipv6: None, 213 | disable_dynamic_max_payload_size: false, 214 | download_only: false, 215 | upload_only: false, 216 | completion: None, 217 | }; 218 | 219 | // Default: both download and upload 220 | assert!(options.should_upload()); 221 | assert!(options.should_download()); 222 | 223 | // Upload only 224 | options.upload_only = true; 225 | assert!(options.should_upload()); 226 | assert!(!options.should_download()); 227 | 228 | // Download only 229 | options.upload_only = false; 230 | options.download_only = true; 231 | assert!(!options.should_upload()); 232 | assert!(options.should_download()); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/measurements.rs: -------------------------------------------------------------------------------- 1 | use crate::boxplot; 2 | use crate::speedtest::Metadata; 3 | use crate::speedtest::TestType; 4 | use crate::OutputFormat; 5 | use indexmap::IndexSet; 6 | use serde::Serialize; 7 | use std::{fmt::Display, io}; 8 | 9 | #[derive(Serialize)] 10 | struct StatMeasurement { 11 | test_type: TestType, 12 | payload_size: usize, 13 | min: f64, 14 | q1: f64, 15 | median: f64, 16 | q3: f64, 17 | max: f64, 18 | avg: f64, 19 | } 20 | 21 | #[derive(Serialize)] 22 | pub struct Measurement { 23 | pub test_type: TestType, 24 | pub payload_size: usize, 25 | pub mbit: f64, 26 | } 27 | 28 | #[derive(Serialize)] 29 | pub struct LatencyMeasurement { 30 | pub avg_latency_ms: f64, 31 | pub min_latency_ms: f64, 32 | pub max_latency_ms: f64, 33 | pub latency_measurements: Vec, 34 | } 35 | 36 | impl Display for Measurement { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | write!( 39 | f, 40 | "{:?}: \t{}\t-> {}", 41 | self.test_type, 42 | format_bytes(self.payload_size), 43 | self.mbit, 44 | ) 45 | } 46 | } 47 | 48 | pub(crate) fn log_measurements( 49 | measurements: &[Measurement], 50 | latency_measurement: Option<&LatencyMeasurement>, 51 | payload_sizes: Vec, 52 | verbose: bool, 53 | output_format: OutputFormat, 54 | metadata: Option<&Metadata>, 55 | ) { 56 | if output_format == OutputFormat::StdOut { 57 | println!("\nSummary Statistics"); 58 | println!("Type Payload | min/max/avg in mbit/s"); 59 | } 60 | let mut stat_measurements: Vec = Vec::new(); 61 | measurements 62 | .iter() 63 | .map(|m| m.test_type) 64 | .collect::>() 65 | .iter() 66 | .for_each(|t| { 67 | stat_measurements.extend(log_measurements_by_test_type( 68 | measurements, 69 | payload_sizes.clone(), 70 | verbose, 71 | output_format, 72 | *t, 73 | )) 74 | }); 75 | match output_format { 76 | OutputFormat::Csv => { 77 | let mut wtr = csv::Writer::from_writer(io::stdout()); 78 | for measurement in &stat_measurements { 79 | wtr.serialize(measurement).unwrap(); 80 | } 81 | wtr.flush().unwrap(); 82 | } 83 | OutputFormat::Json => { 84 | let output = compose_output_json(&stat_measurements, latency_measurement, metadata); 85 | serde_json::to_writer(io::stdout(), &output).unwrap(); 86 | println!(); 87 | } 88 | OutputFormat::JsonPretty => { 89 | let output = compose_output_json(&stat_measurements, latency_measurement, metadata); 90 | serde_json::to_writer_pretty(io::stdout(), &output).unwrap(); 91 | println!(); 92 | } 93 | OutputFormat::StdOut => {} 94 | OutputFormat::None => {} 95 | } 96 | } 97 | 98 | fn compose_output_json( 99 | stat_measurements: &[StatMeasurement], 100 | latency_measurement: Option<&LatencyMeasurement>, 101 | metadata: Option<&Metadata>, 102 | ) -> serde_json::Map { 103 | let mut output = serde_json::Map::new(); 104 | if let Some(metadata) = metadata { 105 | output.insert( 106 | "metadata".to_string(), 107 | serde_json::to_value(metadata).unwrap(), 108 | ); 109 | } 110 | if let Some(latency) = latency_measurement { 111 | output.insert( 112 | "latency_measurement".to_string(), 113 | serde_json::to_value(latency).unwrap(), 114 | ); 115 | } 116 | output.insert( 117 | "speed_measurements".to_string(), 118 | serde_json::to_value(stat_measurements).unwrap(), 119 | ); 120 | output 121 | } 122 | 123 | fn log_measurements_by_test_type( 124 | measurements: &[Measurement], 125 | payload_sizes: Vec, 126 | verbose: bool, 127 | output_format: OutputFormat, 128 | test_type: TestType, 129 | ) -> Vec { 130 | let mut stat_measurements: Vec = Vec::new(); 131 | for payload_size in payload_sizes { 132 | let type_measurements: Vec = measurements 133 | .iter() 134 | .filter(|m| m.test_type == test_type) 135 | .filter(|m| m.payload_size == payload_size) 136 | .map(|m| m.mbit) 137 | .collect(); 138 | 139 | // check if there are any measurements for the current payload_size 140 | // skip stats calculation if there are no measurements 141 | if !type_measurements.is_empty() { 142 | let (min, q1, median, q3, max, avg) = calc_stats(type_measurements).unwrap(); 143 | 144 | let formatted_payload = format_bytes(payload_size); 145 | let fmt_test_type = format!("{test_type:?}"); 146 | stat_measurements.push(StatMeasurement { 147 | test_type, 148 | payload_size, 149 | min, 150 | q1, 151 | median, 152 | q3, 153 | max, 154 | avg, 155 | }); 156 | if output_format == OutputFormat::StdOut { 157 | println!( 158 | "{fmt_test_type:<9} {formatted_payload:<7}| min {min:<7.2} max {max:<7.2} avg {avg:<7.2}" 159 | ); 160 | if verbose { 161 | let plot = boxplot::render_plot(min, q1, median, q3, max); 162 | println!("{plot}\n"); 163 | } 164 | } 165 | } 166 | } 167 | 168 | stat_measurements 169 | } 170 | 171 | fn calc_stats(mbit_measurements: Vec) -> Option<(f64, f64, f64, f64, f64, f64)> { 172 | log::debug!("calc_stats for mbit_measurements {mbit_measurements:?}"); 173 | let length = mbit_measurements.len(); 174 | if length == 0 { 175 | return None; 176 | } 177 | 178 | let mut sorted_data = mbit_measurements.clone(); 179 | sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Less)); 180 | 181 | if length == 1 { 182 | return Some(( 183 | sorted_data[0], 184 | sorted_data[0], 185 | sorted_data[0], 186 | sorted_data[0], 187 | sorted_data[0], 188 | sorted_data[0], 189 | )); 190 | } 191 | 192 | if length < 4 { 193 | return Some(( 194 | *sorted_data.first().unwrap(), 195 | *sorted_data.first().unwrap(), 196 | median(&sorted_data), 197 | *sorted_data.last().unwrap(), 198 | *sorted_data.last().unwrap(), 199 | mbit_measurements.iter().sum::() / mbit_measurements.len() as f64, 200 | )); 201 | } 202 | 203 | let q1 = if length.is_multiple_of(2) { 204 | median(&sorted_data[0..length / 2]) 205 | } else { 206 | median(&sorted_data[0..length.div_ceil(2)]) 207 | }; 208 | 209 | let q3 = if length.is_multiple_of(2) { 210 | median(&sorted_data[length / 2..length]) 211 | } else { 212 | median(&sorted_data[length.div_ceil(2)..length]) 213 | }; 214 | 215 | Some(( 216 | *sorted_data.first().unwrap(), 217 | q1, 218 | median(&sorted_data), 219 | q3, 220 | *sorted_data.last().unwrap(), 221 | mbit_measurements.iter().sum::() / mbit_measurements.len() as f64, 222 | )) 223 | } 224 | 225 | fn median(data: &[f64]) -> f64 { 226 | let length = data.len(); 227 | if length.is_multiple_of(2) { 228 | (data[length / 2 - 1] + data[length / 2]) / 2.0 229 | } else { 230 | data[length / 2] 231 | } 232 | } 233 | 234 | pub(crate) fn format_bytes(bytes: usize) -> String { 235 | match bytes { 236 | 1_000..=999_999 => format!("{}KB", bytes / 1_000), 237 | 1_000_000..=999_999_999 => format!("{}MB", bytes / 1_000_000), 238 | _ => format!("{bytes} bytes"), 239 | } 240 | } 241 | 242 | #[cfg(test)] 243 | mod tests { 244 | use super::*; 245 | 246 | #[test] 247 | fn test_format_bytes() { 248 | assert_eq!(format_bytes(500), "500 bytes"); 249 | assert_eq!(format_bytes(1_000), "1KB"); 250 | assert_eq!(format_bytes(100_000), "100KB"); 251 | assert_eq!(format_bytes(999_999), "999KB"); 252 | assert_eq!(format_bytes(1_000_000), "1MB"); 253 | assert_eq!(format_bytes(25_000_000), "25MB"); 254 | assert_eq!(format_bytes(100_000_000), "100MB"); 255 | assert_eq!(format_bytes(999_999_999), "999MB"); 256 | assert_eq!(format_bytes(1_000_000_000), "1000000000 bytes"); 257 | } 258 | 259 | #[test] 260 | fn test_measurement_display() { 261 | let measurement = Measurement { 262 | test_type: TestType::Download, 263 | payload_size: 1_000_000, 264 | mbit: 50.5, 265 | }; 266 | 267 | let display_str = format!("{measurement}"); 268 | assert!(display_str.contains("Download")); 269 | assert!(display_str.contains("1MB")); 270 | assert!(display_str.contains("50.5")); 271 | } 272 | 273 | #[test] 274 | fn test_calc_stats_empty() { 275 | assert_eq!(calc_stats(vec![]), None); 276 | } 277 | 278 | #[test] 279 | fn test_calc_stats_single_value() { 280 | let result = calc_stats(vec![10.0]).unwrap(); 281 | assert_eq!(result, (10.0, 10.0, 10.0, 10.0, 10.0, 10.0)); 282 | } 283 | 284 | #[test] 285 | fn test_calc_stats_two_values() { 286 | let result = calc_stats(vec![10.0, 20.0]).unwrap(); 287 | assert_eq!(result.0, 10.0); // min 288 | assert_eq!(result.4, 20.0); // max 289 | assert_eq!(result.2, 15.0); // median 290 | assert_eq!(result.5, 15.0); // avg 291 | } 292 | 293 | #[test] 294 | fn test_calc_stats_multiple_values() { 295 | let result = calc_stats(vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap(); 296 | assert_eq!(result.0, 1.0); // min 297 | assert_eq!(result.4, 5.0); // max 298 | assert_eq!(result.2, 3.0); // median 299 | assert_eq!(result.5, 3.0); // avg 300 | } 301 | 302 | #[test] 303 | fn test_calc_stats_unsorted() { 304 | let result = calc_stats(vec![5.0, 1.0, 3.0, 2.0, 4.0]).unwrap(); 305 | assert_eq!(result.0, 1.0); // min 306 | assert_eq!(result.4, 5.0); // max 307 | assert_eq!(result.2, 3.0); // median 308 | assert_eq!(result.5, 3.0); // avg 309 | } 310 | 311 | #[test] 312 | fn test_median_odd_length() { 313 | assert_eq!(median(&[1.0, 2.0, 3.0]), 2.0); 314 | assert_eq!(median(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0); 315 | } 316 | 317 | #[test] 318 | fn test_median_even_length() { 319 | assert_eq!(median(&[1.0, 2.0]), 1.5); 320 | assert_eq!(median(&[1.0, 2.0, 3.0, 4.0]), 2.5); 321 | } 322 | 323 | #[test] 324 | fn test_median_single_value() { 325 | assert_eq!(median(&[5.0]), 5.0); 326 | } 327 | 328 | #[test] 329 | fn test_compose_output_json_includes_metadata() { 330 | let stat_measurements = vec![StatMeasurement { 331 | test_type: TestType::Download, 332 | payload_size: 100_000, 333 | min: 1.0, 334 | q1: 1.5, 335 | median: 2.0, 336 | q3: 2.5, 337 | max: 3.0, 338 | avg: 2.0, 339 | }]; 340 | let latency = LatencyMeasurement { 341 | avg_latency_ms: 10.0, 342 | min_latency_ms: 9.0, 343 | max_latency_ms: 11.0, 344 | latency_measurements: vec![9.0, 10.0, 11.0], 345 | }; 346 | let metadata = Metadata { 347 | city: "City".to_string(), 348 | country: "Country".to_string(), 349 | ip: "127.0.0.1".to_string(), 350 | asn: "ASN".to_string(), 351 | colo: "ABC".to_string(), 352 | }; 353 | 354 | let output = 355 | super::compose_output_json(&stat_measurements, Some(&latency), Some(&metadata)); 356 | 357 | let metadata_value = output.get("metadata").expect("metadata missing"); 358 | let metadata_obj = metadata_value.as_object().expect("metadata not an object"); 359 | assert_eq!( 360 | metadata_obj.get("city").and_then(|v| v.as_str()), 361 | Some("City") 362 | ); 363 | assert_eq!( 364 | metadata_obj.get("country").and_then(|v| v.as_str()), 365 | Some("Country") 366 | ); 367 | assert_eq!( 368 | metadata_obj.get("ip").and_then(|v| v.as_str()), 369 | Some("127.0.0.1") 370 | ); 371 | assert_eq!( 372 | metadata_obj.get("asn").and_then(|v| v.as_str()), 373 | Some("ASN") 374 | ); 375 | assert_eq!( 376 | metadata_obj.get("colo").and_then(|v| v.as_str()), 377 | Some("ABC") 378 | ); 379 | 380 | assert!(output.get("latency_measurement").is_some()); 381 | assert!(output.get("speed_measurements").is_some()); 382 | 383 | let keys: Vec<&str> = output.keys().map(String::as_str).collect(); 384 | assert_eq!( 385 | keys, 386 | vec!["metadata", "latency_measurement", "speed_measurements"] 387 | ); 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/speedtest.rs: -------------------------------------------------------------------------------- 1 | use crate::measurements::format_bytes; 2 | use crate::measurements::log_measurements; 3 | use crate::measurements::LatencyMeasurement; 4 | use crate::measurements::Measurement; 5 | use crate::progress::print_progress; 6 | use crate::OutputFormat; 7 | use crate::SpeedTestCLIOptions; 8 | use log; 9 | use regex::Regex; 10 | use reqwest::{blocking::Client, StatusCode}; 11 | use serde::Serialize; 12 | use std::{ 13 | fmt::Display, 14 | sync::atomic::{AtomicBool, Ordering}, 15 | time::{Duration, Instant}, 16 | }; 17 | 18 | const BASE_URL: &str = "https://speed.cloudflare.com"; 19 | const DOWNLOAD_URL: &str = "__down?bytes="; 20 | const UPLOAD_URL: &str = "__up"; 21 | static WARNED_NEGATIVE_LATENCY: AtomicBool = AtomicBool::new(false); 22 | 23 | #[derive(Clone, Copy, Debug, Hash, Serialize, Eq, PartialEq)] 24 | pub enum TestType { 25 | Download, 26 | Upload, 27 | } 28 | 29 | #[derive(Clone, Debug, PartialEq, Eq)] 30 | pub enum PayloadSize { 31 | K100 = 100_000, 32 | M1 = 1_000_000, 33 | M10 = 10_000_000, 34 | M25 = 25_000_000, 35 | M100 = 100_000_000, 36 | } 37 | 38 | impl Display for PayloadSize { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | write!(f, "{}", format_bytes(self.clone() as usize)) 41 | } 42 | } 43 | 44 | impl PayloadSize { 45 | pub fn from(payload_string: String) -> Result { 46 | match payload_string.to_lowercase().as_str() { 47 | "100_000" | "100000" | "100k" | "100kb" => Ok(Self::K100), 48 | "1_000_000" | "1000000" | "1m" | "1mb" => Ok(Self::M1), 49 | "10_000_000" | "10000000" | "10m" | "10mb" => Ok(Self::M10), 50 | "25_000_000" | "25000000" | "25m" | "25mb" => Ok(Self::M25), 51 | "100_000_000" | "100000000" | "100m" | "100mb" => Ok(Self::M100), 52 | _ => Err("Value needs to be one of 100k, 1m, 10m, 25m or 100m".to_string()), 53 | } 54 | } 55 | 56 | pub fn sizes_from_max(max_payload_size: PayloadSize) -> Vec { 57 | log::debug!("getting payload iterations for max_payload_size {max_payload_size:?}"); 58 | let payload_bytes: Vec = 59 | vec![100_000, 1_000_000, 10_000_000, 25_000_000, 100_000_000]; 60 | match max_payload_size { 61 | PayloadSize::K100 => payload_bytes[0..1].to_vec(), 62 | PayloadSize::M1 => payload_bytes[0..2].to_vec(), 63 | PayloadSize::M10 => payload_bytes[0..3].to_vec(), 64 | PayloadSize::M25 => payload_bytes[0..4].to_vec(), 65 | PayloadSize::M100 => payload_bytes[0..5].to_vec(), 66 | } 67 | } 68 | } 69 | 70 | #[derive(Clone, Debug, Serialize)] 71 | pub struct Metadata { 72 | pub city: String, 73 | pub country: String, 74 | pub ip: String, 75 | pub asn: String, 76 | pub colo: String, 77 | } 78 | 79 | impl Display for Metadata { 80 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 81 | write!( 82 | f, 83 | "City: {}\nCountry: {}\nIp: {}\nAsn: {}\nColo: {}", 84 | self.city, self.country, self.ip, self.asn, self.colo 85 | ) 86 | } 87 | } 88 | 89 | pub fn speed_test(client: Client, options: SpeedTestCLIOptions) -> Vec { 90 | let metadata = match fetch_metadata(&client) { 91 | Ok(metadata) => metadata, 92 | Err(e) => { 93 | eprintln!("Error fetching metadata: {e}"); 94 | std::process::exit(1); 95 | } 96 | }; 97 | if options.output_format == OutputFormat::StdOut { 98 | println!("{metadata}"); 99 | } 100 | let (latency_measurements, avg_latency) = 101 | run_latency_test(&client, options.nr_latency_tests, options.output_format); 102 | let latency_measurement = if !latency_measurements.is_empty() { 103 | Some(LatencyMeasurement { 104 | avg_latency_ms: avg_latency, 105 | min_latency_ms: latency_measurements 106 | .iter() 107 | .copied() 108 | .fold(f64::INFINITY, f64::min), 109 | max_latency_ms: latency_measurements 110 | .iter() 111 | .copied() 112 | .fold(f64::NEG_INFINITY, f64::max), 113 | latency_measurements, 114 | }) 115 | } else { 116 | None 117 | }; 118 | 119 | let payload_sizes = PayloadSize::sizes_from_max(options.max_payload_size.clone()); 120 | let mut measurements = Vec::new(); 121 | 122 | if options.should_download() { 123 | measurements.extend(run_tests( 124 | &client, 125 | test_download, 126 | TestType::Download, 127 | payload_sizes.clone(), 128 | options.nr_tests, 129 | options.output_format, 130 | options.disable_dynamic_max_payload_size, 131 | )); 132 | } 133 | 134 | if options.should_upload() { 135 | measurements.extend(run_tests( 136 | &client, 137 | test_upload, 138 | TestType::Upload, 139 | payload_sizes.clone(), 140 | options.nr_tests, 141 | options.output_format, 142 | options.disable_dynamic_max_payload_size, 143 | )); 144 | } 145 | 146 | log_measurements( 147 | &measurements, 148 | latency_measurement.as_ref(), 149 | payload_sizes, 150 | options.verbose, 151 | options.output_format, 152 | Some(&metadata), 153 | ); 154 | measurements 155 | } 156 | 157 | pub fn run_latency_test( 158 | client: &Client, 159 | nr_latency_tests: u32, 160 | output_format: OutputFormat, 161 | ) -> (Vec, f64) { 162 | let mut measurements: Vec = Vec::new(); 163 | for i in 0..nr_latency_tests { 164 | if output_format == OutputFormat::StdOut { 165 | print_progress("latency test", i + 1, nr_latency_tests); 166 | } 167 | let latency = test_latency(client); 168 | measurements.push(latency); 169 | } 170 | let avg_latency = measurements.iter().sum::() / measurements.len() as f64; 171 | 172 | if output_format == OutputFormat::StdOut { 173 | println!( 174 | "\nAvg GET request latency {avg_latency:.2} ms (RTT excluding server processing time)\n" 175 | ); 176 | } 177 | (measurements, avg_latency) 178 | } 179 | 180 | pub fn test_latency(client: &Client) -> f64 { 181 | let url = &format!("{}/{}{}", BASE_URL, DOWNLOAD_URL, 0); 182 | let req_builder = client.get(url); 183 | 184 | let start = Instant::now(); 185 | let mut response = req_builder.send().expect("failed to get response"); 186 | let _status_code = response.status(); 187 | // Drain body to complete the request; ignore errors. 188 | let _ = std::io::copy(&mut response, &mut std::io::sink()); 189 | let total_ms = start.elapsed().as_secs_f64() * 1_000.0; 190 | 191 | let re = Regex::new(r"cfRequestDuration;dur=([\d.]+)").unwrap(); 192 | let server_timing = response 193 | .headers() 194 | .get("Server-Timing") 195 | .expect("No Server-Timing in response header") 196 | .to_str() 197 | .unwrap(); 198 | let cf_req_duration: f64 = re 199 | .captures(server_timing) 200 | .unwrap() 201 | .get(1) 202 | .unwrap() 203 | .as_str() 204 | .parse() 205 | .unwrap(); 206 | let mut req_latency = total_ms - cf_req_duration; 207 | log::debug!( 208 | "latency debug: total_ms={total_ms:.3} cf_req_duration_ms={cf_req_duration:.3} req_latency_total={req_latency:.3} server_timing={server_timing}" 209 | ); 210 | if req_latency < 0.0 { 211 | if !WARNED_NEGATIVE_LATENCY.swap(true, Ordering::Relaxed) { 212 | log::warn!( 213 | "negative latency after server timing subtraction; clamping to 0.0 (total_ms={total_ms:.3} cf_req_duration_ms={cf_req_duration:.3})" 214 | ); 215 | } 216 | req_latency = 0.0 217 | } 218 | req_latency 219 | } 220 | 221 | const TIME_THRESHOLD: Duration = Duration::from_secs(5); 222 | 223 | pub fn run_tests( 224 | client: &Client, 225 | test_fn: fn(&Client, usize, OutputFormat) -> f64, 226 | test_type: TestType, 227 | payload_sizes: Vec, 228 | nr_tests: u32, 229 | output_format: OutputFormat, 230 | disable_dynamic_max_payload_size: bool, 231 | ) -> Vec { 232 | let mut measurements: Vec = Vec::new(); 233 | for payload_size in payload_sizes { 234 | log::debug!("running tests for payload_size {payload_size}"); 235 | let start = Instant::now(); 236 | for i in 0..nr_tests { 237 | if output_format == OutputFormat::StdOut { 238 | print_progress( 239 | &format!("{:?} {:<5}", test_type, format_bytes(payload_size)), 240 | i, 241 | nr_tests, 242 | ); 243 | } 244 | let mbit = test_fn(client, payload_size, output_format); 245 | measurements.push(Measurement { 246 | test_type, 247 | payload_size, 248 | mbit, 249 | }); 250 | } 251 | if output_format == OutputFormat::StdOut { 252 | print_progress( 253 | &format!("{:?} {:<5}", test_type, format_bytes(payload_size)), 254 | nr_tests, 255 | nr_tests, 256 | ); 257 | println!() 258 | } 259 | let duration = start.elapsed(); 260 | 261 | // only check TIME_THRESHOLD if dynamic max payload sizing is not disabled 262 | if !disable_dynamic_max_payload_size && duration > TIME_THRESHOLD { 263 | log::info!("Exceeded threshold"); 264 | break; 265 | } 266 | } 267 | measurements 268 | } 269 | 270 | pub fn test_upload(client: &Client, payload_size_bytes: usize, output_format: OutputFormat) -> f64 { 271 | let url = &format!("{BASE_URL}/{UPLOAD_URL}"); 272 | let payload: Vec = vec![1; payload_size_bytes]; 273 | let req_builder = client.post(url).body(payload); 274 | let (mut response, status_code, mbits, duration) = { 275 | let start = Instant::now(); 276 | let response = req_builder.send().expect("failed to get response"); 277 | let status_code = response.status(); 278 | let duration = start.elapsed(); 279 | let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64(); 280 | (response, status_code, mbits, duration) 281 | }; 282 | // Drain response after timing so we don't skew upload measurement. 283 | let _ = std::io::copy(&mut response, &mut std::io::sink()); 284 | if output_format == OutputFormat::StdOut { 285 | print_current_speed(mbits, duration, status_code, payload_size_bytes); 286 | } 287 | mbits 288 | } 289 | 290 | pub fn test_download( 291 | client: &Client, 292 | payload_size_bytes: usize, 293 | output_format: OutputFormat, 294 | ) -> f64 { 295 | let url = &format!("{BASE_URL}/{DOWNLOAD_URL}{payload_size_bytes}"); 296 | let req_builder = client.get(url); 297 | let (status_code, mbits, duration) = { 298 | let start = Instant::now(); 299 | let mut response = req_builder.send().expect("failed to get response"); 300 | let status_code = response.status(); 301 | // Stream the body to avoid buffering the full payload in memory. 302 | let _ = std::io::copy(&mut response, &mut std::io::sink()); 303 | let duration = start.elapsed(); 304 | let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64(); 305 | (status_code, mbits, duration) 306 | }; 307 | if output_format == OutputFormat::StdOut { 308 | print_current_speed(mbits, duration, status_code, payload_size_bytes); 309 | } 310 | mbits 311 | } 312 | 313 | fn print_current_speed( 314 | mbits: f64, 315 | duration: Duration, 316 | status_code: StatusCode, 317 | payload_size_bytes: usize, 318 | ) { 319 | print!( 320 | " {:>6.2} mbit/s | {:>5} in {:>4}ms -> status: {} ", 321 | mbits, 322 | format_bytes(payload_size_bytes), 323 | duration.as_millis(), 324 | status_code 325 | ); 326 | } 327 | 328 | pub fn fetch_metadata(client: &Client) -> Result { 329 | let url = &format!("{}/{}{}", BASE_URL, DOWNLOAD_URL, 0); 330 | let headers = client.get(url).send()?.headers().to_owned(); 331 | Ok(Metadata { 332 | city: extract_header_value(&headers, "cf-meta-city", "City N/A"), 333 | country: extract_header_value(&headers, "cf-meta-country", "Country N/A"), 334 | ip: extract_header_value(&headers, "cf-meta-ip", "IP N/A"), 335 | asn: extract_header_value(&headers, "cf-meta-asn", "ASN N/A"), 336 | colo: extract_header_value(&headers, "cf-meta-colo", "Colo N/A"), 337 | }) 338 | } 339 | 340 | fn extract_header_value( 341 | headers: &reqwest::header::HeaderMap, 342 | header_name: &str, 343 | na_value: &str, 344 | ) -> String { 345 | headers 346 | .get(header_name) 347 | .and_then(|value| value.to_str().ok()) 348 | .unwrap_or(na_value) 349 | .to_owned() 350 | } 351 | 352 | #[cfg(test)] 353 | mod tests { 354 | use super::*; 355 | 356 | #[test] 357 | fn test_payload_size_from_valid_inputs() { 358 | // Test 100K variants 359 | assert_eq!(PayloadSize::from("100k".to_string()), Ok(PayloadSize::K100)); 360 | assert_eq!(PayloadSize::from("100K".to_string()), Ok(PayloadSize::K100)); 361 | assert_eq!( 362 | PayloadSize::from("100kb".to_string()), 363 | Ok(PayloadSize::K100) 364 | ); 365 | assert_eq!( 366 | PayloadSize::from("100KB".to_string()), 367 | Ok(PayloadSize::K100) 368 | ); 369 | assert_eq!( 370 | PayloadSize::from("100000".to_string()), 371 | Ok(PayloadSize::K100) 372 | ); 373 | assert_eq!( 374 | PayloadSize::from("100_000".to_string()), 375 | Ok(PayloadSize::K100) 376 | ); 377 | 378 | // Test 1M variants 379 | assert_eq!(PayloadSize::from("1m".to_string()), Ok(PayloadSize::M1)); 380 | assert_eq!(PayloadSize::from("1M".to_string()), Ok(PayloadSize::M1)); 381 | assert_eq!(PayloadSize::from("1mb".to_string()), Ok(PayloadSize::M1)); 382 | assert_eq!(PayloadSize::from("1MB".to_string()), Ok(PayloadSize::M1)); 383 | assert_eq!( 384 | PayloadSize::from("1000000".to_string()), 385 | Ok(PayloadSize::M1) 386 | ); 387 | assert_eq!( 388 | PayloadSize::from("1_000_000".to_string()), 389 | Ok(PayloadSize::M1) 390 | ); 391 | 392 | // Test 10M variants 393 | assert_eq!(PayloadSize::from("10m".to_string()), Ok(PayloadSize::M10)); 394 | assert_eq!(PayloadSize::from("10M".to_string()), Ok(PayloadSize::M10)); 395 | assert_eq!(PayloadSize::from("10mb".to_string()), Ok(PayloadSize::M10)); 396 | assert_eq!(PayloadSize::from("10MB".to_string()), Ok(PayloadSize::M10)); 397 | assert_eq!( 398 | PayloadSize::from("10000000".to_string()), 399 | Ok(PayloadSize::M10) 400 | ); 401 | assert_eq!( 402 | PayloadSize::from("10_000_000".to_string()), 403 | Ok(PayloadSize::M10) 404 | ); 405 | 406 | // Test 25M variants 407 | assert_eq!(PayloadSize::from("25m".to_string()), Ok(PayloadSize::M25)); 408 | assert_eq!(PayloadSize::from("25M".to_string()), Ok(PayloadSize::M25)); 409 | assert_eq!(PayloadSize::from("25mb".to_string()), Ok(PayloadSize::M25)); 410 | assert_eq!(PayloadSize::from("25MB".to_string()), Ok(PayloadSize::M25)); 411 | assert_eq!( 412 | PayloadSize::from("25000000".to_string()), 413 | Ok(PayloadSize::M25) 414 | ); 415 | assert_eq!( 416 | PayloadSize::from("25_000_000".to_string()), 417 | Ok(PayloadSize::M25) 418 | ); 419 | 420 | // Test 100M variants 421 | assert_eq!(PayloadSize::from("100m".to_string()), Ok(PayloadSize::M100)); 422 | assert_eq!(PayloadSize::from("100M".to_string()), Ok(PayloadSize::M100)); 423 | assert_eq!( 424 | PayloadSize::from("100mb".to_string()), 425 | Ok(PayloadSize::M100) 426 | ); 427 | assert_eq!( 428 | PayloadSize::from("100MB".to_string()), 429 | Ok(PayloadSize::M100) 430 | ); 431 | assert_eq!( 432 | PayloadSize::from("100000000".to_string()), 433 | Ok(PayloadSize::M100) 434 | ); 435 | assert_eq!( 436 | PayloadSize::from("100_000_000".to_string()), 437 | Ok(PayloadSize::M100) 438 | ); 439 | } 440 | 441 | #[test] 442 | fn test_payload_size_from_invalid_inputs() { 443 | assert!(PayloadSize::from("invalid".to_string()).is_err()); 444 | assert!(PayloadSize::from("50m".to_string()).is_err()); 445 | assert!(PayloadSize::from("200k".to_string()).is_err()); 446 | assert!(PayloadSize::from("".to_string()).is_err()); 447 | assert!(PayloadSize::from("1g".to_string()).is_err()); 448 | 449 | let error_msg = PayloadSize::from("invalid".to_string()).unwrap_err(); 450 | assert_eq!( 451 | error_msg, 452 | "Value needs to be one of 100k, 1m, 10m, 25m or 100m" 453 | ); 454 | } 455 | 456 | #[test] 457 | fn test_payload_size_values() { 458 | assert_eq!(PayloadSize::K100 as usize, 100_000); 459 | assert_eq!(PayloadSize::M1 as usize, 1_000_000); 460 | assert_eq!(PayloadSize::M10 as usize, 10_000_000); 461 | assert_eq!(PayloadSize::M25 as usize, 25_000_000); 462 | assert_eq!(PayloadSize::M100 as usize, 100_000_000); 463 | } 464 | 465 | #[test] 466 | fn test_payload_size_sizes_from_max() { 467 | assert_eq!( 468 | PayloadSize::sizes_from_max(PayloadSize::K100), 469 | vec![100_000] 470 | ); 471 | assert_eq!( 472 | PayloadSize::sizes_from_max(PayloadSize::M1), 473 | vec![100_000, 1_000_000] 474 | ); 475 | assert_eq!( 476 | PayloadSize::sizes_from_max(PayloadSize::M10), 477 | vec![100_000, 1_000_000, 10_000_000] 478 | ); 479 | assert_eq!( 480 | PayloadSize::sizes_from_max(PayloadSize::M25), 481 | vec![100_000, 1_000_000, 10_000_000, 25_000_000] 482 | ); 483 | assert_eq!( 484 | PayloadSize::sizes_from_max(PayloadSize::M100), 485 | vec![100_000, 1_000_000, 10_000_000, 25_000_000, 100_000_000] 486 | ); 487 | } 488 | 489 | #[test] 490 | fn test_payload_size_display() { 491 | let size = PayloadSize::K100; 492 | let display_str = format!("{size}"); 493 | assert!(!display_str.is_empty()); 494 | } 495 | 496 | #[test] 497 | fn test_fetch_metadata_ipv6_timeout_error() { 498 | use std::time::Duration; 499 | 500 | let client = reqwest::blocking::Client::builder() 501 | .local_address("::".parse::().unwrap()) 502 | .timeout(Duration::from_millis(100)) 503 | .build() 504 | .unwrap(); 505 | 506 | let result = fetch_metadata(&client); 507 | assert!(result.is_err()); 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.21" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.13" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.7" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.5" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 49 | dependencies = [ 50 | "windows-sys 0.61.2", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.11" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys 0.61.2", 62 | ] 63 | 64 | [[package]] 65 | name = "atomic-waker" 66 | version = "1.1.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 69 | 70 | [[package]] 71 | name = "base64" 72 | version = "0.22.1" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 75 | 76 | [[package]] 77 | name = "bitflags" 78 | version = "2.10.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 81 | 82 | [[package]] 83 | name = "bumpalo" 84 | version = "3.19.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 87 | 88 | [[package]] 89 | name = "bytes" 90 | version = "1.11.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 93 | 94 | [[package]] 95 | name = "cc" 96 | version = "1.2.48" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" 99 | dependencies = [ 100 | "find-msvc-tools", 101 | "shlex", 102 | ] 103 | 104 | [[package]] 105 | name = "cfg-if" 106 | version = "1.0.4" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 109 | 110 | [[package]] 111 | name = "cfg_aliases" 112 | version = "0.2.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 115 | 116 | [[package]] 117 | name = "cfspeedtest" 118 | version = "2.0.2" 119 | dependencies = [ 120 | "clap", 121 | "clap_complete", 122 | "csv", 123 | "env_logger", 124 | "indexmap", 125 | "log", 126 | "regex", 127 | "reqwest", 128 | "serde", 129 | "serde_json", 130 | ] 131 | 132 | [[package]] 133 | name = "clap" 134 | version = "4.5.53" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" 137 | dependencies = [ 138 | "clap_builder", 139 | "clap_derive", 140 | ] 141 | 142 | [[package]] 143 | name = "clap_builder" 144 | version = "4.5.53" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" 147 | dependencies = [ 148 | "anstream", 149 | "anstyle", 150 | "clap_lex", 151 | "strsim", 152 | ] 153 | 154 | [[package]] 155 | name = "clap_complete" 156 | version = "4.5.61" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" 159 | dependencies = [ 160 | "clap", 161 | ] 162 | 163 | [[package]] 164 | name = "clap_derive" 165 | version = "4.5.49" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 168 | dependencies = [ 169 | "heck", 170 | "proc-macro2", 171 | "quote", 172 | "syn", 173 | ] 174 | 175 | [[package]] 176 | name = "clap_lex" 177 | version = "0.7.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 180 | 181 | [[package]] 182 | name = "colorchoice" 183 | version = "1.0.4" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 186 | 187 | [[package]] 188 | name = "csv" 189 | version = "1.4.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" 192 | dependencies = [ 193 | "csv-core", 194 | "itoa", 195 | "ryu", 196 | "serde_core", 197 | ] 198 | 199 | [[package]] 200 | name = "csv-core" 201 | version = "0.1.13" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" 204 | dependencies = [ 205 | "memchr", 206 | ] 207 | 208 | [[package]] 209 | name = "displaydoc" 210 | version = "0.2.5" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 213 | dependencies = [ 214 | "proc-macro2", 215 | "quote", 216 | "syn", 217 | ] 218 | 219 | [[package]] 220 | name = "env_filter" 221 | version = "0.1.4" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" 224 | dependencies = [ 225 | "log", 226 | "regex", 227 | ] 228 | 229 | [[package]] 230 | name = "env_logger" 231 | version = "0.11.8" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 234 | dependencies = [ 235 | "anstream", 236 | "anstyle", 237 | "env_filter", 238 | "jiff", 239 | "log", 240 | ] 241 | 242 | [[package]] 243 | name = "equivalent" 244 | version = "1.0.2" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 247 | 248 | [[package]] 249 | name = "find-msvc-tools" 250 | version = "0.1.5" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 253 | 254 | [[package]] 255 | name = "form_urlencoded" 256 | version = "1.2.2" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 259 | dependencies = [ 260 | "percent-encoding", 261 | ] 262 | 263 | [[package]] 264 | name = "futures-channel" 265 | version = "0.3.31" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 268 | dependencies = [ 269 | "futures-core", 270 | "futures-sink", 271 | ] 272 | 273 | [[package]] 274 | name = "futures-core" 275 | version = "0.3.31" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 278 | 279 | [[package]] 280 | name = "futures-io" 281 | version = "0.3.31" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 284 | 285 | [[package]] 286 | name = "futures-sink" 287 | version = "0.3.31" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 290 | 291 | [[package]] 292 | name = "futures-task" 293 | version = "0.3.31" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 296 | 297 | [[package]] 298 | name = "futures-util" 299 | version = "0.3.31" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 302 | dependencies = [ 303 | "futures-core", 304 | "futures-io", 305 | "futures-sink", 306 | "futures-task", 307 | "memchr", 308 | "pin-project-lite", 309 | "pin-utils", 310 | "slab", 311 | ] 312 | 313 | [[package]] 314 | name = "getrandom" 315 | version = "0.2.16" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 318 | dependencies = [ 319 | "cfg-if", 320 | "js-sys", 321 | "libc", 322 | "wasi", 323 | "wasm-bindgen", 324 | ] 325 | 326 | [[package]] 327 | name = "getrandom" 328 | version = "0.3.4" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 331 | dependencies = [ 332 | "cfg-if", 333 | "js-sys", 334 | "libc", 335 | "r-efi", 336 | "wasip2", 337 | "wasm-bindgen", 338 | ] 339 | 340 | [[package]] 341 | name = "hashbrown" 342 | version = "0.16.1" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 345 | 346 | [[package]] 347 | name = "heck" 348 | version = "0.5.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 351 | 352 | [[package]] 353 | name = "http" 354 | version = "1.4.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 357 | dependencies = [ 358 | "bytes", 359 | "itoa", 360 | ] 361 | 362 | [[package]] 363 | name = "http-body" 364 | version = "1.0.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 367 | dependencies = [ 368 | "bytes", 369 | "http", 370 | ] 371 | 372 | [[package]] 373 | name = "http-body-util" 374 | version = "0.1.3" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 377 | dependencies = [ 378 | "bytes", 379 | "futures-core", 380 | "http", 381 | "http-body", 382 | "pin-project-lite", 383 | ] 384 | 385 | [[package]] 386 | name = "httparse" 387 | version = "1.10.1" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 390 | 391 | [[package]] 392 | name = "hyper" 393 | version = "1.8.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 396 | dependencies = [ 397 | "atomic-waker", 398 | "bytes", 399 | "futures-channel", 400 | "futures-core", 401 | "http", 402 | "http-body", 403 | "httparse", 404 | "itoa", 405 | "pin-project-lite", 406 | "pin-utils", 407 | "smallvec", 408 | "tokio", 409 | "want", 410 | ] 411 | 412 | [[package]] 413 | name = "hyper-rustls" 414 | version = "0.27.7" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 417 | dependencies = [ 418 | "http", 419 | "hyper", 420 | "hyper-util", 421 | "rustls", 422 | "rustls-pki-types", 423 | "tokio", 424 | "tokio-rustls", 425 | "tower-service", 426 | "webpki-roots", 427 | ] 428 | 429 | [[package]] 430 | name = "hyper-util" 431 | version = "0.1.18" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" 434 | dependencies = [ 435 | "base64", 436 | "bytes", 437 | "futures-channel", 438 | "futures-core", 439 | "futures-util", 440 | "http", 441 | "http-body", 442 | "hyper", 443 | "ipnet", 444 | "libc", 445 | "percent-encoding", 446 | "pin-project-lite", 447 | "socket2", 448 | "tokio", 449 | "tower-service", 450 | "tracing", 451 | ] 452 | 453 | [[package]] 454 | name = "icu_collections" 455 | version = "2.1.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 458 | dependencies = [ 459 | "displaydoc", 460 | "potential_utf", 461 | "yoke", 462 | "zerofrom", 463 | "zerovec", 464 | ] 465 | 466 | [[package]] 467 | name = "icu_locale_core" 468 | version = "2.1.1" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 471 | dependencies = [ 472 | "displaydoc", 473 | "litemap", 474 | "tinystr", 475 | "writeable", 476 | "zerovec", 477 | ] 478 | 479 | [[package]] 480 | name = "icu_normalizer" 481 | version = "2.1.1" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 484 | dependencies = [ 485 | "icu_collections", 486 | "icu_normalizer_data", 487 | "icu_properties", 488 | "icu_provider", 489 | "smallvec", 490 | "zerovec", 491 | ] 492 | 493 | [[package]] 494 | name = "icu_normalizer_data" 495 | version = "2.1.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 498 | 499 | [[package]] 500 | name = "icu_properties" 501 | version = "2.1.1" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" 504 | dependencies = [ 505 | "icu_collections", 506 | "icu_locale_core", 507 | "icu_properties_data", 508 | "icu_provider", 509 | "zerotrie", 510 | "zerovec", 511 | ] 512 | 513 | [[package]] 514 | name = "icu_properties_data" 515 | version = "2.1.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" 518 | 519 | [[package]] 520 | name = "icu_provider" 521 | version = "2.1.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 524 | dependencies = [ 525 | "displaydoc", 526 | "icu_locale_core", 527 | "writeable", 528 | "yoke", 529 | "zerofrom", 530 | "zerotrie", 531 | "zerovec", 532 | ] 533 | 534 | [[package]] 535 | name = "idna" 536 | version = "1.1.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 539 | dependencies = [ 540 | "idna_adapter", 541 | "smallvec", 542 | "utf8_iter", 543 | ] 544 | 545 | [[package]] 546 | name = "idna_adapter" 547 | version = "1.2.1" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 550 | dependencies = [ 551 | "icu_normalizer", 552 | "icu_properties", 553 | ] 554 | 555 | [[package]] 556 | name = "indexmap" 557 | version = "2.12.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 560 | dependencies = [ 561 | "equivalent", 562 | "hashbrown", 563 | ] 564 | 565 | [[package]] 566 | name = "ipnet" 567 | version = "2.11.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 570 | 571 | [[package]] 572 | name = "iri-string" 573 | version = "0.7.9" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" 576 | dependencies = [ 577 | "memchr", 578 | "serde", 579 | ] 580 | 581 | [[package]] 582 | name = "is_terminal_polyfill" 583 | version = "1.70.2" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 586 | 587 | [[package]] 588 | name = "itoa" 589 | version = "1.0.15" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 592 | 593 | [[package]] 594 | name = "jiff" 595 | version = "0.2.16" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" 598 | dependencies = [ 599 | "jiff-static", 600 | "log", 601 | "portable-atomic", 602 | "portable-atomic-util", 603 | "serde_core", 604 | ] 605 | 606 | [[package]] 607 | name = "jiff-static" 608 | version = "0.2.16" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" 611 | dependencies = [ 612 | "proc-macro2", 613 | "quote", 614 | "syn", 615 | ] 616 | 617 | [[package]] 618 | name = "js-sys" 619 | version = "0.3.83" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" 622 | dependencies = [ 623 | "once_cell", 624 | "wasm-bindgen", 625 | ] 626 | 627 | [[package]] 628 | name = "libc" 629 | version = "0.2.177" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 632 | 633 | [[package]] 634 | name = "litemap" 635 | version = "0.8.1" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 638 | 639 | [[package]] 640 | name = "log" 641 | version = "0.4.29" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 644 | 645 | [[package]] 646 | name = "lru-slab" 647 | version = "0.1.2" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 650 | 651 | [[package]] 652 | name = "memchr" 653 | version = "2.7.6" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 656 | 657 | [[package]] 658 | name = "mio" 659 | version = "1.1.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 662 | dependencies = [ 663 | "libc", 664 | "wasi", 665 | "windows-sys 0.61.2", 666 | ] 667 | 668 | [[package]] 669 | name = "once_cell" 670 | version = "1.21.3" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 673 | 674 | [[package]] 675 | name = "once_cell_polyfill" 676 | version = "1.70.2" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 679 | 680 | [[package]] 681 | name = "percent-encoding" 682 | version = "2.3.2" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 685 | 686 | [[package]] 687 | name = "pin-project-lite" 688 | version = "0.2.16" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 691 | 692 | [[package]] 693 | name = "pin-utils" 694 | version = "0.1.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 697 | 698 | [[package]] 699 | name = "portable-atomic" 700 | version = "1.11.1" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 703 | 704 | [[package]] 705 | name = "portable-atomic-util" 706 | version = "0.2.4" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 709 | dependencies = [ 710 | "portable-atomic", 711 | ] 712 | 713 | [[package]] 714 | name = "potential_utf" 715 | version = "0.1.4" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 718 | dependencies = [ 719 | "zerovec", 720 | ] 721 | 722 | [[package]] 723 | name = "ppv-lite86" 724 | version = "0.2.21" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 727 | dependencies = [ 728 | "zerocopy", 729 | ] 730 | 731 | [[package]] 732 | name = "proc-macro2" 733 | version = "1.0.103" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 736 | dependencies = [ 737 | "unicode-ident", 738 | ] 739 | 740 | [[package]] 741 | name = "quinn" 742 | version = "0.11.9" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 745 | dependencies = [ 746 | "bytes", 747 | "cfg_aliases", 748 | "pin-project-lite", 749 | "quinn-proto", 750 | "quinn-udp", 751 | "rustc-hash", 752 | "rustls", 753 | "socket2", 754 | "thiserror", 755 | "tokio", 756 | "tracing", 757 | "web-time", 758 | ] 759 | 760 | [[package]] 761 | name = "quinn-proto" 762 | version = "0.11.13" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 765 | dependencies = [ 766 | "bytes", 767 | "getrandom 0.3.4", 768 | "lru-slab", 769 | "rand", 770 | "ring", 771 | "rustc-hash", 772 | "rustls", 773 | "rustls-pki-types", 774 | "slab", 775 | "thiserror", 776 | "tinyvec", 777 | "tracing", 778 | "web-time", 779 | ] 780 | 781 | [[package]] 782 | name = "quinn-udp" 783 | version = "0.5.14" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 786 | dependencies = [ 787 | "cfg_aliases", 788 | "libc", 789 | "once_cell", 790 | "socket2", 791 | "tracing", 792 | "windows-sys 0.60.2", 793 | ] 794 | 795 | [[package]] 796 | name = "quote" 797 | version = "1.0.42" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 800 | dependencies = [ 801 | "proc-macro2", 802 | ] 803 | 804 | [[package]] 805 | name = "r-efi" 806 | version = "5.3.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 809 | 810 | [[package]] 811 | name = "rand" 812 | version = "0.9.2" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 815 | dependencies = [ 816 | "rand_chacha", 817 | "rand_core", 818 | ] 819 | 820 | [[package]] 821 | name = "rand_chacha" 822 | version = "0.9.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 825 | dependencies = [ 826 | "ppv-lite86", 827 | "rand_core", 828 | ] 829 | 830 | [[package]] 831 | name = "rand_core" 832 | version = "0.9.3" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 835 | dependencies = [ 836 | "getrandom 0.3.4", 837 | ] 838 | 839 | [[package]] 840 | name = "regex" 841 | version = "1.12.2" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 844 | dependencies = [ 845 | "aho-corasick", 846 | "memchr", 847 | "regex-automata", 848 | "regex-syntax", 849 | ] 850 | 851 | [[package]] 852 | name = "regex-automata" 853 | version = "0.4.13" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 856 | dependencies = [ 857 | "aho-corasick", 858 | "memchr", 859 | "regex-syntax", 860 | ] 861 | 862 | [[package]] 863 | name = "regex-syntax" 864 | version = "0.8.8" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 867 | 868 | [[package]] 869 | name = "reqwest" 870 | version = "0.12.25" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" 873 | dependencies = [ 874 | "base64", 875 | "bytes", 876 | "futures-channel", 877 | "futures-core", 878 | "futures-util", 879 | "http", 880 | "http-body", 881 | "http-body-util", 882 | "hyper", 883 | "hyper-rustls", 884 | "hyper-util", 885 | "js-sys", 886 | "log", 887 | "percent-encoding", 888 | "pin-project-lite", 889 | "quinn", 890 | "rustls", 891 | "rustls-pki-types", 892 | "serde", 893 | "serde_json", 894 | "serde_urlencoded", 895 | "sync_wrapper", 896 | "tokio", 897 | "tokio-rustls", 898 | "tower", 899 | "tower-http", 900 | "tower-service", 901 | "url", 902 | "wasm-bindgen", 903 | "wasm-bindgen-futures", 904 | "web-sys", 905 | "webpki-roots", 906 | ] 907 | 908 | [[package]] 909 | name = "ring" 910 | version = "0.17.14" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 913 | dependencies = [ 914 | "cc", 915 | "cfg-if", 916 | "getrandom 0.2.16", 917 | "libc", 918 | "untrusted", 919 | "windows-sys 0.52.0", 920 | ] 921 | 922 | [[package]] 923 | name = "rustc-hash" 924 | version = "2.1.1" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 927 | 928 | [[package]] 929 | name = "rustls" 930 | version = "0.23.35" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 933 | dependencies = [ 934 | "once_cell", 935 | "ring", 936 | "rustls-pki-types", 937 | "rustls-webpki", 938 | "subtle", 939 | "zeroize", 940 | ] 941 | 942 | [[package]] 943 | name = "rustls-pki-types" 944 | version = "1.13.1" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" 947 | dependencies = [ 948 | "web-time", 949 | "zeroize", 950 | ] 951 | 952 | [[package]] 953 | name = "rustls-webpki" 954 | version = "0.103.8" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" 957 | dependencies = [ 958 | "ring", 959 | "rustls-pki-types", 960 | "untrusted", 961 | ] 962 | 963 | [[package]] 964 | name = "rustversion" 965 | version = "1.0.22" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 968 | 969 | [[package]] 970 | name = "ryu" 971 | version = "1.0.20" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 974 | 975 | [[package]] 976 | name = "serde" 977 | version = "1.0.228" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 980 | dependencies = [ 981 | "serde_core", 982 | "serde_derive", 983 | ] 984 | 985 | [[package]] 986 | name = "serde_core" 987 | version = "1.0.228" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 990 | dependencies = [ 991 | "serde_derive", 992 | ] 993 | 994 | [[package]] 995 | name = "serde_derive" 996 | version = "1.0.228" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 999 | dependencies = [ 1000 | "proc-macro2", 1001 | "quote", 1002 | "syn", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "serde_json" 1007 | version = "1.0.145" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 1010 | dependencies = [ 1011 | "indexmap", 1012 | "itoa", 1013 | "memchr", 1014 | "ryu", 1015 | "serde", 1016 | "serde_core", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "serde_urlencoded" 1021 | version = "0.7.1" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1024 | dependencies = [ 1025 | "form_urlencoded", 1026 | "itoa", 1027 | "ryu", 1028 | "serde", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "shlex" 1033 | version = "1.3.0" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1036 | 1037 | [[package]] 1038 | name = "slab" 1039 | version = "0.4.11" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1042 | 1043 | [[package]] 1044 | name = "smallvec" 1045 | version = "1.15.1" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1048 | 1049 | [[package]] 1050 | name = "socket2" 1051 | version = "0.6.1" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 1054 | dependencies = [ 1055 | "libc", 1056 | "windows-sys 0.60.2", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "stable_deref_trait" 1061 | version = "1.2.1" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1064 | 1065 | [[package]] 1066 | name = "strsim" 1067 | version = "0.11.1" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1070 | 1071 | [[package]] 1072 | name = "subtle" 1073 | version = "2.6.1" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1076 | 1077 | [[package]] 1078 | name = "syn" 1079 | version = "2.0.111" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 1082 | dependencies = [ 1083 | "proc-macro2", 1084 | "quote", 1085 | "unicode-ident", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "sync_wrapper" 1090 | version = "1.0.2" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1093 | dependencies = [ 1094 | "futures-core", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "synstructure" 1099 | version = "0.13.2" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1102 | dependencies = [ 1103 | "proc-macro2", 1104 | "quote", 1105 | "syn", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "thiserror" 1110 | version = "2.0.17" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 1113 | dependencies = [ 1114 | "thiserror-impl", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "thiserror-impl" 1119 | version = "2.0.17" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 1122 | dependencies = [ 1123 | "proc-macro2", 1124 | "quote", 1125 | "syn", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "tinystr" 1130 | version = "0.8.2" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 1133 | dependencies = [ 1134 | "displaydoc", 1135 | "zerovec", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "tinyvec" 1140 | version = "1.10.0" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 1143 | dependencies = [ 1144 | "tinyvec_macros", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "tinyvec_macros" 1149 | version = "0.1.1" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1152 | 1153 | [[package]] 1154 | name = "tokio" 1155 | version = "1.48.0" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 1158 | dependencies = [ 1159 | "bytes", 1160 | "libc", 1161 | "mio", 1162 | "pin-project-lite", 1163 | "socket2", 1164 | "windows-sys 0.61.2", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "tokio-rustls" 1169 | version = "0.26.4" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 1172 | dependencies = [ 1173 | "rustls", 1174 | "tokio", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "tower" 1179 | version = "0.5.2" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1182 | dependencies = [ 1183 | "futures-core", 1184 | "futures-util", 1185 | "pin-project-lite", 1186 | "sync_wrapper", 1187 | "tokio", 1188 | "tower-layer", 1189 | "tower-service", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "tower-http" 1194 | version = "0.6.8" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 1197 | dependencies = [ 1198 | "bitflags", 1199 | "bytes", 1200 | "futures-util", 1201 | "http", 1202 | "http-body", 1203 | "iri-string", 1204 | "pin-project-lite", 1205 | "tower", 1206 | "tower-layer", 1207 | "tower-service", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "tower-layer" 1212 | version = "0.3.3" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1215 | 1216 | [[package]] 1217 | name = "tower-service" 1218 | version = "0.3.3" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1221 | 1222 | [[package]] 1223 | name = "tracing" 1224 | version = "0.1.43" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" 1227 | dependencies = [ 1228 | "pin-project-lite", 1229 | "tracing-core", 1230 | ] 1231 | 1232 | [[package]] 1233 | name = "tracing-core" 1234 | version = "0.1.35" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" 1237 | dependencies = [ 1238 | "once_cell", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "try-lock" 1243 | version = "0.2.5" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1246 | 1247 | [[package]] 1248 | name = "unicode-ident" 1249 | version = "1.0.22" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1252 | 1253 | [[package]] 1254 | name = "untrusted" 1255 | version = "0.9.0" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1258 | 1259 | [[package]] 1260 | name = "url" 1261 | version = "2.5.7" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1264 | dependencies = [ 1265 | "form_urlencoded", 1266 | "idna", 1267 | "percent-encoding", 1268 | "serde", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "utf8_iter" 1273 | version = "1.0.4" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1276 | 1277 | [[package]] 1278 | name = "utf8parse" 1279 | version = "0.2.2" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1282 | 1283 | [[package]] 1284 | name = "want" 1285 | version = "0.3.1" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1288 | dependencies = [ 1289 | "try-lock", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "wasi" 1294 | version = "0.11.1+wasi-snapshot-preview1" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1297 | 1298 | [[package]] 1299 | name = "wasip2" 1300 | version = "1.0.1+wasi-0.2.4" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 1303 | dependencies = [ 1304 | "wit-bindgen", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "wasm-bindgen" 1309 | version = "0.2.106" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 1312 | dependencies = [ 1313 | "cfg-if", 1314 | "once_cell", 1315 | "rustversion", 1316 | "wasm-bindgen-macro", 1317 | "wasm-bindgen-shared", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "wasm-bindgen-futures" 1322 | version = "0.4.56" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" 1325 | dependencies = [ 1326 | "cfg-if", 1327 | "js-sys", 1328 | "once_cell", 1329 | "wasm-bindgen", 1330 | "web-sys", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "wasm-bindgen-macro" 1335 | version = "0.2.106" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 1338 | dependencies = [ 1339 | "quote", 1340 | "wasm-bindgen-macro-support", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "wasm-bindgen-macro-support" 1345 | version = "0.2.106" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 1348 | dependencies = [ 1349 | "bumpalo", 1350 | "proc-macro2", 1351 | "quote", 1352 | "syn", 1353 | "wasm-bindgen-shared", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "wasm-bindgen-shared" 1358 | version = "0.2.106" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 1361 | dependencies = [ 1362 | "unicode-ident", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "web-sys" 1367 | version = "0.3.83" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" 1370 | dependencies = [ 1371 | "js-sys", 1372 | "wasm-bindgen", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "web-time" 1377 | version = "1.1.0" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1380 | dependencies = [ 1381 | "js-sys", 1382 | "wasm-bindgen", 1383 | ] 1384 | 1385 | [[package]] 1386 | name = "webpki-roots" 1387 | version = "1.0.4" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" 1390 | dependencies = [ 1391 | "rustls-pki-types", 1392 | ] 1393 | 1394 | [[package]] 1395 | name = "windows-link" 1396 | version = "0.2.1" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1399 | 1400 | [[package]] 1401 | name = "windows-sys" 1402 | version = "0.52.0" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1405 | dependencies = [ 1406 | "windows-targets 0.52.6", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "windows-sys" 1411 | version = "0.60.2" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1414 | dependencies = [ 1415 | "windows-targets 0.53.5", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "windows-sys" 1420 | version = "0.61.2" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1423 | dependencies = [ 1424 | "windows-link", 1425 | ] 1426 | 1427 | [[package]] 1428 | name = "windows-targets" 1429 | version = "0.52.6" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1432 | dependencies = [ 1433 | "windows_aarch64_gnullvm 0.52.6", 1434 | "windows_aarch64_msvc 0.52.6", 1435 | "windows_i686_gnu 0.52.6", 1436 | "windows_i686_gnullvm 0.52.6", 1437 | "windows_i686_msvc 0.52.6", 1438 | "windows_x86_64_gnu 0.52.6", 1439 | "windows_x86_64_gnullvm 0.52.6", 1440 | "windows_x86_64_msvc 0.52.6", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "windows-targets" 1445 | version = "0.53.5" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1448 | dependencies = [ 1449 | "windows-link", 1450 | "windows_aarch64_gnullvm 0.53.1", 1451 | "windows_aarch64_msvc 0.53.1", 1452 | "windows_i686_gnu 0.53.1", 1453 | "windows_i686_gnullvm 0.53.1", 1454 | "windows_i686_msvc 0.53.1", 1455 | "windows_x86_64_gnu 0.53.1", 1456 | "windows_x86_64_gnullvm 0.53.1", 1457 | "windows_x86_64_msvc 0.53.1", 1458 | ] 1459 | 1460 | [[package]] 1461 | name = "windows_aarch64_gnullvm" 1462 | version = "0.52.6" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1465 | 1466 | [[package]] 1467 | name = "windows_aarch64_gnullvm" 1468 | version = "0.53.1" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1471 | 1472 | [[package]] 1473 | name = "windows_aarch64_msvc" 1474 | version = "0.52.6" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1477 | 1478 | [[package]] 1479 | name = "windows_aarch64_msvc" 1480 | version = "0.53.1" 1481 | source = "registry+https://github.com/rust-lang/crates.io-index" 1482 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1483 | 1484 | [[package]] 1485 | name = "windows_i686_gnu" 1486 | version = "0.52.6" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1489 | 1490 | [[package]] 1491 | name = "windows_i686_gnu" 1492 | version = "0.53.1" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1495 | 1496 | [[package]] 1497 | name = "windows_i686_gnullvm" 1498 | version = "0.52.6" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1501 | 1502 | [[package]] 1503 | name = "windows_i686_gnullvm" 1504 | version = "0.53.1" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1507 | 1508 | [[package]] 1509 | name = "windows_i686_msvc" 1510 | version = "0.52.6" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1513 | 1514 | [[package]] 1515 | name = "windows_i686_msvc" 1516 | version = "0.53.1" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1519 | 1520 | [[package]] 1521 | name = "windows_x86_64_gnu" 1522 | version = "0.52.6" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1525 | 1526 | [[package]] 1527 | name = "windows_x86_64_gnu" 1528 | version = "0.53.1" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1531 | 1532 | [[package]] 1533 | name = "windows_x86_64_gnullvm" 1534 | version = "0.52.6" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1537 | 1538 | [[package]] 1539 | name = "windows_x86_64_gnullvm" 1540 | version = "0.53.1" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1543 | 1544 | [[package]] 1545 | name = "windows_x86_64_msvc" 1546 | version = "0.52.6" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1549 | 1550 | [[package]] 1551 | name = "windows_x86_64_msvc" 1552 | version = "0.53.1" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1555 | 1556 | [[package]] 1557 | name = "wit-bindgen" 1558 | version = "0.46.0" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 1561 | 1562 | [[package]] 1563 | name = "writeable" 1564 | version = "0.6.2" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 1567 | 1568 | [[package]] 1569 | name = "yoke" 1570 | version = "0.8.1" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 1573 | dependencies = [ 1574 | "stable_deref_trait", 1575 | "yoke-derive", 1576 | "zerofrom", 1577 | ] 1578 | 1579 | [[package]] 1580 | name = "yoke-derive" 1581 | version = "0.8.1" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 1584 | dependencies = [ 1585 | "proc-macro2", 1586 | "quote", 1587 | "syn", 1588 | "synstructure", 1589 | ] 1590 | 1591 | [[package]] 1592 | name = "zerocopy" 1593 | version = "0.8.31" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 1596 | dependencies = [ 1597 | "zerocopy-derive", 1598 | ] 1599 | 1600 | [[package]] 1601 | name = "zerocopy-derive" 1602 | version = "0.8.31" 1603 | source = "registry+https://github.com/rust-lang/crates.io-index" 1604 | checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 1605 | dependencies = [ 1606 | "proc-macro2", 1607 | "quote", 1608 | "syn", 1609 | ] 1610 | 1611 | [[package]] 1612 | name = "zerofrom" 1613 | version = "0.1.6" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1616 | dependencies = [ 1617 | "zerofrom-derive", 1618 | ] 1619 | 1620 | [[package]] 1621 | name = "zerofrom-derive" 1622 | version = "0.1.6" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1625 | dependencies = [ 1626 | "proc-macro2", 1627 | "quote", 1628 | "syn", 1629 | "synstructure", 1630 | ] 1631 | 1632 | [[package]] 1633 | name = "zeroize" 1634 | version = "1.8.2" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1637 | 1638 | [[package]] 1639 | name = "zerotrie" 1640 | version = "0.2.3" 1641 | source = "registry+https://github.com/rust-lang/crates.io-index" 1642 | checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 1643 | dependencies = [ 1644 | "displaydoc", 1645 | "yoke", 1646 | "zerofrom", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "zerovec" 1651 | version = "0.11.5" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 1654 | dependencies = [ 1655 | "yoke", 1656 | "zerofrom", 1657 | "zerovec-derive", 1658 | ] 1659 | 1660 | [[package]] 1661 | name = "zerovec-derive" 1662 | version = "0.11.2" 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" 1664 | checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 1665 | dependencies = [ 1666 | "proc-macro2", 1667 | "quote", 1668 | "syn", 1669 | ] 1670 | --------------------------------------------------------------------------------