├── .gitignore
├── tests
└── config
│ ├── stripped-config.php.xml
│ ├── 2021-07-speedtest-config.xml
│ ├── geo-test-servers-static.php.xml
│ ├── stripped-servers-static.php.xml
│ ├── config.php.xml
│ └── 2020-07-speedtest-config.xml
├── src
├── lib.rs
├── log.rs
├── distance.rs
├── speedtest_csv.rs
├── error.rs
├── speedtest_servers_config.rs
├── speedtest_config.rs
├── main.rs
└── speedtest.rs
├── .github
├── dependabot.yml
└── workflows
│ ├── usage_demo.yml
│ ├── ci.yml
│ └── release.yml
├── Cargo.toml
├── LICENSE-MIT
├── README.md
├── CHANGELOG.md
├── LICENSE-APACHE
└── Cargo.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .idea/
3 | .vscode/settings.json
4 |
--------------------------------------------------------------------------------
/tests/config/stripped-config.php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | // This crate really isn't meant to be stable.
2 |
3 | pub mod distance;
4 | pub mod error;
5 | pub mod speedtest;
6 | pub mod speedtest_config;
7 | pub mod speedtest_csv;
8 | pub mod speedtest_servers_config;
9 |
10 | #[cfg(not(feature = "log"))]
11 | mod log;
12 |
--------------------------------------------------------------------------------
/src/log.rs:
--------------------------------------------------------------------------------
1 | macro_rules! info {
2 | // info!(target: "my_target", key1 = 42, key2 = true; "a {} event", "log")
3 | // info!(target: "my_target", "a {} event", "log")
4 | (target: $target:expr, $($arg:tt)+) => {};
5 |
6 | // info!("a {} event", "log")
7 | ($($arg:tt)+) => {};
8 | }
9 |
10 | pub(crate) use info;
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "cargo" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | - package-ecosystem: "github-actions" # See documentation for possible values
13 | directory: "/" # Location of package manifests
14 | schedule:
15 | interval: "weekly"
16 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | authors = ["Nelson Chen "]
3 | description = "Speedtest.net testing utility and crate"
4 | exclude = ["tests/config/*"]
5 | license = "MIT OR Apache-2.0"
6 | name = "speedtest-rs"
7 | repository = "https://github.com/nelsonjchen/speedtest-rs"
8 | version = "0.2.0"
9 | edition = "2021"
10 |
11 | [dependencies]
12 | clap = { version = "4.5.10", features = ["derive"] }
13 | chrono = "0.4.38"
14 | env_logger = "0.11.4"
15 | log = { version = "0.4.22", optional = true }
16 | url = "2.5.2"
17 | mockito = "1.4.0"
18 | md5 = "0.7.0"
19 | csv = "1.3.0"
20 | serde = { version = "1.0.204", features = ["derive"] }
21 | roxmltree = "0.20.0"
22 | rayon = "1.10.0"
23 | iter-read = "1.0.1"
24 |
25 | [dependencies.reqwest]
26 | version = "0.12"
27 | features = ["blocking"]
28 |
29 | [features]
30 | # default = ["log"]
31 | rustls-tls = ["reqwest/rustls-tls"]
32 | log = ["dep:log"]
33 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any
2 | person obtaining a copy of this software and associated
3 | documentation files (the "Software"), to deal in the
4 | Software without restriction, including without
5 | limitation the rights to use, copy, modify, merge,
6 | publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following
9 | conditions:
10 |
11 | The above copyright notice and this permission notice
12 | shall be included in all copies or substantial portions
13 | of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/.github/workflows/usage_demo.yml:
--------------------------------------------------------------------------------
1 | on:
2 | # Trigger the workflow on push or pull request on all branches
3 | push:
4 | pull_request:
5 | # And every day at midnight
6 | schedule:
7 | - cron: '0 0 * * *'
8 |
9 | name: Usage Demo
10 |
11 | jobs:
12 | usage-demo:
13 | name: "Usage Demo"
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - uses: actions-rs/toolchain@v1
20 | with:
21 | toolchain: stable
22 | profile: minimal
23 | override: true
24 |
25 | - uses: Swatinem/rust-cache@v2
26 |
27 | - uses: actions-rs/cargo@v1
28 | name: Build
29 | with:
30 | command: build
31 | args: --features=${{ matrix.features }}
32 |
33 | - uses: actions-rs/cargo@v1
34 | name: Run (Typical)
35 | with:
36 | command: run
37 |
38 | - uses: actions-rs/cargo@v1
39 | name: Run (CSV Output)
40 | with:
41 | command: run
42 | args: -- --csv
43 | - uses: actions-rs/cargo@v1
44 | name: Run (CSV Header)
45 | with:
46 | command: run
47 | args: -- --csv-header
48 |
--------------------------------------------------------------------------------
/src/distance.rs:
--------------------------------------------------------------------------------
1 | use std::f32::consts;
2 |
3 | #[derive(Clone, Debug, PartialEq, Default)]
4 | pub struct EarthLocation {
5 | pub latitude: f32,
6 | pub longitude: f32,
7 | }
8 |
9 | pub fn compute_distance(origin: &EarthLocation, destination: &EarthLocation) -> f32 {
10 | let radius: f32 = 6371.0;
11 | let d_lat = to_radians(origin.latitude - destination.latitude);
12 | let d_long = to_radians(origin.longitude - destination.longitude);
13 | let a = (d_lat / 2.0).sin() * (d_lat / 2.0).sin()
14 | + to_radians(origin.latitude).cos()
15 | * to_radians(destination.latitude).cos()
16 | * (d_long / 2.0).sin()
17 | * (d_long / 2.0).sin();
18 | let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
19 | radius * c
20 | }
21 |
22 | fn to_radians(degree: f32) -> f32 {
23 | let value: f32 = consts::PI;
24 | degree * (value / 180.0f32)
25 | }
26 |
27 | #[cfg(test)]
28 | mod tests {
29 | use super::*;
30 |
31 | #[test]
32 | fn test_distance() {
33 | let origin = EarthLocation {
34 | latitude: 32.9545,
35 | longitude: -117.2333,
36 | };
37 | let destination = EarthLocation {
38 | latitude: 70.0733,
39 | longitude: 29.7497,
40 | };
41 | let distance = compute_distance(&origin, &destination);
42 | let diff = (distance - 8255.1).abs();
43 | println!("distance: {distance} diff: {diff}");
44 | assert!(diff < 0.2);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/config/2021-07-speedtest-config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | f7a45ced624d3a70-1df5b7cd427370f7-b91ee21d6cb22d7b
6 | speedtest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on:
2 | # Trigger the workflow on push or pull request,
3 | # but only for the master branch
4 | push:
5 | pull_request:
6 |
7 | name: Continuous integration
8 |
9 | jobs:
10 | ci:
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | matrix:
14 | rust:
15 | - stable
16 | - beta
17 | - nightly
18 | os:
19 | - ubuntu-latest
20 | - macOS-latest
21 | - windows-latest
22 | features:
23 | - "rustls-tls"
24 | - ""
25 |
26 | steps:
27 | - uses: actions/checkout@v4
28 |
29 | - uses: actions-rs/toolchain@v1
30 | with:
31 | profile: minimal
32 | toolchain: ${{ matrix.rust }}
33 | override: true
34 | components: rustfmt, clippy
35 |
36 | - uses: Swatinem/rust-cache@v2
37 |
38 | - uses: actions-rs/cargo@v1
39 | name: Build
40 | with:
41 | command: build
42 | args: --features=${{ matrix.features }}
43 |
44 | - uses: actions-rs/cargo@v1
45 | name: Unit Test
46 | with:
47 | command: test
48 | args: --features=${{ matrix.features }}
49 |
50 | - uses: actions-rs/cargo@v1
51 | name: Run
52 | env:
53 | RUST_LOG: "debug"
54 | with:
55 | command: run
56 | args: --features=${{ matrix.features }}
57 |
58 | - uses: actions-rs/cargo@v1
59 | name: Format Check
60 | with:
61 | command: fmt
62 | args: --all -- --check
63 |
64 | - uses: actions-rs/cargo@v1
65 | name: Clippy
66 | with:
67 | command: clippy
68 | args: -- -D warnings
69 |
--------------------------------------------------------------------------------
/src/speedtest_csv.rs:
--------------------------------------------------------------------------------
1 | use serde::Serialize;
2 |
3 | #[derive(Debug, Serialize, Default)]
4 | pub struct SpeedTestCsvResult<'a> {
5 | #[serde(rename = "Server ID")]
6 | pub server_id: &'a str,
7 | #[serde(rename = "Sponsor")]
8 | pub sponsor: &'a str,
9 | #[serde(rename = "Server Name")]
10 | pub server_name: &'a str,
11 | #[serde(rename = "Timestamp")]
12 | pub timestamp: &'a str,
13 | #[serde(rename = "Distance")]
14 | pub distance: &'a str,
15 | #[serde(rename = "Ping")]
16 | pub ping: &'a str,
17 | #[serde(rename = "Download")]
18 | pub download: &'a str,
19 | #[serde(rename = "Upload")]
20 | pub upload: &'a str,
21 | #[serde(rename = "Share")]
22 | pub share: &'a str,
23 | #[serde(rename = "IP Address")]
24 | pub ip_address: &'a str,
25 | }
26 |
27 | impl<'a> SpeedTestCsvResult<'a> {
28 | pub fn header_serialize(self) -> String {
29 | // Un-dynamic for now
30 | // Blocked on:
31 | // * https://github.com/BurntSushi/rust-csv/issues/161 being implemented or solved
32 | // * https://github.com/BurntSushi/rust-csv/pull/193/files, like in this?
33 | "Server ID,Sponsor,Server Name,Timestamp,Distance,Ping,Download,Upload,Share,IP Address"
34 | .to_string()
35 | }
36 | }
37 |
38 | #[cfg(test)]
39 | mod tests {
40 | use super::*;
41 | use std::error::Error;
42 |
43 | #[test]
44 | fn test_header_serialize() -> Result<(), Box> {
45 | let original = "Server ID,Sponsor,Server Name,Timestamp,Distance,Ping,Download,Upload,Share,IP Address";
46 |
47 | let results = SpeedTestCsvResult::default();
48 |
49 | assert_eq!(results.header_serialize(), original);
50 | Ok(())
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 |
3 | #[derive(Debug)]
4 | pub enum SpeedTestError {
5 | Reqwest(reqwest::Error),
6 | Io(::std::io::Error),
7 | Csv(csv::Error),
8 | ParseFloatError(std::num::ParseFloatError),
9 | ParseIntError(std::num::ParseIntError),
10 | AddrParseError(std::net::AddrParseError),
11 | RoXmlTreeError(roxmltree::Error),
12 | ConfigParseError,
13 | ServerParseError,
14 | LatencyTestInvalidPath,
15 | LatencyTestNoServerError,
16 | LatencyTestClosestError,
17 | UrlParseError(url::ParseError),
18 | SystemTimeError(std::time::SystemTimeError),
19 | ParseShareUrlError,
20 | ThreadPoolBuildError(rayon::ThreadPoolBuildError),
21 | }
22 |
23 | impl From for SpeedTestError {
24 | fn from(err: reqwest::Error) -> SpeedTestError {
25 | SpeedTestError::Reqwest(err)
26 | }
27 | }
28 |
29 | impl From<::std::io::Error> for SpeedTestError {
30 | fn from(err: ::std::io::Error) -> SpeedTestError {
31 | SpeedTestError::Io(err)
32 | }
33 | }
34 |
35 | impl From for SpeedTestError {
36 | fn from(err: csv::Error) -> SpeedTestError {
37 | SpeedTestError::Csv(err)
38 | }
39 | }
40 |
41 | impl From for SpeedTestError {
42 | fn from(err: std::num::ParseFloatError) -> SpeedTestError {
43 | SpeedTestError::ParseFloatError(err)
44 | }
45 | }
46 |
47 | impl From for SpeedTestError {
48 | fn from(err: std::num::ParseIntError) -> SpeedTestError {
49 | SpeedTestError::ParseIntError(err)
50 | }
51 | }
52 |
53 | impl From for SpeedTestError {
54 | fn from(err: std::net::AddrParseError) -> SpeedTestError {
55 | SpeedTestError::AddrParseError(err)
56 | }
57 | }
58 |
59 | impl From for SpeedTestError {
60 | fn from(err: roxmltree::Error) -> SpeedTestError {
61 | SpeedTestError::RoXmlTreeError(err)
62 | }
63 | }
64 |
65 | impl From for SpeedTestError {
66 | fn from(err: url::ParseError) -> SpeedTestError {
67 | SpeedTestError::UrlParseError(err)
68 | }
69 | }
70 |
71 | impl From for SpeedTestError {
72 | fn from(err: std::time::SystemTimeError) -> SpeedTestError {
73 | SpeedTestError::SystemTimeError(err)
74 | }
75 | }
76 |
77 | impl From for SpeedTestError {
78 | fn from(err: rayon::ThreadPoolBuildError) -> SpeedTestError {
79 | SpeedTestError::ThreadPoolBuildError(err)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/config/geo-test-servers-static.php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/config/stripped-servers-static.php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Maintenance Message
2 |
3 | I’m sorry for the pun, but I no longer have the bandwidth to maintain or develop this project. I also don’t have the bandwidth to search for or evaluate new maintainers—and, frankly, I’m not terribly interested in doing so after hearing about stuff like xz. This project isn't "xz", but I don't want to make a bad choice.
4 |
5 | When I originally built this project, the goal was to port speedtest-cli (a Python tool) to Rust for use on an ARM9/ARM926EJ-S receipt printer. Ironically, although I now work for an ISP, my interest in further developing or supporting this project hasn’t increased. This tool technically violates Ookla’s current Terms of Service, and Ookla now provides its own binaries for speed tests. Besides, I’m also not on the ISP team that handles speed testing and/or uses iperf.
6 |
7 | As of now, this project is officially mothballed. I will not be accepting pull requests for code changes or updating the crate, at least for the foreseeable future. However, I will accept pull requests related to the list of alternative projects below. You’re welcome to fork this project and give it a new name, which I’d be happy to add to this list.
8 |
9 | Alternatives (ordered by GitHub stars at the time of PR):
10 |
11 | * to be filled
12 |
13 | # speedtest-rs
14 |
15 | *a tool like `speedtest-cli`, but in Rust*
16 |
17 | 
18 | [](https://crates.io/crates/speedtest-rs)
19 |
20 | Status: This is usable for lower-end residential connections using ["HTTP Legacy Fallback"][http_legacy_fallback]
21 |
22 | ## Install from AUR
23 |
24 | ```sh
25 | paru -S speedtest-rs
26 | ```
27 |
28 | or
29 |
30 | ```sh
31 | paru -S speedtest-rs-bin
32 | ```
33 |
34 | ## [HTTP Legacy Fallback][http_legacy_fallback]
35 |
36 | This tool currently only supports [HTTP Legacy Fallback][http_legacy_fallback] for testing.
37 |
38 | High bandwidth connections higher than ~200Mbps may return incorrect results!
39 |
40 | The testing operations are different from socket versions of tools connecting to speedtest.net infrastructure. In the many FOSS Go versions, tests are done to find an amount of data that can run for a default of 3 seconds over some TCP connection. In particular, `speedtest-cli` and `speedtest-rs` tests with what Ookla calls the ["HTTP Legacy Fallback"][http_legacy_fallback] for hosts that cannot establish a direct TCP connection.
41 |
42 | ### Ookla speedtest now has their own non-FOSS CLI tool that's native and available for many platforms.
43 |
44 | * TCP-based
45 | * Higher Bandwidth capable.
46 |
47 | https://www.speedtest.net/apps/cli
48 |
49 | Please look here. Unfortunately, it is not FOSS. Still, it is supported by them and can be used for non-commercial purposes.
50 |
51 | ## Purpose
52 |
53 | This is a learning exercise for me to learn Rust and keeping up with its ecosystem.
54 |
55 | The [HTTP Legacy Fallback][http_legacy_fallback] is currently based on the popular Python implementation:
56 |
57 | https://github.com/sivel/speedtest-cli @ 2.1.2
58 |
59 | There are also other speedtest.net using tools using different approaches to be stolen from in the future. For example:
60 |
61 | https://github.com/traetox/speedtest
62 |
63 | This example seems different as it appears to just use TCP connections and some protocol. It's probably more suitable to high-speed connections. TODO: Add a default TCP-mode.
64 |
65 | ## Use as a Library
66 |
67 | The API is very much not stable. Use at your own risk. Semver adherence definitely not guaranteed. Please lock to exact versions if you must.
68 |
69 | ## License
70 |
71 | Licensed under either of
72 |
73 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
74 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
75 |
76 | at your option.
77 |
78 | ### Contribution
79 |
80 | Unless you explicitly state otherwise, any contribution intentionally submitted
81 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
82 | additional terms or conditions.
83 |
84 | [http_legacy_fallback]: https://web.archive.org/web/20161109011118/http://www.ookla.com/support/a84541858
85 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # From https://github.com/paskausks/rust-bin-github-workflows/blob/master/.github/workflows/release.yml
2 | # With some cribbing from GitHub readmes
3 |
4 | on:
5 | push:
6 | # Sequence of patterns matched against refs/tags
7 | tags:
8 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
9 |
10 | name: Create Release
11 |
12 | env:
13 | # Could, potentially automatically parse
14 | # the bin name, but let's do it automatically for now.
15 | RELEASE_BIN: speedtest-rs
16 |
17 | # Space separated paths to include in the archive.
18 | # Start relative paths with a dot if you don't want
19 | # paths to be preserved. Use "/" as a delimiter.
20 | RELEASE_ADDS: README.md LICENSE-APACHE LICENSE-MIT
21 | jobs:
22 | build:
23 | name: Build Release
24 |
25 | runs-on: ${{ matrix.os }}
26 | strategy:
27 | matrix:
28 | build: [linux, macos, windows]
29 | include:
30 | - build: linux
31 | os: ubuntu-latest
32 | rust: stable
33 | - build: macos
34 | os: macos-latest
35 | rust: stable
36 | - build: windows
37 | os: windows-latest
38 | rust: stable
39 |
40 | steps:
41 | - uses: actions/checkout@v4
42 |
43 | - uses: actions-rs/toolchain@v1
44 | with:
45 | profile: minimal
46 | toolchain: ${{ matrix.rust }}
47 | override: true
48 |
49 | - name: Build
50 | run: cargo build --verbose --release
51 |
52 | - name: Create artifact directory
53 | run: mkdir artifacts
54 |
55 | - name: Create archive for Linux
56 | run: 7z a -ttar -so -an ./target/release/${{ env.RELEASE_BIN }} ${{ env.RELEASE_ADDS }} | 7z a -si ./artifacts/${{ env.RELEASE_BIN }}-linux-x86_64.tar.gz
57 | if: matrix.os == 'ubuntu-latest'
58 |
59 | - name: Create archive for Windows
60 | run: 7z a -tzip ./artifacts/${{ env.RELEASE_BIN }}-windows-x86_64.zip ./target/release/${{ env.RELEASE_BIN }}.exe ${{ env.RELEASE_ADDS }}
61 | if: matrix.os == 'windows-latest'
62 |
63 | - name: Install p7zip
64 | # 7Zip not available on MacOS, install p7zip via homebrew.
65 | run: brew install p7zip
66 | if: matrix.os == 'macos-latest'
67 |
68 | - name: Create archive for MacOS
69 | run: 7z a -tzip ./artifacts/${{ env.RELEASE_BIN }}-mac-x86_64.zip ./target/release/${{ env.RELEASE_BIN }} ${{ env.RELEASE_ADDS }}
70 | if: matrix.os == 'macos-latest'
71 |
72 | # This will double-zip
73 | # See - https://github.com/actions/upload-artifact/issues/39
74 | - uses: actions/upload-artifact@v4
75 | name: Upload archive
76 | with:
77 | name: ${{ runner.os }}
78 | path: artifacts/
79 |
80 | release:
81 | name: Create Release
82 | needs: build
83 | runs-on: ubuntu-latest
84 |
85 | steps:
86 |
87 | - uses: actions/download-artifact@v4
88 | with:
89 | name: Linux
90 | - uses: actions/download-artifact@v4
91 | with:
92 | name: macOS
93 | - uses: actions/download-artifact@v4
94 | with:
95 | name: Windows
96 |
97 | - name: Create Release
98 | id: create_release
99 | uses: actions/create-release@v1
100 | env:
101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
102 | with:
103 | tag_name: ${{ github.ref }}
104 | release_name: Release ${{ github.ref }}
105 | draft: false
106 | prerelease: false
107 |
108 | - name: Upload Release Asset (Linux)
109 | uses: actions/upload-release-asset@v1
110 | env:
111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
112 | with:
113 | upload_url: ${{ steps.create_release.outputs.upload_url }}
114 | asset_path: ${{ env.RELEASE_BIN }}-linux-x86_64.tar.gz
115 | asset_name: ${{ env.RELEASE_BIN }}-linux-x86_64.tar.gz
116 | asset_content_type: application/zip
117 |
118 | - name: Upload Release Asset (Windows)
119 | uses: actions/upload-release-asset@v1
120 | env:
121 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
122 | with:
123 | upload_url: ${{ steps.create_release.outputs.upload_url }}
124 | asset_path: ${{ env.RELEASE_BIN }}-windows-x86_64.zip
125 | asset_name: ${{ env.RELEASE_BIN }}-windows-x86_64.zip
126 | asset_content_type: application/zip
127 |
128 | - name: Upload Release Asset (Mac)
129 | uses: actions/upload-release-asset@v1
130 | env:
131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
132 | with:
133 | upload_url: ${{ steps.create_release.outputs.upload_url }}
134 | asset_path: ${{ env.RELEASE_BIN }}-mac-x86_64.zip
135 | asset_name: ${{ env.RELEASE_BIN }}-mac-x86_64.zip
136 | asset_content_type: application/zip
137 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 | ## [0.2.0] - 2024-07-27
8 |
9 | ### Changed
10 |
11 | - [Update dependencies in general by @dilawar](https://github.com/nelsonjchen/speedtest-rs/pull/165)
12 |
13 | ## [0.1.5] - 2024-02-11
14 |
15 | ### Added
16 |
17 | - [feat: hide log behind a feature by @radiohertz](https://github.com/nelsonjchen/speedtest-rs/pull/144)
18 |
19 | ### Changed
20 |
21 | - Update dependencies in general
22 |
23 |
24 | ## [0.1.4] - 2023-02-25
25 |
26 | ### Changed
27 |
28 | - Update dependencies in general
29 |
30 | ## [0.1.3] - 2021-07-24
31 | ### Fixed
32 | - [Don't log finish parsed configuration if it fails](https://github.com/nelsonjchen/speedtest-rs/pull/84)
33 |
34 | ## [0.1.2] - 2021-04-14
35 | ### Fixed
36 | - [Check whether ignore_server str is empty.](https://github.com/nelsonjchen/speedtest-rs/pull/78) Thanks [@pcmid](https://github.com/pcmid)!
37 |
38 | ## [0.1.1] - 2020-07-26
39 | ### Added
40 | - Add a plain `lib.rs` with no guarantees on stability
41 |
42 | ## [0.1.0] - 2020-07-23
43 | ### Changed
44 | - [Major reimplementation of download and upload test implementation to be more accurate to speedtest-cli.](https://github.com/nelsonjchen/speedtest-rs/pull/74)
45 | That said, speedtest-cli isn't too accurate on high bandwidth connections.
46 | A future alternate or default accurate implementation will need to be like socket based speedtest tools.
47 | - Replaced xml-rs implementation with roxmltree implementation.
48 |
49 | ### Added
50 | - Upload tests no longer pre-allocate the test upload content. It is now iterator based and generates the uploaded bytes on the fly.
51 | Speedtest-cli takes ~300-400MB of RAM to preallocate upload request data on my connection to do its upload tests.
52 | Speedtest-rs now takes ~8MB.
53 |
54 | ## [0.0.15] - 2020-07-11
55 | ### Added
56 | - [Mini server support with `--mini`](https://github.com/nelsonjchen/speedtest-rs/pull/72)
57 |
58 | ## [0.0.14] - 2020-03-05
59 | ### Added
60 | - CSV output support in the form of `--csv` and `--csv-header`
61 |
62 | ## [0.0.13] - 2020-02-09
63 | ### Changed
64 | - Swapped out MD5 crate to simpler version
65 | - Replaced Error Chain with plain old error enums.
66 |
67 | ### Added
68 | - `rustls-tls` feature to use `rustls-tls` in reqwest.
69 |
70 | ## [0.0.12] - 2019-10-13
71 | ### Fixed
72 | - [Skip servers if the latency test failed.](https://github.com/nelsonjchen/speedtest-rs/pull/22)
73 | ### Changed
74 | - Update dependencies
75 |
76 | ## [0.0.11] - 2019-02-04
77 | ### Changed
78 | - Update dependencies and followed the API changes
79 | - Updated to Rust 2018
80 |
81 | ## [0.0.10] - 2017-12-03
82 | ### Changed
83 | - Update infrastructure and ensure things still build on beta and nightly as of
84 | release.
85 | - Lay out initial foundation for a "error-chain" approach instead of unwraps
86 | everywhere. This may be replaced later with the "failure" crate. WIP.
87 | - Update some internal formatting to modern Rust. WIP.
88 |
89 | ## [0.0.9] - 2016-12-22
90 | ### Changed
91 | - Swap out usage of hyper directly with reqwest.
92 |
93 | - `speedtest-rs` now uses the platform's native TLS implementation. Compile
94 | issues on Windows or Mac due to OpenSSL issues or sheanigans are no
95 | longer an issue.
96 |
97 | ## [0.0.8] - 2016-08-14
98 |
99 | ### Changed
100 |
101 | - Updated dependencies. In particular, updated to the new `url` crate API.
102 |
103 | ## [0.0.7] - 2016-01-27
104 |
105 | ### Changed
106 |
107 | - Update progress bar behavior to be more like `speedtest-cli` by displaying
108 | attempts to upload a file piece instead of completion.
109 |
110 | ## [0.0.6] - 2016-01-25
111 |
112 | ### Changed
113 |
114 | - Correct issue with confusion on maths used to calculate bits and bytes. I
115 | should probably code when I'm awake and not when I'm tired, exhausted, and
116 | delirious. Fix was put in while I'm delirious so who knows if this works!
117 | - Fixed issue where not using `--bytes` results in "Mbytes/s" output even
118 | though output is "Mbit/s".
119 |
120 | ## [0.0.5] - 2016-01-15
121 |
122 | ### Changed
123 |
124 | - Also applied omitted 10 second test run limit to download.
125 |
126 | ## [0.0.4] - 2015-12-24
127 |
128 | ### Added
129 |
130 | - Added `--share` to generate and provide an URL to the speedtest.net share
131 | results image.
132 |
133 | ## [0.0.3] - 2015-12-23
134 |
135 | ### Changed
136 |
137 | - Server list URL changed to non-static version. The static version appears to
138 | have been taken down for good this time.
139 |
140 |
141 | ## [0.0.2] - 2015-12-04
142 |
143 | ### Added
144 |
145 | - Add `--simple` flag which prints out results but not progress bars simular to
146 | `speedtest-cli`.
147 | - Generate User Agent string from crate version
148 |
149 | ### Changed
150 | - Made latency test determination a lot more like `speedtest-cli`'s weird
151 | metric for "averaging". Not sure if fix but they say it was intentional.
152 |
153 |
154 | ## [0.0.1] - 2015-11-18
155 |
156 | ### Added
157 |
158 | - Progress indicators and "TUI" like `speedtest-cli`
159 | - Test download speed like `speedtest-cli`
160 | - Test upload speed like `speedtest-cli`
161 | - Option to display values in bytes instead.... also like `speedtest-cli`.
162 |
--------------------------------------------------------------------------------
/src/speedtest_servers_config.rs:
--------------------------------------------------------------------------------
1 | use crate::distance::{self, EarthLocation};
2 | use crate::{error::SpeedTestError, speedtest::SpeedTestServer, speedtest_config::SpeedTestConfig};
3 | use std::cmp::Ordering::Less;
4 |
5 | pub struct SpeedTestServersConfig {
6 | pub servers: Vec,
7 | }
8 |
9 | impl SpeedTestServersConfig {
10 | pub fn parse_with_config(
11 | server_config_xml: &str,
12 | config: &SpeedTestConfig,
13 | ) -> Result {
14 | let document = roxmltree::Document::parse(server_config_xml)?;
15 | let servers = document
16 | .descendants()
17 | .filter(|node| node.tag_name().name() == "server")
18 | .map::, _>(|n| {
19 | let location = EarthLocation {
20 | latitude: n
21 | .attribute("lat")
22 | .ok_or(SpeedTestError::ServerParseError)?
23 | .parse()?,
24 | longitude: n
25 | .attribute("lon")
26 | .ok_or(SpeedTestError::ServerParseError)?
27 | .parse()?,
28 | };
29 | Ok(SpeedTestServer {
30 | country: n
31 | .attribute("country")
32 | .ok_or(SpeedTestError::ServerParseError)?
33 | .to_string(),
34 | host: n
35 | .attribute("host")
36 | .ok_or(SpeedTestError::ServerParseError)?
37 | .to_string(),
38 | id: n
39 | .attribute("id")
40 | .ok_or(SpeedTestError::ServerParseError)?
41 | .parse()?,
42 | location: location.clone(),
43 | distance: Some(distance::compute_distance(&config.location, &location)),
44 | name: n
45 | .attribute("name")
46 | .ok_or(SpeedTestError::ServerParseError)?
47 | .to_string(),
48 | sponsor: n
49 | .attribute("sponsor")
50 | .ok_or(SpeedTestError::ServerParseError)?
51 | .to_string(),
52 | url: n
53 | .attribute("url")
54 | .ok_or(SpeedTestError::ServerParseError)?
55 | .to_string(),
56 | })
57 | })
58 | .filter_map(Result::ok)
59 | .filter(|server| !config.ignore_servers.contains(&server.id))
60 | .collect();
61 | Ok(SpeedTestServersConfig { servers })
62 | }
63 |
64 | pub fn servers_sorted_by_distance(&self, config: &SpeedTestConfig) -> Vec {
65 | let location = &config.location;
66 | let mut sorted_servers = self.servers.clone();
67 | sorted_servers.sort_by(|a, b| {
68 | let a_distance = distance::compute_distance(location, &a.location);
69 | let b_distance = distance::compute_distance(location, &b.location);
70 | a_distance.partial_cmp(&b_distance).unwrap_or(Less)
71 | });
72 | sorted_servers
73 | }
74 | }
75 |
76 | #[cfg(test)]
77 | mod tests {
78 | use super::*;
79 | use crate::speedtest_config::*;
80 |
81 | fn sample_spt_config() -> SpeedTestConfig {
82 | // Somewhere in Los Angeles
83 | SpeedTestConfig {
84 | location: EarthLocation {
85 | latitude: 32.9954,
86 | longitude: -117.0753,
87 | },
88 | ..SpeedTestConfig::default()
89 | }
90 | }
91 |
92 | #[test]
93 | fn test_parse_speedtest_servers_xml() {
94 | let spt_config = sample_spt_config();
95 | let config_str = include_str!("../tests/config/geo-test-servers-static.php.xml");
96 |
97 | let server_config =
98 | SpeedTestServersConfig::parse_with_config(config_str, &spt_config).unwrap();
99 | assert!(server_config.servers.len() > 5);
100 | let server = server_config.servers.get(1).unwrap();
101 | assert!(!server.url.is_empty());
102 | assert!(!server.country.is_empty());
103 | }
104 |
105 | #[test]
106 | fn test_parse_speedtest_servers_xml_with_ignore_servers() {
107 | let spt_config = SpeedTestConfig {
108 | ignore_servers: vec![5905],
109 | ..sample_spt_config()
110 | };
111 | let config_str = include_str!("../tests/config/geo-test-servers-static.php.xml");
112 |
113 | let server_config =
114 | SpeedTestServersConfig::parse_with_config(config_str, &spt_config).unwrap();
115 | assert!(server_config.servers.iter().all(|s| s.id != 5905));
116 | assert!(server_config.servers.len() > 5);
117 | let server = server_config.servers.get(1).unwrap();
118 | assert!(!server.url.is_empty());
119 | assert!(!server.country.is_empty());
120 | }
121 |
122 | #[test]
123 | fn test_fastest_server() {
124 | let spt_config = sample_spt_config();
125 | let config_str = include_str!("../tests/config/geo-test-servers-static.php.xml");
126 |
127 | let config = SpeedTestServersConfig::parse_with_config(config_str, &spt_config).unwrap();
128 | let closest_server = &config.servers_sorted_by_distance(&spt_config)[0];
129 | assert_eq!("Los Angeles, CA", closest_server.name);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/speedtest_config.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 |
3 | use crate::{distance::EarthLocation, error::SpeedTestError};
4 | use std::{net::Ipv4Addr, time::Duration};
5 |
6 | pub struct SpeedTestClientConfig {
7 | pub ip: Ipv4Addr,
8 | pub isp: String,
9 | }
10 |
11 | impl Default for SpeedTestClientConfig {
12 | fn default() -> Self {
13 | SpeedTestClientConfig {
14 | ip: Ipv4Addr::new(127, 0, 0, 1),
15 | isp: String::default(),
16 | }
17 | }
18 | }
19 |
20 | #[derive(Default)]
21 | pub struct SpeedTestSizeConfig {
22 | pub upload: Vec,
23 | pub download: Vec,
24 | }
25 |
26 | #[derive(Default)]
27 | pub struct SpeedTestCountsConfig {
28 | pub upload: usize,
29 | pub download: usize,
30 | }
31 |
32 | #[derive(Default)]
33 | pub struct SpeedTestThreadsConfig {
34 | pub upload: usize,
35 | pub download: usize,
36 | }
37 |
38 | pub struct SpeedTestLengthConfig {
39 | pub upload: Duration,
40 | pub download: Duration,
41 | }
42 |
43 | impl Default for SpeedTestLengthConfig {
44 | fn default() -> Self {
45 | SpeedTestLengthConfig {
46 | upload: Duration::from_secs(10),
47 | download: Duration::from_secs(10),
48 | }
49 | }
50 | }
51 |
52 | #[derive(Default)]
53 | pub struct SpeedTestConfig {
54 | pub client: SpeedTestClientConfig,
55 | pub ignore_servers: Vec,
56 | pub sizes: SpeedTestSizeConfig,
57 | pub counts: SpeedTestCountsConfig,
58 | pub threads: SpeedTestThreadsConfig,
59 | pub length: SpeedTestLengthConfig,
60 | pub upload_max: usize,
61 | pub location: EarthLocation,
62 | }
63 |
64 | impl SpeedTestConfig {
65 | pub fn parse(config_xml: &str) -> Result {
66 | let document = roxmltree::Document::parse(config_xml)?;
67 |
68 | let server_config_node = document
69 | .descendants()
70 | .find(|n| n.has_tag_name("server-config"))
71 | .ok_or(SpeedTestError::ConfigParseError)?;
72 | let download_node = document
73 | .descendants()
74 | .find(|n| n.has_tag_name("download"))
75 | .ok_or(SpeedTestError::ConfigParseError)?;
76 | let upload_node = document
77 | .descendants()
78 | .find(|n| n.has_tag_name("upload"))
79 | .ok_or(SpeedTestError::ConfigParseError)?;
80 | let client_node = document
81 | .descendants()
82 | .find(|n| n.has_tag_name("client"))
83 | .ok_or(SpeedTestError::ConfigParseError)?;
84 |
85 | let ignore_servers: Vec = server_config_node
86 | .attribute("ignoreids")
87 | .ok_or(SpeedTestError::ConfigParseError)?
88 | .split(',')
89 | .filter(|s| !s.is_empty())
90 | .map(|s| s.parse::())
91 | .collect::, _>>()?;
92 |
93 | let ratio = upload_node
94 | .attribute("ratio")
95 | .ok_or(SpeedTestError::ConfigParseError)?
96 | .parse::()?;
97 |
98 | let upload_max = upload_node
99 | .attribute("maxchunkcount")
100 | .ok_or(SpeedTestError::ConfigParseError)?
101 | .parse::()?;
102 |
103 | let up_sizes = [32768usize, 65536, 131072, 262144, 524288, 1048576, 7340032];
104 |
105 | let sizes = SpeedTestSizeConfig {
106 | upload: up_sizes
107 | .get(ratio - 1..)
108 | .ok_or(SpeedTestError::ConfigParseError)?
109 | .to_vec(),
110 | download: vec![350usize, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000],
111 | };
112 |
113 | let size_count = sizes.upload.len();
114 |
115 | let upload_count = (upload_max as f32 / size_count as f32).ceil() as usize;
116 |
117 | let counts = SpeedTestCountsConfig {
118 | upload: upload_count,
119 | download: download_node
120 | .attribute("threadsperurl")
121 | .ok_or(SpeedTestError::ConfigParseError)?
122 | .parse::()?,
123 | };
124 |
125 | let threads = SpeedTestThreadsConfig {
126 | upload: upload_node
127 | .attribute("threads")
128 | .ok_or(SpeedTestError::ConfigParseError)?
129 | .parse::()?,
130 | download: server_config_node
131 | .attribute("threadcount")
132 | .ok_or(SpeedTestError::ConfigParseError)?
133 | .parse::()?
134 | * 2,
135 | };
136 |
137 | let length = SpeedTestLengthConfig {
138 | upload: upload_node
139 | .attribute("testlength")
140 | .ok_or(SpeedTestError::ConfigParseError)?
141 | .parse::()
142 | .map(Duration::from_secs)?,
143 | download: download_node
144 | .attribute("testlength")
145 | .ok_or(SpeedTestError::ConfigParseError)?
146 | .parse::()
147 | .map(Duration::from_secs)?,
148 | };
149 |
150 | let client = SpeedTestClientConfig {
151 | ip: client_node
152 | .attribute("ip")
153 | .ok_or(SpeedTestError::ConfigParseError)?
154 | .parse()?,
155 | isp: client_node
156 | .attribute("isp")
157 | .ok_or(SpeedTestError::ConfigParseError)?
158 | .to_string(),
159 | };
160 |
161 | Ok(SpeedTestConfig {
162 | client,
163 | ignore_servers,
164 | sizes,
165 | counts,
166 | threads,
167 | length,
168 | upload_max,
169 | location: EarthLocation {
170 | latitude: client_node
171 | .attribute("lat")
172 | .ok_or(SpeedTestError::ConfigParseError)?
173 | .parse()?,
174 | longitude: client_node
175 | .attribute("lon")
176 | .ok_or(SpeedTestError::ConfigParseError)?
177 | .parse()?,
178 | },
179 | })
180 | }
181 | }
182 |
183 | #[cfg(test)]
184 | mod tests {
185 | use super::*;
186 |
187 | #[test]
188 | fn test_parse_config_xml() {
189 | let config =
190 | SpeedTestConfig::parse(include_str!("../tests/config/config.php.xml")).unwrap();
191 | assert_eq!("174.79.12.26", config.client.ip.to_string());
192 | assert_eq!(
193 | EarthLocation {
194 | latitude: 32.9954,
195 | longitude: -117.0753,
196 | },
197 | config.location
198 | );
199 | assert_eq!("Cox Communications", config.client.isp);
200 | }
201 |
202 | #[test]
203 | fn test_parse_config_xml_83() {
204 | let config =
205 | SpeedTestConfig::parse(include_str!("../tests/config/2021-07-speedtest-config.xml"))
206 | .unwrap();
207 | assert_eq!("Cox Communications", config.client.isp);
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/tests/config/config.php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 | 9c1687ea58e5e770-1df5b7cd427370f7-4b62a84526ea1f56
9 | speedtest
10 |
11 |
13 |
14 |
16 |
17 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Rate Your ISP
31 | COPY IP
32 | kilobits
33 | megabits
34 | NEW SERVER
35 | TEST AGAIN
36 | UPLOAD SPEED
37 | DOWNLOAD SPEED
38 | kbps
39 | Mbps
40 | BEGIN TEST
41 | START TEST TO RECOMMENDED SERVER
42 | megabytes
43 | kilobytes
44 | kB/s
45 | MB/s
46 | Mbps
47 | How happy are you with your current Internet service
48 | provider?
49 | Very unhappy
50 | Unhappy
51 | Neutral
52 | Happy
53 | Very happy
54 | YOUR RESULT WILL BECOME PART OF A SPEED WAVE
55 | PING
56 | Hosted by
57 | TOTAL TESTS TO DATE
58 | COPIED
59 | AUTO STARTING SPEED TEST IN
60 | SECONDS
61 | SECOND
62 | ERROR
63 | Try Again
64 | START A SPEED WAVE
65 | Speed Wave Name
66 | Your result is now part of the Speed Wave!
67 | Your Result
68 | Help Us Understand Broadband Costs
69 | Download Package
70 | Upload Package
71 | How much do you pay?
72 | Includes:
73 | Is this your postal code?
74 | SUBMIT
75 | GET A FREE OOKLA SPEEDTEST ACCOUNT
76 | Being logged in would allow you to start a
77 | Speed Wave here! Registration is free and only requires a valid email
78 | address.
79 | Your Email Address
80 |
81 | https://www.facebook.com/dialog/feed?app_id=581657151866321&link=http://www.speedtest.net/my-result/{RESULTID}&description=This%20is%20my%20Ookla%20Speedtest%20result.%20Compare%20your%20speed%20to%20mine!&redirect_uri=http://www.speedtest.net&name=Check%20out%20my%20Ookla%20Speedtest%20results.%20What%27s%20your%20speed%3F
82 | VIEW SPEED WAVE
83 | CREATE
84 | Speed
85 | Phone
86 | TV
87 | What speeds do you pay for?
88 | Thanks for participating in the survey!
89 | SELECTING BEST SERVER BASED ON PING
90 | MY RESULTS
91 | CREATE
92 | YOUR PREFERRED SERVER
93 | RECOMMENDED SERVER
94 | CONNECTING
95 | COPY
96 | SHARE THIS RESULT
97 | COMPARE YOUR RESULT
98 | CONTRIBUTE TO NET INDEX
99 | CLOSE
100 | RETAKE THE SURVEY
101 | IMAGE
102 | FORUM
103 | Use this test result to begin your own Speed
104 | Wave!
105 | Fastest ISPs
106 | wave
107 | share
108 | link:{LANG_CODE}/results.php?source=compare
109 | contribute
110 | bits per second
111 | standard
112 | en
113 | http://pinterest.com/pin/create/button/?url=http%3A%2F%2Fwww.speedtest.net%2F&media=http%3A%2F%2Fspeedtest.net%2Fresult%2F{RESULTID}.png&description=Check%20out%20my%20result%20from%20Ookla%20Speedtest!
114 |
115 | Continue
116 | EDIT
117 | Download
118 | Download:
119 | Upload
120 | Upload:
121 | Connection Type?
122 | Home
123 | Business
124 | School
125 | Public Wi-Fi
126 | Other
127 | My ISP is:
128 | Yes
129 | Wrong
130 | Yes
131 | Wrong
132 | Please enter your postal code
133 | Please enter your ISP name
134 | OK
135 | Please check your upload speed. This
136 | seems faster than expected.
137 | Please check the amount entered. This
138 | seems higher than expected.
139 | Please check your Download speed. This
140 | seems faster than expected.
141 | WEB
142 | EMBED
143 | Are you on
144 | Take our Broadband Internet
145 | Survey!
146 |
147 |
148 |
149 |
150 |
152 |
153 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod distance;
2 | mod error;
3 | #[cfg(not(feature = "log"))]
4 | mod log;
5 | mod speedtest;
6 | mod speedtest_config;
7 | mod speedtest_csv;
8 | mod speedtest_servers_config;
9 |
10 | use crate::speedtest_csv::SpeedTestCsvResult;
11 | use chrono::Utc;
12 | use clap::Parser;
13 | #[cfg(feature = "log")]
14 | use log::info;
15 | #[cfg(not(feature = "log"))]
16 | use log::info;
17 | use std::io::{self, Write};
18 | use url::Url;
19 |
20 | #[derive(Parser)]
21 | #[command(version, about, long_about = None)]
22 | struct Cli {
23 | /// Don't run download test
24 | #[arg(long, default_value_t = false)]
25 | no_download: bool,
26 |
27 | /// Don't run upload test
28 | #[arg(long, default_value_t = false)]
29 | no_upload: bool,
30 |
31 | /// Display a list of speedtest.net servers sorted by distance
32 | #[arg(long, default_value_t = false)]
33 | list: bool,
34 |
35 | /// Generate and provide an URL to the speedtest.net share results image
36 | #[arg(long, default_value_t = false)]
37 | share: bool,
38 |
39 | /// Display values in bytes instead of bits.
40 | #[arg(long, default_value_t = false)]
41 | bytes: bool,
42 |
43 | /// Suppress verbose output, only show basic information
44 | #[arg(long, default_value_t = false)]
45 | simple: bool,
46 |
47 | /// Suppress verbose output, only show basic information in CSV format.
48 | /// Speeds listed in bit/s and not affected by --bytes.
49 | #[arg(long, default_value_t = false)]
50 | csv: bool,
51 |
52 | /// Print CSV headers
53 | #[arg(long, default_value_t = false)]
54 | csv_header: bool,
55 |
56 | /// Single character delimiter to use in CSV output
57 | #[arg(long, default_value_t = ',')]
58 | csv_delimiter: char,
59 |
60 | /// Address of speedtest-mini server
61 | #[arg(short, long)]
62 | mini: Option,
63 | }
64 |
65 | fn main() -> Result<(), error::SpeedTestError> {
66 | env_logger::init();
67 |
68 | let matches = Cli::parse();
69 |
70 | // This appears to be purely informational.
71 | if matches.csv_header {
72 | let results = speedtest_csv::SpeedTestCsvResult::default();
73 |
74 | println!("{}", results.header_serialize());
75 | return Ok(());
76 | }
77 |
78 | let machine_format = matches.csv;
79 |
80 | if !matches.simple && !machine_format {
81 | println!("Retrieving speedtest.net configuration...");
82 | }
83 | let mut config = speedtest::get_configuration()?;
84 |
85 | let mut server_list_sorted;
86 | if let Some(mini) = matches.mini {
87 | let mini_url = Url::parse(&mini).unwrap();
88 |
89 | // matches.value_of("mini").unwrap().to_string()
90 |
91 | let host = mini_url.host().unwrap().to_string();
92 | let hostport = mini_url //
93 | .port()
94 | .map_or_else(
95 | || format!("{}:{}", mini_url.host().unwrap(), mini_url.port().unwrap()),
96 | |_| host.to_string(),
97 | );
98 |
99 | let mut path = mini_url.path();
100 | if path == "/" {
101 | path = "/speedtest/upload.php";
102 | }
103 |
104 | let url = format!("{}://{hostport}{path}", mini_url.scheme());
105 |
106 | server_list_sorted = vec![speedtest::SpeedTestServer {
107 | country: host.to_string(),
108 | host: hostport,
109 | id: 0,
110 | location: distance::EarthLocation {
111 | latitude: 0.0,
112 | longitude: 0.0,
113 | },
114 | distance: None,
115 | name: host.to_string(),
116 | sponsor: host,
117 | url,
118 | }]
119 | } else {
120 | if !matches.simple && !machine_format {
121 | println!("Retrieving speedtest.net server list...");
122 | }
123 | let server_list = speedtest::get_server_list_with_config(&config)?;
124 | server_list_sorted = server_list.servers_sorted_by_distance(&config);
125 |
126 | if matches.list {
127 | for server in server_list_sorted {
128 | println!(
129 | "{:4}) {} ({}, {}) [{}]",
130 | server.id,
131 | server.sponsor,
132 | server.name,
133 | server.country,
134 | server
135 | .distance
136 | .map_or_else(|| "None".to_string(), |d| format!("{d:.2} km")),
137 | );
138 | }
139 | return Ok(());
140 | }
141 | if !matches.simple && !machine_format {
142 | println!(
143 | "Testing from {} ({})...",
144 | config.client.isp, config.client.ip
145 | );
146 | println!("Selecting best server based on latency...");
147 | }
148 |
149 | info!("Five Closest Servers");
150 | server_list_sorted.truncate(5);
151 | for _server in &server_list_sorted {
152 | info!("Close Server: {_server:?}");
153 | }
154 | }
155 | let latency_test_result = speedtest::get_best_server_based_on_latency(&server_list_sorted[..])?;
156 |
157 | if !machine_format {
158 | if !matches.simple {
159 | println!(
160 | "Hosted by {} ({}){}: {}.{} ms",
161 | latency_test_result.server.sponsor,
162 | latency_test_result.server.name,
163 | latency_test_result
164 | .server
165 | .distance
166 | .map_or("".to_string(), |d| format!(" [{d:.2} km]")),
167 | latency_test_result.latency.as_millis(),
168 | latency_test_result.latency.as_micros() % 1000,
169 | );
170 | } else {
171 | println!(
172 | "Ping: {}.{} ms",
173 | latency_test_result.latency.as_millis(),
174 | latency_test_result.latency.as_millis() % 1000,
175 | );
176 | }
177 | }
178 |
179 | let best_server = latency_test_result.server;
180 |
181 | let download_measurement;
182 | let inner_download_measurement;
183 |
184 | if !matches.no_download {
185 | if !matches.simple && !machine_format {
186 | print!("Testing download speed");
187 | inner_download_measurement = speedtest::test_download_with_progress_and_config(
188 | best_server,
189 | print_dot,
190 | &mut config,
191 | )?;
192 | println!();
193 | } else {
194 | inner_download_measurement =
195 | speedtest::test_download_with_progress_and_config(best_server, || {}, &mut config)?;
196 | }
197 |
198 | if !machine_format {
199 | if matches.bytes {
200 | println!(
201 | "Download: {:.2} Mbyte/s",
202 | ((inner_download_measurement.kbps() / 8) as f32 / 1000.00)
203 | );
204 | } else {
205 | println!(
206 | "Download: {:.2} Mbit/s",
207 | (inner_download_measurement.kbps()) as f32 / 1000.00
208 | );
209 | }
210 | }
211 | download_measurement = Some(&inner_download_measurement);
212 | } else {
213 | download_measurement = None;
214 | }
215 |
216 | let upload_measurement;
217 | let inner_upload_measurement;
218 |
219 | if !matches.no_upload {
220 | if !matches.simple && !machine_format {
221 | print!("Testing upload speed");
222 | inner_upload_measurement =
223 | speedtest::test_upload_with_progress_and_config(best_server, print_dot, &config)?;
224 | println!();
225 | } else {
226 | inner_upload_measurement =
227 | speedtest::test_upload_with_progress_and_config(best_server, || {}, &config)?;
228 | }
229 |
230 | if !machine_format {
231 | if matches.bytes {
232 | println!(
233 | "Upload: {:.2} Mbyte/s",
234 | ((inner_upload_measurement.kbps() / 8) as f32 / 1000.00)
235 | );
236 | } else {
237 | println!(
238 | "Upload: {:.2} Mbit/s",
239 | (inner_upload_measurement.kbps() as f32 / 1000.00)
240 | );
241 | }
242 | }
243 | upload_measurement = Some(&inner_upload_measurement);
244 | } else {
245 | upload_measurement = None;
246 | }
247 |
248 | let speedtest_result = speedtest::SpeedTestResult {
249 | download_measurement,
250 | upload_measurement,
251 | server: best_server,
252 | latency_measurement: &latency_test_result,
253 | };
254 |
255 | if matches.csv {
256 | let speedtest_csv_result = SpeedTestCsvResult {
257 | server_id: &best_server.id.to_string(),
258 | sponsor: &best_server.sponsor,
259 | server_name: &best_server.name,
260 | timestamp: &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true),
261 | distance: &(latency_test_result
262 | .server
263 | .distance
264 | .map_or("".to_string(), |d| format!("{d:.14}")))[..],
265 | ping: &format!(
266 | "{}.{}",
267 | latency_test_result.latency.as_millis(),
268 | latency_test_result.latency.as_micros() % 1000
269 | ),
270 | download: &download_measurement
271 | .map_or(0.0, |x| x.bps_f64())
272 | .to_string(),
273 | upload: &upload_measurement.map_or(0.0, |x| x.bps_f64()).to_string(),
274 | share: &if matches.share {
275 | speedtest::get_share_url(&speedtest_result)?
276 | } else {
277 | "".to_string()
278 | },
279 | ip_address: &config.client.ip.to_string(),
280 | };
281 | let mut wtr = csv::WriterBuilder::new()
282 | .has_headers(false)
283 | .delimiter(matches.csv_delimiter as u8)
284 | .from_writer(io::stdout());
285 | wtr.serialize(speedtest_csv_result)?;
286 | wtr.flush()?;
287 | return Ok(());
288 | }
289 |
290 | if matches.share && !machine_format {
291 | info!("Share Request {speedtest_result:?}",);
292 | println!(
293 | "Share results: {}",
294 | speedtest::get_share_url(&speedtest_result)?
295 | );
296 | }
297 |
298 | if let (Some(download_measurement), Some(upload_measurement)) =
299 | (download_measurement, upload_measurement)
300 | {
301 | if !machine_format
302 | && ((download_measurement.kbps() as f32 / 1000.00) > 200.0
303 | || (upload_measurement.kbps() as f32 / 1000.00) > 200.0)
304 | {
305 | println!("WARNING: This tool may not be accurate for high bandwidth connections! Consider using a socket-based client alternative.")
306 | }
307 | }
308 | Ok(())
309 | }
310 |
311 | fn print_dot() {
312 | print!(".");
313 | io::stdout().flush().unwrap();
314 | }
315 |
--------------------------------------------------------------------------------
/tests/config/2020-07-speedtest-config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | f7a45ced624d3a70-1df5b7cd427370f7-b91ee21d6cb22d7b
6 | speedtest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Rate Your ISP
22 | COPY IP
23 | kilobits
24 | megabits
25 | NEW SERVER
26 | TEST AGAIN
27 | UPLOAD SPEED
28 | DOWNLOAD SPEED
29 | kbps
30 | Mbps
31 | BEGIN TEST
32 | START TEST TO RECOMMENDED SERVER
33 | megabytes
34 | kilobytes
35 | kB/s
36 | MB/s
37 | Mbps
38 | How happy are you with your current Internet service provider?
39 | Very unhappy
40 | Unhappy
41 | Neutral
42 | Happy
43 | Very happy
44 | YOUR RESULT WILL BECOME PART OF A SPEED WAVE
45 | PING
46 | Hosted by
47 | TOTAL TESTS
48 | TO DATE
49 | COPIED
50 | AUTO STARTING SPEED TEST IN
51 | SECONDS
52 | SECOND
53 | ERROR
54 | Try Again
55 | START A SPEED WAVE
56 | Speed Wave Name
57 | Your result is now part of the Speed Wave!
58 | Your Result
59 | Help Us Understand Broadband Costs
60 | Download Package
61 | Upload Package
62 | How much do you pay?
63 | Includes:
64 | Is this your postal code?
65 | SUBMIT
66 | GET A FREE OOKLA SPEEDTEST ACCOUNT
67 | Being logged in would allow you to start a Speed Wave here!
68 | Registration is free and only requires a valid email address.
69 | Your Email Address
70 |
71 | https://www.facebook.com/dialog/feed?app_id=581657151866321&link=http://www.speedtest.net/my-result/{RESULTID}&description=This%20is%20my%20Ookla%20Speedtest%20result.%20Compare%20your%20speed%20to%20mine!&redirect_uri=http://www.speedtest.net&name=Check%20out%20my%20Ookla%20Speedtest%20results.%20What%27s%20your%20speed%3F
72 | VIEW SPEED WAVE
73 | CREATE
74 | Speed
75 | Phone
76 | TV
77 | What speeds do you pay for?
78 | Thanks for participating in the survey!
79 | SELECTING BEST SERVER BASED ON PING
80 | MY RESULTS
81 | CREATE
82 | YOUR PREFERRED SERVER
83 | RECOMMENDED SERVER
84 | CONNECTING
85 | COPY
86 | SHARE THIS RESULT
87 | COMPARE
88 | YOUR RESULT
89 | CONTRIBUTE
90 | TO NET INDEX
91 | CLOSE
92 | RETAKE THE
93 | SURVEY
94 | IMAGE
95 | FORUM
96 | Use this test result to begin your own Speed Wave!
97 | Fastest ISPs
98 | wave
99 | share
100 | link:{LANG_CODE}/results.php?source=compare
101 | contribute
102 | bits per second
103 | standard
104 | en
105 | http://pinterest.com/pin/create/button/?url=http%3A%2F%2Fwww.speedtest.net%2F&media=http%3A%2F%2Fspeedtest.net%2Fresult%2F{RESULTID}.png&description=Check%20out%20my%20result%20from%20Ookla%20Speedtest!
106 |
107 | Continue
108 | EDIT
109 | Download
110 | Download:
111 | Upload
112 | Upload:
113 | Connection Type?
114 | Home
115 | Business
116 | School
117 | Public Wi-Fi
118 | Other
119 | My ISP is:
120 | Yes
121 | Wrong
122 | Yes
123 | Wrong
124 | Please enter your postal code
125 | Please enter your ISP name
126 | OK
127 | Please check your upload speed.
128 | This seems faster than expected.
129 | Please check the amount entered.
130 | This seems higher than expected.
131 | Please check your Download speed.
132 | This seems faster than expected.
133 | WEB
134 | EMBED
135 | Are you on
136 | Take our Broadband Internet Survey!
137 | TEST AGAIN
138 | COMPARE
139 |
140 |
141 |
--------------------------------------------------------------------------------
/src/speedtest.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 |
3 | use std::{
4 | io::Read,
5 | path::Path,
6 | sync::atomic::{AtomicBool, Ordering},
7 | time::{Duration, SystemTime, UNIX_EPOCH},
8 | };
9 |
10 | #[cfg(feature = "log")]
11 | use log::info;
12 |
13 | #[cfg(not(feature = "log"))]
14 | use super::log::info;
15 |
16 | use reqwest::blocking::{Body, Client, Request, Response};
17 | use reqwest::header::{HeaderValue, CONNECTION, CONTENT_TYPE, REFERER, USER_AGENT};
18 | use reqwest::Url;
19 |
20 | use crate::distance::EarthLocation;
21 | use crate::error::SpeedTestError;
22 | use crate::speedtest_config::SpeedTestConfig;
23 | use crate::speedtest_servers_config::SpeedTestServersConfig;
24 | use rayon::prelude::*;
25 |
26 | const ST_USER_AGENT: &str = concat!("reqwest/speedtest-rs ", env!("CARGO_PKG_VERSION"));
27 |
28 | #[derive(Clone, Debug)]
29 | pub struct SpeedTestServer {
30 | pub country: String,
31 | pub host: String,
32 | pub id: u32,
33 | pub location: EarthLocation,
34 | pub distance: Option,
35 | pub name: String,
36 | pub sponsor: String,
37 | pub url: String,
38 | }
39 |
40 | pub fn download_configuration() -> Result {
41 | info!("Downloading Configuration from speedtest.net");
42 |
43 | let mut _server = mockito::Server::new();
44 |
45 | #[cfg(not(test))]
46 | let url = "http://www.speedtest.net/speedtest-config.php";
47 | #[cfg(test)]
48 | let url = &format!("{}/speedtest-config.php", &_server.url());
49 |
50 | let client = Client::new();
51 | // Creating an outgoing request.
52 | let res = client
53 | .get(url)
54 | .header(CONNECTION, "close")
55 | .header(USER_AGENT, ST_USER_AGENT.to_owned())
56 | .send()?;
57 | info!("Downloaded Configuration from speedtest.net");
58 | Ok(res)
59 | }
60 |
61 | pub fn get_configuration() -> Result {
62 | let config_body = download_configuration()?;
63 | info!("Parsing Configuration");
64 | let spt_config = SpeedTestConfig::parse(&(config_body.text()?))?;
65 | info!("Parsed Configuration");
66 | Ok(spt_config)
67 | }
68 |
69 | pub fn download_server_list() -> Result {
70 | info!("Download Server List");
71 | let mut _server = mockito::Server::new();
72 |
73 | #[cfg(not(test))]
74 | let url = "http://www.speedtest.net/speedtest-servers.php";
75 | #[cfg(test)]
76 | let url = &format!("{}/speedtest-servers.php", &_server.url());
77 |
78 | let client = Client::new();
79 | let server_res = client
80 | .get(url)
81 | .header(CONNECTION, "close")
82 | .header(USER_AGENT, ST_USER_AGENT)
83 | .send()?;
84 | info!("Downloaded Server List");
85 | Ok(server_res)
86 | }
87 |
88 | pub fn get_server_list_with_config(
89 | config: &SpeedTestConfig,
90 | ) -> Result {
91 | let config_body = download_server_list()?;
92 | info!("Parsing Server List");
93 | let server_config_string = config_body.text()?;
94 |
95 | info!("Parsed Server List");
96 | SpeedTestServersConfig::parse_with_config(&server_config_string, config)
97 | }
98 |
99 | #[derive(Debug)]
100 | pub struct SpeedTestLatencyTestResult<'a> {
101 | pub server: &'a SpeedTestServer,
102 | pub latency: Duration,
103 | }
104 |
105 | pub fn get_best_server_based_on_latency(
106 | servers: &[SpeedTestServer],
107 | ) -> Result {
108 | info!("Testing for fastest server");
109 | let client = Client::new();
110 | let mut fastest_server = None;
111 | let mut fastest_latency = Duration::new(u64::MAX, 0);
112 | // Return error if no servers are available.
113 | if servers.is_empty() {
114 | return Err(SpeedTestError::LatencyTestNoServerError);
115 | }
116 | 'server_loop: for server in servers {
117 | let path = Path::new(&server.url);
118 | let latency_path = format!(
119 | "{}/latency.txt",
120 | path.parent()
121 | .ok_or(SpeedTestError::LatencyTestInvalidPath)?
122 | .display()
123 | );
124 | info!("Downloading: {:?}", latency_path);
125 | let mut latency_measurements = vec![];
126 | for _ in 0..3 {
127 | let start_time = SystemTime::now();
128 | let res = client
129 | .get(&latency_path)
130 | .header(CONNECTION, "close")
131 | .header(USER_AGENT, ST_USER_AGENT.to_owned())
132 | .send();
133 | if res.is_err() {
134 | // Log the error and continue to the next server.
135 | info!("Error: {:?}", res.err());
136 | continue 'server_loop;
137 | }
138 | let _ = res?.bytes()?.last();
139 | let latency_measurement = SystemTime::now().duration_since(start_time)?;
140 | info!("Sampled {} ms", latency_measurement.as_millis());
141 | latency_measurements.push(latency_measurement);
142 | }
143 | // Divide by the double to get the non-RTT time but the trip time.
144 | // NOT PING or RTT
145 | // https://github.com/sivel/speedtest-cli/pull/199
146 | let latency = latency_measurements
147 | .iter()
148 | .fold(Duration::new(0, 0), |a, &i| a + i)
149 | / ((latency_measurements.len() as u32) * 2);
150 | info!("Trip calculated to {} ms", latency.as_millis());
151 |
152 | if latency < fastest_latency {
153 | fastest_server = Some(server);
154 | fastest_latency = latency;
155 | }
156 | }
157 | info!(
158 | "Fastest Server @ {}ms : {fastest_server:?}",
159 | fastest_latency.as_millis(),
160 | );
161 | Ok(SpeedTestLatencyTestResult {
162 | server: fastest_server.ok_or(SpeedTestError::LatencyTestClosestError)?,
163 | latency: fastest_latency,
164 | })
165 | }
166 |
167 | #[derive(Debug)]
168 | pub struct SpeedMeasurement {
169 | pub size: usize,
170 | pub duration: Duration,
171 | }
172 |
173 | impl SpeedMeasurement {
174 | pub fn kbps(&self) -> u32 {
175 | (self.size as u32 * 8) / self.duration.as_millis() as u32
176 | }
177 |
178 | pub fn bps_f64(&self) -> f64 {
179 | (self.size as f64 * 8.0) / (self.duration.as_millis() as f64 / (1000.0))
180 | }
181 | }
182 |
183 | pub fn test_download_with_progress_and_config(
184 | server: &SpeedTestServer,
185 | progress_callback: F,
186 | config: &mut SpeedTestConfig,
187 | ) -> Result
188 | where
189 | F: Fn() + Send + Sync + 'static,
190 | {
191 | info!("Testing Download speed");
192 | let root_url = Url::parse(&server.url)?;
193 |
194 | let mut urls = vec![];
195 | for size in &config.sizes.download {
196 | let mut download_with_size_url = root_url.clone();
197 | {
198 | let mut path_segments_mut = download_with_size_url
199 | .path_segments_mut()
200 | .map_err(|_| SpeedTestError::ServerParseError)?;
201 | path_segments_mut.push(&format!("random{size}x{size}.jpg"));
202 | }
203 | for _ in 0..config.counts.download {
204 | urls.push(download_with_size_url.clone());
205 | }
206 | }
207 |
208 | let _request_count = urls.len();
209 |
210 | let requests = urls
211 | .iter()
212 | .enumerate()
213 | .map(|(i, url)| {
214 | let mut cache_busting_url = url.clone();
215 | cache_busting_url.query_pairs_mut().append_pair(
216 | "x",
217 | &format!(
218 | "{}.{i}",
219 | SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(),
220 | ),
221 | );
222 | let mut request = Request::new(reqwest::Method::GET, url.clone());
223 | request.headers_mut().insert(
224 | reqwest::header::CACHE_CONTROL,
225 | HeaderValue::from_static("no-cache"),
226 | );
227 | request.headers_mut().insert(
228 | reqwest::header::USER_AGENT,
229 | HeaderValue::from_static(ST_USER_AGENT),
230 | );
231 | request.headers_mut().insert(
232 | reqwest::header::CONNECTION,
233 | HeaderValue::from_static("close"),
234 | );
235 | Ok(request)
236 | })
237 | .collect::, SpeedTestError>>()?;
238 |
239 | // TODO: Setup Ctrl-C Termination to use this "event".
240 | let early_termination = AtomicBool::new(false);
241 |
242 | // Start Timer
243 | let start_time = SystemTime::now();
244 |
245 | info!("Download Threads: {}", config.threads.download);
246 | let pool = rayon::ThreadPoolBuilder::new()
247 | .num_threads(config.threads.download)
248 | .build()?;
249 |
250 | info!("Total to be requested {requests:?}");
251 |
252 | let total_transferred_per_thread = pool.install(|| {
253 | requests
254 | .into_iter()
255 | // Make it sequential like the original. Ramp up the file sizes.
256 | .par_bridge()
257 | .map(|r| {
258 | let client = Client::new();
259 | // let downloaded_count = vec![];
260 | progress_callback();
261 | info!("Requesting {}", r.url());
262 | let mut response = client.execute(r)?;
263 | let mut buf = [0u8; 10240];
264 | let mut read_amounts = vec![];
265 | while (SystemTime::now().duration_since(start_time)? < config.length.upload)
266 | && !early_termination.load(Ordering::Relaxed)
267 | {
268 | let read_amount = response.read(&mut buf)?;
269 | read_amounts.push(read_amount);
270 | if read_amount == 0 {
271 | break;
272 | }
273 | }
274 | let total_transfered = read_amounts.iter().sum::();
275 | progress_callback();
276 |
277 | Ok(total_transfered)
278 | })
279 | .collect::, SpeedTestError>>()
280 | });
281 |
282 | let total_transferred: usize = total_transferred_per_thread?.iter().sum();
283 |
284 | let end_time = SystemTime::now();
285 |
286 | let measurement = SpeedMeasurement {
287 | size: total_transferred,
288 | duration: end_time.duration_since(start_time)?,
289 | };
290 |
291 | if measurement.bps_f64() > 100000.0 {
292 | config.threads.upload = 8
293 | }
294 |
295 | Ok(measurement)
296 | }
297 |
298 | #[derive(Debug)]
299 | pub struct SpeedTestUploadRequest {
300 | pub request: Request,
301 | pub size: usize,
302 | }
303 |
304 | pub fn test_upload_with_progress_and_config(
305 | server: &SpeedTestServer,
306 | progress_callback: F,
307 | config: &SpeedTestConfig,
308 | ) -> Result
309 | where
310 | F: Fn() + Send + Sync + 'static,
311 | {
312 | info!("Testing Upload speed");
313 |
314 | let mut sizes = vec![];
315 | for &size in &config.sizes.upload {
316 | for _ in 0..config.counts.upload {
317 | sizes.push(size)
318 | }
319 | }
320 |
321 | let best_url = Url::parse(&server.url)?;
322 |
323 | let request_count = config.upload_max;
324 |
325 | let requests = sizes
326 | .into_iter()
327 | .map(|size| {
328 | let content_iter = b"content1="
329 | .iter()
330 | .chain(b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".iter().cycle())
331 | .take(size);
332 | let content_iter_read = iter_read::IterRead::new(content_iter);
333 | let body = Body::sized(content_iter_read, size as u64);
334 | let mut request = Request::new(reqwest::Method::POST, best_url.clone());
335 | request.headers_mut().insert(
336 | reqwest::header::USER_AGENT,
337 | HeaderValue::from_static(ST_USER_AGENT),
338 | );
339 | request.headers_mut().insert(
340 | reqwest::header::CONNECTION,
341 | HeaderValue::from_static("close"),
342 | );
343 | *request.body_mut() = Some(body);
344 | Ok(SpeedTestUploadRequest { request, size })
345 | })
346 | .collect::, SpeedTestError>>()?;
347 | // TODO: Setup Ctrl-C Termination to use this "event".
348 | let early_termination = AtomicBool::new(false);
349 |
350 | // Start Timer
351 | let start_time = SystemTime::now();
352 |
353 | info!("Upload Threads: {}", config.threads.upload);
354 | let pool = rayon::ThreadPoolBuilder::new()
355 | .num_threads(config.threads.upload)
356 | .build()?;
357 |
358 | info!("Total to be requested {:?}", requests.len());
359 | let total_transferred_per_thread = pool.install(|| {
360 | requests
361 | .into_iter()
362 | .take(request_count)
363 | // Make it sequential like the original. Ramp up the file sizes.
364 | .par_bridge()
365 | .map(|r| {
366 | progress_callback();
367 |
368 | if (SystemTime::now().duration_since(start_time)? < config.length.upload)
369 | && !early_termination.load(Ordering::Relaxed)
370 | {
371 | let client = Client::new();
372 | info!("Requesting {}", r.request.url());
373 | let response = client.execute(r.request);
374 | if response.is_err() {
375 | return Ok(r.size);
376 | };
377 | } else {
378 | return Ok(0);
379 | }
380 | progress_callback();
381 |
382 | Ok(r.size)
383 | })
384 | .collect::, SpeedTestError>>()
385 | });
386 |
387 | let total_transferred: usize = total_transferred_per_thread?.iter().sum();
388 |
389 | let end_time = SystemTime::now();
390 |
391 | let measurement = SpeedMeasurement {
392 | size: total_transferred,
393 | duration: end_time.duration_since(start_time)?,
394 | };
395 |
396 | Ok(measurement)
397 | }
398 |
399 | #[derive(Debug)]
400 | pub struct SpeedTestResult<'a, 'b, 'c> {
401 | pub download_measurement: Option<&'a SpeedMeasurement>,
402 | pub upload_measurement: Option<&'b SpeedMeasurement>,
403 | pub server: &'c SpeedTestServer,
404 | pub latency_measurement: &'c SpeedTestLatencyTestResult<'c>,
405 | }
406 |
407 | impl<'a, 'b, 'c> SpeedTestResult<'a, 'b, 'c> {
408 | pub fn hash(&self) -> String {
409 | let hashed_str = format!(
410 | "{}-{}-{}-{}",
411 | self.latency_measurement.latency.as_millis(),
412 | self.upload_measurement.map_or(0, |x| x.kbps()),
413 | self.download_measurement.map_or(0, |x| x.kbps()),
414 | "297aae72"
415 | );
416 |
417 | format!("{:x}", md5::compute(hashed_str))
418 | }
419 | }
420 |
421 | pub fn get_share_url(speedtest_result: &SpeedTestResult) -> Result {
422 | info!("Generating share URL");
423 |
424 | let download = speedtest_result
425 | .download_measurement
426 | .map_or(0, |x| x.kbps());
427 | info!("Download parameter is {download:?}");
428 | let upload = speedtest_result.upload_measurement.map_or(0, |x| x.kbps());
429 | info!("Upload parameter is {upload:?}");
430 | let server = speedtest_result.server.id;
431 | info!("Server parameter is {server:?}");
432 | let ping = speedtest_result.latency_measurement.latency;
433 | info!("Ping parameter is {ping:?}");
434 |
435 | let pairs = [
436 | ("download", download.to_string()),
437 | ("ping", ping.as_millis().to_string()),
438 | ("upload", upload.to_string()),
439 | ("promo", String::new()),
440 | ("startmode", "pingselect".to_string()),
441 | ("recommendedserverid", format!("{server}")),
442 | ("accuracy", "1".to_string()),
443 | ("serverid", format!("{server}")),
444 | ("hash", speedtest_result.hash()),
445 | ];
446 |
447 | let body = url::form_urlencoded::Serializer::new(String::new())
448 | .extend_pairs(pairs.iter())
449 | .finish();
450 |
451 | info!("Share Body Request: {body:?}");
452 |
453 | let client = Client::new();
454 | let res = client
455 | .post("http://www.speedtest.net/api/api.php")
456 | .header(CONNECTION, "close")
457 | .header(REFERER, "http://c.speedtest.net/flash/speedtest.swf")
458 | .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
459 | .body(body)
460 | .send();
461 | let encode_return = res?.text()?;
462 | let response_id = parse_share_request_response_id(encode_return.as_bytes())?;
463 | Ok(format!("http://www.speedtest.net/result/{response_id}.png"))
464 | }
465 |
466 | pub fn parse_share_request_response_id(input: &[u8]) -> Result {
467 | url::form_urlencoded::parse(input)
468 | .find(|pair| pair.0 == "resultid")
469 | .map_or_else(
470 | || Err(SpeedTestError::ParseShareUrlError),
471 | |pair| Ok(pair.1.into_owned()),
472 | )
473 | }
474 |
475 | #[cfg(test)]
476 | mod tests {
477 | use super::*;
478 |
479 | #[test]
480 | fn test_parse_share_request_response_id() {
481 | let resp = "resultid=4932415710&date=12%2F21%2F2015&time=5%3A10+AM&rating=0".as_bytes();
482 | assert_eq!(parse_share_request_response_id(resp).unwrap(), "4932415710");
483 | }
484 |
485 | #[test]
486 | fn test_share_url_hash() {
487 | let download_measurement = SpeedMeasurement {
488 | size: (6096 * 100) as usize,
489 | duration: Duration::new(1, 0),
490 | };
491 | println!("Download: {:?}", download_measurement);
492 | let upload_measurement = SpeedMeasurement {
493 | size: (1861 * 100) as usize,
494 | duration: Duration::new(1, 0),
495 | };
496 | println!("Upload: {:?}", upload_measurement);
497 | let server = SpeedTestServer {
498 | country: "".to_owned(),
499 | host: "".to_owned(),
500 | id: 5116,
501 | location: EarthLocation {
502 | latitude: 0.0,
503 | longitude: 0.0,
504 | },
505 | distance: None,
506 | name: "".to_owned(),
507 | sponsor: "".to_owned(),
508 | url: "".to_owned(),
509 | };
510 | println!("Server: {server:?}");
511 | let latency_measurement = SpeedTestLatencyTestResult {
512 | server: &server,
513 | latency: Duration::from_millis(26),
514 | };
515 | println!("Latency: {latency_measurement:?}");
516 | let request = SpeedTestResult {
517 | download_measurement: Some(&download_measurement),
518 | upload_measurement: Some(&upload_measurement),
519 | server: &server,
520 | latency_measurement: &latency_measurement,
521 | };
522 | assert_eq!(request.hash(), "f10eb3dd8d3c38a221e823d859680045");
523 | }
524 |
525 | #[test]
526 | fn test_construct_share_form() {}
527 |
528 | #[test]
529 | fn test_get_configuration() {
530 | let mut server = mockito::Server::new();
531 |
532 | let _m = server
533 | .mock("GET", "/speedtest-config.php")
534 | .with_status(200)
535 | .with_body_from_file("tests/config/stripped-config.php.xml")
536 | .create();
537 | let _config = get_configuration();
538 | }
539 |
540 | #[test]
541 | fn test_get_server_list_with_config() {
542 | let mut server = mockito::Server::new();
543 |
544 | let _m = server
545 | .mock("GET", "/speedtest-config.php")
546 | .with_status(200)
547 | .with_body_from_file("tests/config/servers-static.php.xml")
548 | .create();
549 |
550 | let _server_list_config = get_server_list_with_config(&SpeedTestConfig::default());
551 | }
552 | }
553 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.22.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler"
16 | version = "1.0.2"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
19 |
20 | [[package]]
21 | name = "aho-corasick"
22 | version = "1.1.3"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
25 | dependencies = [
26 | "memchr",
27 | ]
28 |
29 | [[package]]
30 | name = "android-tzdata"
31 | version = "0.1.1"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
34 |
35 | [[package]]
36 | name = "android_system_properties"
37 | version = "0.1.5"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
40 | dependencies = [
41 | "libc",
42 | ]
43 |
44 | [[package]]
45 | name = "anstream"
46 | version = "0.6.14"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
49 | dependencies = [
50 | "anstyle",
51 | "anstyle-parse",
52 | "anstyle-query",
53 | "anstyle-wincon",
54 | "colorchoice",
55 | "is_terminal_polyfill",
56 | "utf8parse",
57 | ]
58 |
59 | [[package]]
60 | name = "anstyle"
61 | version = "1.0.7"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
64 |
65 | [[package]]
66 | name = "anstyle-parse"
67 | version = "0.2.4"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
70 | dependencies = [
71 | "utf8parse",
72 | ]
73 |
74 | [[package]]
75 | name = "anstyle-query"
76 | version = "1.1.0"
77 | source = "registry+https://github.com/rust-lang/crates.io-index"
78 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
79 | dependencies = [
80 | "windows-sys 0.52.0",
81 | ]
82 |
83 | [[package]]
84 | name = "anstyle-wincon"
85 | version = "3.0.3"
86 | source = "registry+https://github.com/rust-lang/crates.io-index"
87 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
88 | dependencies = [
89 | "anstyle",
90 | "windows-sys 0.52.0",
91 | ]
92 |
93 | [[package]]
94 | name = "assert-json-diff"
95 | version = "2.0.2"
96 | source = "registry+https://github.com/rust-lang/crates.io-index"
97 | checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
98 | dependencies = [
99 | "serde",
100 | "serde_json",
101 | ]
102 |
103 | [[package]]
104 | name = "atomic-waker"
105 | version = "1.1.2"
106 | source = "registry+https://github.com/rust-lang/crates.io-index"
107 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
108 |
109 | [[package]]
110 | name = "autocfg"
111 | version = "1.3.0"
112 | source = "registry+https://github.com/rust-lang/crates.io-index"
113 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
114 |
115 | [[package]]
116 | name = "backtrace"
117 | version = "0.3.73"
118 | source = "registry+https://github.com/rust-lang/crates.io-index"
119 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
120 | dependencies = [
121 | "addr2line",
122 | "cc",
123 | "cfg-if",
124 | "libc",
125 | "miniz_oxide",
126 | "object",
127 | "rustc-demangle",
128 | ]
129 |
130 | [[package]]
131 | name = "base64"
132 | version = "0.22.1"
133 | source = "registry+https://github.com/rust-lang/crates.io-index"
134 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
135 |
136 | [[package]]
137 | name = "bitflags"
138 | version = "1.3.2"
139 | source = "registry+https://github.com/rust-lang/crates.io-index"
140 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
141 |
142 | [[package]]
143 | name = "bitflags"
144 | version = "2.6.0"
145 | source = "registry+https://github.com/rust-lang/crates.io-index"
146 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
147 |
148 | [[package]]
149 | name = "bumpalo"
150 | version = "3.16.0"
151 | source = "registry+https://github.com/rust-lang/crates.io-index"
152 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
153 |
154 | [[package]]
155 | name = "bytes"
156 | version = "1.6.1"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
159 |
160 | [[package]]
161 | name = "cc"
162 | version = "1.1.6"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
165 |
166 | [[package]]
167 | name = "cfg-if"
168 | version = "1.0.0"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
171 |
172 | [[package]]
173 | name = "chrono"
174 | version = "0.4.38"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
177 | dependencies = [
178 | "android-tzdata",
179 | "iana-time-zone",
180 | "js-sys",
181 | "num-traits",
182 | "wasm-bindgen",
183 | "windows-targets 0.52.6",
184 | ]
185 |
186 | [[package]]
187 | name = "clap"
188 | version = "4.5.10"
189 | source = "registry+https://github.com/rust-lang/crates.io-index"
190 | checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142"
191 | dependencies = [
192 | "clap_builder",
193 | "clap_derive",
194 | ]
195 |
196 | [[package]]
197 | name = "clap_builder"
198 | version = "4.5.10"
199 | source = "registry+https://github.com/rust-lang/crates.io-index"
200 | checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac"
201 | dependencies = [
202 | "anstream",
203 | "anstyle",
204 | "clap_lex",
205 | "strsim",
206 | ]
207 |
208 | [[package]]
209 | name = "clap_derive"
210 | version = "4.5.8"
211 | source = "registry+https://github.com/rust-lang/crates.io-index"
212 | checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
213 | dependencies = [
214 | "heck",
215 | "proc-macro2",
216 | "quote",
217 | "syn",
218 | ]
219 |
220 | [[package]]
221 | name = "clap_lex"
222 | version = "0.7.1"
223 | source = "registry+https://github.com/rust-lang/crates.io-index"
224 | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
225 |
226 | [[package]]
227 | name = "colorchoice"
228 | version = "1.0.1"
229 | source = "registry+https://github.com/rust-lang/crates.io-index"
230 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
231 |
232 | [[package]]
233 | name = "colored"
234 | version = "2.1.0"
235 | source = "registry+https://github.com/rust-lang/crates.io-index"
236 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
237 | dependencies = [
238 | "lazy_static",
239 | "windows-sys 0.48.0",
240 | ]
241 |
242 | [[package]]
243 | name = "core-foundation"
244 | version = "0.9.4"
245 | source = "registry+https://github.com/rust-lang/crates.io-index"
246 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
247 | dependencies = [
248 | "core-foundation-sys",
249 | "libc",
250 | ]
251 |
252 | [[package]]
253 | name = "core-foundation-sys"
254 | version = "0.8.6"
255 | source = "registry+https://github.com/rust-lang/crates.io-index"
256 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
257 |
258 | [[package]]
259 | name = "crossbeam-deque"
260 | version = "0.8.5"
261 | source = "registry+https://github.com/rust-lang/crates.io-index"
262 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
263 | dependencies = [
264 | "crossbeam-epoch",
265 | "crossbeam-utils",
266 | ]
267 |
268 | [[package]]
269 | name = "crossbeam-epoch"
270 | version = "0.9.18"
271 | source = "registry+https://github.com/rust-lang/crates.io-index"
272 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
273 | dependencies = [
274 | "crossbeam-utils",
275 | ]
276 |
277 | [[package]]
278 | name = "crossbeam-utils"
279 | version = "0.8.20"
280 | source = "registry+https://github.com/rust-lang/crates.io-index"
281 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
282 |
283 | [[package]]
284 | name = "csv"
285 | version = "1.3.0"
286 | source = "registry+https://github.com/rust-lang/crates.io-index"
287 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
288 | dependencies = [
289 | "csv-core",
290 | "itoa",
291 | "ryu",
292 | "serde",
293 | ]
294 |
295 | [[package]]
296 | name = "csv-core"
297 | version = "0.1.11"
298 | source = "registry+https://github.com/rust-lang/crates.io-index"
299 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
300 | dependencies = [
301 | "memchr",
302 | ]
303 |
304 | [[package]]
305 | name = "either"
306 | version = "1.13.0"
307 | source = "registry+https://github.com/rust-lang/crates.io-index"
308 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
309 |
310 | [[package]]
311 | name = "encoding_rs"
312 | version = "0.8.33"
313 | source = "registry+https://github.com/rust-lang/crates.io-index"
314 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
315 | dependencies = [
316 | "cfg-if",
317 | ]
318 |
319 | [[package]]
320 | name = "env_filter"
321 | version = "0.1.1"
322 | source = "registry+https://github.com/rust-lang/crates.io-index"
323 | checksum = "c6dc8c8ff84895b051f07a0e65f975cf225131742531338752abfb324e4449ff"
324 | dependencies = [
325 | "log",
326 | "regex",
327 | ]
328 |
329 | [[package]]
330 | name = "env_logger"
331 | version = "0.11.4"
332 | source = "registry+https://github.com/rust-lang/crates.io-index"
333 | checksum = "06676b12debf7bba6903559720abca942d3a66b8acb88815fd2c7c6537e9ade1"
334 | dependencies = [
335 | "anstream",
336 | "anstyle",
337 | "env_filter",
338 | "humantime",
339 | "log",
340 | ]
341 |
342 | [[package]]
343 | name = "equivalent"
344 | version = "1.0.1"
345 | source = "registry+https://github.com/rust-lang/crates.io-index"
346 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
347 |
348 | [[package]]
349 | name = "errno"
350 | version = "0.3.8"
351 | source = "registry+https://github.com/rust-lang/crates.io-index"
352 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
353 | dependencies = [
354 | "libc",
355 | "windows-sys 0.52.0",
356 | ]
357 |
358 | [[package]]
359 | name = "fastrand"
360 | version = "2.0.1"
361 | source = "registry+https://github.com/rust-lang/crates.io-index"
362 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
363 |
364 | [[package]]
365 | name = "fnv"
366 | version = "1.0.7"
367 | source = "registry+https://github.com/rust-lang/crates.io-index"
368 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
369 |
370 | [[package]]
371 | name = "foreign-types"
372 | version = "0.3.2"
373 | source = "registry+https://github.com/rust-lang/crates.io-index"
374 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
375 | dependencies = [
376 | "foreign-types-shared",
377 | ]
378 |
379 | [[package]]
380 | name = "foreign-types-shared"
381 | version = "0.1.1"
382 | source = "registry+https://github.com/rust-lang/crates.io-index"
383 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
384 |
385 | [[package]]
386 | name = "form_urlencoded"
387 | version = "1.2.1"
388 | source = "registry+https://github.com/rust-lang/crates.io-index"
389 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
390 | dependencies = [
391 | "percent-encoding",
392 | ]
393 |
394 | [[package]]
395 | name = "futures-channel"
396 | version = "0.3.30"
397 | source = "registry+https://github.com/rust-lang/crates.io-index"
398 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
399 | dependencies = [
400 | "futures-core",
401 | "futures-sink",
402 | ]
403 |
404 | [[package]]
405 | name = "futures-core"
406 | version = "0.3.30"
407 | source = "registry+https://github.com/rust-lang/crates.io-index"
408 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
409 |
410 | [[package]]
411 | name = "futures-io"
412 | version = "0.3.30"
413 | source = "registry+https://github.com/rust-lang/crates.io-index"
414 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
415 |
416 | [[package]]
417 | name = "futures-sink"
418 | version = "0.3.30"
419 | source = "registry+https://github.com/rust-lang/crates.io-index"
420 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
421 |
422 | [[package]]
423 | name = "futures-task"
424 | version = "0.3.30"
425 | source = "registry+https://github.com/rust-lang/crates.io-index"
426 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
427 |
428 | [[package]]
429 | name = "futures-util"
430 | version = "0.3.30"
431 | source = "registry+https://github.com/rust-lang/crates.io-index"
432 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
433 | dependencies = [
434 | "futures-core",
435 | "futures-io",
436 | "futures-sink",
437 | "futures-task",
438 | "memchr",
439 | "pin-project-lite",
440 | "pin-utils",
441 | "slab",
442 | ]
443 |
444 | [[package]]
445 | name = "getrandom"
446 | version = "0.2.15"
447 | source = "registry+https://github.com/rust-lang/crates.io-index"
448 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
449 | dependencies = [
450 | "cfg-if",
451 | "libc",
452 | "wasi",
453 | ]
454 |
455 | [[package]]
456 | name = "gimli"
457 | version = "0.29.0"
458 | source = "registry+https://github.com/rust-lang/crates.io-index"
459 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
460 |
461 | [[package]]
462 | name = "h2"
463 | version = "0.3.26"
464 | source = "registry+https://github.com/rust-lang/crates.io-index"
465 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
466 | dependencies = [
467 | "bytes",
468 | "fnv",
469 | "futures-core",
470 | "futures-sink",
471 | "futures-util",
472 | "http 0.2.12",
473 | "indexmap",
474 | "slab",
475 | "tokio",
476 | "tokio-util",
477 | "tracing",
478 | ]
479 |
480 | [[package]]
481 | name = "h2"
482 | version = "0.4.5"
483 | source = "registry+https://github.com/rust-lang/crates.io-index"
484 | checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
485 | dependencies = [
486 | "atomic-waker",
487 | "bytes",
488 | "fnv",
489 | "futures-core",
490 | "futures-sink",
491 | "http 1.1.0",
492 | "indexmap",
493 | "slab",
494 | "tokio",
495 | "tokio-util",
496 | "tracing",
497 | ]
498 |
499 | [[package]]
500 | name = "hashbrown"
501 | version = "0.14.5"
502 | source = "registry+https://github.com/rust-lang/crates.io-index"
503 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
504 |
505 | [[package]]
506 | name = "heck"
507 | version = "0.5.0"
508 | source = "registry+https://github.com/rust-lang/crates.io-index"
509 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
510 |
511 | [[package]]
512 | name = "hermit-abi"
513 | version = "0.3.9"
514 | source = "registry+https://github.com/rust-lang/crates.io-index"
515 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
516 |
517 | [[package]]
518 | name = "http"
519 | version = "0.2.12"
520 | source = "registry+https://github.com/rust-lang/crates.io-index"
521 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
522 | dependencies = [
523 | "bytes",
524 | "fnv",
525 | "itoa",
526 | ]
527 |
528 | [[package]]
529 | name = "http"
530 | version = "1.1.0"
531 | source = "registry+https://github.com/rust-lang/crates.io-index"
532 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
533 | dependencies = [
534 | "bytes",
535 | "fnv",
536 | "itoa",
537 | ]
538 |
539 | [[package]]
540 | name = "http-body"
541 | version = "0.4.6"
542 | source = "registry+https://github.com/rust-lang/crates.io-index"
543 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
544 | dependencies = [
545 | "bytes",
546 | "http 0.2.12",
547 | "pin-project-lite",
548 | ]
549 |
550 | [[package]]
551 | name = "http-body"
552 | version = "1.0.0"
553 | source = "registry+https://github.com/rust-lang/crates.io-index"
554 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
555 | dependencies = [
556 | "bytes",
557 | "http 1.1.0",
558 | ]
559 |
560 | [[package]]
561 | name = "http-body-util"
562 | version = "0.1.2"
563 | source = "registry+https://github.com/rust-lang/crates.io-index"
564 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
565 | dependencies = [
566 | "bytes",
567 | "futures-util",
568 | "http 1.1.0",
569 | "http-body 1.0.0",
570 | "pin-project-lite",
571 | ]
572 |
573 | [[package]]
574 | name = "httparse"
575 | version = "1.9.4"
576 | source = "registry+https://github.com/rust-lang/crates.io-index"
577 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
578 |
579 | [[package]]
580 | name = "httpdate"
581 | version = "1.0.3"
582 | source = "registry+https://github.com/rust-lang/crates.io-index"
583 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
584 |
585 | [[package]]
586 | name = "humantime"
587 | version = "2.1.0"
588 | source = "registry+https://github.com/rust-lang/crates.io-index"
589 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
590 |
591 | [[package]]
592 | name = "hyper"
593 | version = "0.14.30"
594 | source = "registry+https://github.com/rust-lang/crates.io-index"
595 | checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
596 | dependencies = [
597 | "bytes",
598 | "futures-channel",
599 | "futures-core",
600 | "futures-util",
601 | "h2 0.3.26",
602 | "http 0.2.12",
603 | "http-body 0.4.6",
604 | "httparse",
605 | "httpdate",
606 | "itoa",
607 | "pin-project-lite",
608 | "tokio",
609 | "tower-service",
610 | "tracing",
611 | "want",
612 | ]
613 |
614 | [[package]]
615 | name = "hyper"
616 | version = "1.4.1"
617 | source = "registry+https://github.com/rust-lang/crates.io-index"
618 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
619 | dependencies = [
620 | "bytes",
621 | "futures-channel",
622 | "futures-util",
623 | "h2 0.4.5",
624 | "http 1.1.0",
625 | "http-body 1.0.0",
626 | "httparse",
627 | "itoa",
628 | "pin-project-lite",
629 | "smallvec",
630 | "tokio",
631 | "want",
632 | ]
633 |
634 | [[package]]
635 | name = "hyper-rustls"
636 | version = "0.27.2"
637 | source = "registry+https://github.com/rust-lang/crates.io-index"
638 | checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
639 | dependencies = [
640 | "futures-util",
641 | "http 1.1.0",
642 | "hyper 1.4.1",
643 | "hyper-util",
644 | "rustls",
645 | "rustls-pki-types",
646 | "tokio",
647 | "tokio-rustls",
648 | "tower-service",
649 | "webpki-roots",
650 | ]
651 |
652 | [[package]]
653 | name = "hyper-tls"
654 | version = "0.6.0"
655 | source = "registry+https://github.com/rust-lang/crates.io-index"
656 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
657 | dependencies = [
658 | "bytes",
659 | "http-body-util",
660 | "hyper 1.4.1",
661 | "hyper-util",
662 | "native-tls",
663 | "tokio",
664 | "tokio-native-tls",
665 | "tower-service",
666 | ]
667 |
668 | [[package]]
669 | name = "hyper-util"
670 | version = "0.1.6"
671 | source = "registry+https://github.com/rust-lang/crates.io-index"
672 | checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
673 | dependencies = [
674 | "bytes",
675 | "futures-channel",
676 | "futures-util",
677 | "http 1.1.0",
678 | "http-body 1.0.0",
679 | "hyper 1.4.1",
680 | "pin-project-lite",
681 | "socket2",
682 | "tokio",
683 | "tower",
684 | "tower-service",
685 | "tracing",
686 | ]
687 |
688 | [[package]]
689 | name = "iana-time-zone"
690 | version = "0.1.60"
691 | source = "registry+https://github.com/rust-lang/crates.io-index"
692 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
693 | dependencies = [
694 | "android_system_properties",
695 | "core-foundation-sys",
696 | "iana-time-zone-haiku",
697 | "js-sys",
698 | "wasm-bindgen",
699 | "windows-core",
700 | ]
701 |
702 | [[package]]
703 | name = "iana-time-zone-haiku"
704 | version = "0.1.2"
705 | source = "registry+https://github.com/rust-lang/crates.io-index"
706 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
707 | dependencies = [
708 | "cc",
709 | ]
710 |
711 | [[package]]
712 | name = "idna"
713 | version = "0.5.0"
714 | source = "registry+https://github.com/rust-lang/crates.io-index"
715 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
716 | dependencies = [
717 | "unicode-bidi",
718 | "unicode-normalization",
719 | ]
720 |
721 | [[package]]
722 | name = "indexmap"
723 | version = "2.2.6"
724 | source = "registry+https://github.com/rust-lang/crates.io-index"
725 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
726 | dependencies = [
727 | "equivalent",
728 | "hashbrown",
729 | ]
730 |
731 | [[package]]
732 | name = "ipnet"
733 | version = "2.9.0"
734 | source = "registry+https://github.com/rust-lang/crates.io-index"
735 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
736 |
737 | [[package]]
738 | name = "is_terminal_polyfill"
739 | version = "1.70.0"
740 | source = "registry+https://github.com/rust-lang/crates.io-index"
741 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
742 |
743 | [[package]]
744 | name = "iter-read"
745 | version = "1.0.1"
746 | source = "registry+https://github.com/rust-lang/crates.io-index"
747 | checksum = "a598c1abae8e3456ebda517868b254b6bc2a9bb6501ffd5b9d0875bf332e048b"
748 |
749 | [[package]]
750 | name = "itoa"
751 | version = "1.0.11"
752 | source = "registry+https://github.com/rust-lang/crates.io-index"
753 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
754 |
755 | [[package]]
756 | name = "js-sys"
757 | version = "0.3.69"
758 | source = "registry+https://github.com/rust-lang/crates.io-index"
759 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
760 | dependencies = [
761 | "wasm-bindgen",
762 | ]
763 |
764 | [[package]]
765 | name = "lazy_static"
766 | version = "1.5.0"
767 | source = "registry+https://github.com/rust-lang/crates.io-index"
768 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
769 |
770 | [[package]]
771 | name = "libc"
772 | version = "0.2.155"
773 | source = "registry+https://github.com/rust-lang/crates.io-index"
774 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
775 |
776 | [[package]]
777 | name = "linux-raw-sys"
778 | version = "0.4.13"
779 | source = "registry+https://github.com/rust-lang/crates.io-index"
780 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
781 |
782 | [[package]]
783 | name = "lock_api"
784 | version = "0.4.12"
785 | source = "registry+https://github.com/rust-lang/crates.io-index"
786 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
787 | dependencies = [
788 | "autocfg",
789 | "scopeguard",
790 | ]
791 |
792 | [[package]]
793 | name = "log"
794 | version = "0.4.22"
795 | source = "registry+https://github.com/rust-lang/crates.io-index"
796 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
797 |
798 | [[package]]
799 | name = "md5"
800 | version = "0.7.0"
801 | source = "registry+https://github.com/rust-lang/crates.io-index"
802 | checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
803 |
804 | [[package]]
805 | name = "memchr"
806 | version = "2.7.4"
807 | source = "registry+https://github.com/rust-lang/crates.io-index"
808 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
809 |
810 | [[package]]
811 | name = "mime"
812 | version = "0.3.17"
813 | source = "registry+https://github.com/rust-lang/crates.io-index"
814 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
815 |
816 | [[package]]
817 | name = "miniz_oxide"
818 | version = "0.7.4"
819 | source = "registry+https://github.com/rust-lang/crates.io-index"
820 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
821 | dependencies = [
822 | "adler",
823 | ]
824 |
825 | [[package]]
826 | name = "mio"
827 | version = "1.0.1"
828 | source = "registry+https://github.com/rust-lang/crates.io-index"
829 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
830 | dependencies = [
831 | "hermit-abi",
832 | "libc",
833 | "wasi",
834 | "windows-sys 0.52.0",
835 | ]
836 |
837 | [[package]]
838 | name = "mockito"
839 | version = "1.4.0"
840 | source = "registry+https://github.com/rust-lang/crates.io-index"
841 | checksum = "d2f6e023aa5bdf392aa06c78e4a4e6d498baab5138d0c993503350ebbc37bf1e"
842 | dependencies = [
843 | "assert-json-diff",
844 | "colored",
845 | "futures-core",
846 | "hyper 0.14.30",
847 | "log",
848 | "rand",
849 | "regex",
850 | "serde_json",
851 | "serde_urlencoded",
852 | "similar",
853 | "tokio",
854 | ]
855 |
856 | [[package]]
857 | name = "native-tls"
858 | version = "0.2.11"
859 | source = "registry+https://github.com/rust-lang/crates.io-index"
860 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
861 | dependencies = [
862 | "lazy_static",
863 | "libc",
864 | "log",
865 | "openssl",
866 | "openssl-probe",
867 | "openssl-sys",
868 | "schannel",
869 | "security-framework",
870 | "security-framework-sys",
871 | "tempfile",
872 | ]
873 |
874 | [[package]]
875 | name = "num-traits"
876 | version = "0.2.19"
877 | source = "registry+https://github.com/rust-lang/crates.io-index"
878 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
879 | dependencies = [
880 | "autocfg",
881 | ]
882 |
883 | [[package]]
884 | name = "object"
885 | version = "0.36.1"
886 | source = "registry+https://github.com/rust-lang/crates.io-index"
887 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
888 | dependencies = [
889 | "memchr",
890 | ]
891 |
892 | [[package]]
893 | name = "once_cell"
894 | version = "1.19.0"
895 | source = "registry+https://github.com/rust-lang/crates.io-index"
896 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
897 |
898 | [[package]]
899 | name = "openssl"
900 | version = "0.10.63"
901 | source = "registry+https://github.com/rust-lang/crates.io-index"
902 | checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
903 | dependencies = [
904 | "bitflags 2.6.0",
905 | "cfg-if",
906 | "foreign-types",
907 | "libc",
908 | "once_cell",
909 | "openssl-macros",
910 | "openssl-sys",
911 | ]
912 |
913 | [[package]]
914 | name = "openssl-macros"
915 | version = "0.1.1"
916 | source = "registry+https://github.com/rust-lang/crates.io-index"
917 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
918 | dependencies = [
919 | "proc-macro2",
920 | "quote",
921 | "syn",
922 | ]
923 |
924 | [[package]]
925 | name = "openssl-probe"
926 | version = "0.1.5"
927 | source = "registry+https://github.com/rust-lang/crates.io-index"
928 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
929 |
930 | [[package]]
931 | name = "openssl-sys"
932 | version = "0.9.99"
933 | source = "registry+https://github.com/rust-lang/crates.io-index"
934 | checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
935 | dependencies = [
936 | "cc",
937 | "libc",
938 | "pkg-config",
939 | "vcpkg",
940 | ]
941 |
942 | [[package]]
943 | name = "parking_lot"
944 | version = "0.12.3"
945 | source = "registry+https://github.com/rust-lang/crates.io-index"
946 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
947 | dependencies = [
948 | "lock_api",
949 | "parking_lot_core",
950 | ]
951 |
952 | [[package]]
953 | name = "parking_lot_core"
954 | version = "0.9.10"
955 | source = "registry+https://github.com/rust-lang/crates.io-index"
956 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
957 | dependencies = [
958 | "cfg-if",
959 | "libc",
960 | "redox_syscall",
961 | "smallvec",
962 | "windows-targets 0.52.6",
963 | ]
964 |
965 | [[package]]
966 | name = "percent-encoding"
967 | version = "2.3.1"
968 | source = "registry+https://github.com/rust-lang/crates.io-index"
969 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
970 |
971 | [[package]]
972 | name = "pin-project"
973 | version = "1.1.5"
974 | source = "registry+https://github.com/rust-lang/crates.io-index"
975 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
976 | dependencies = [
977 | "pin-project-internal",
978 | ]
979 |
980 | [[package]]
981 | name = "pin-project-internal"
982 | version = "1.1.5"
983 | source = "registry+https://github.com/rust-lang/crates.io-index"
984 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
985 | dependencies = [
986 | "proc-macro2",
987 | "quote",
988 | "syn",
989 | ]
990 |
991 | [[package]]
992 | name = "pin-project-lite"
993 | version = "0.2.14"
994 | source = "registry+https://github.com/rust-lang/crates.io-index"
995 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
996 |
997 | [[package]]
998 | name = "pin-utils"
999 | version = "0.1.0"
1000 | source = "registry+https://github.com/rust-lang/crates.io-index"
1001 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
1002 |
1003 | [[package]]
1004 | name = "pkg-config"
1005 | version = "0.3.29"
1006 | source = "registry+https://github.com/rust-lang/crates.io-index"
1007 | checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
1008 |
1009 | [[package]]
1010 | name = "ppv-lite86"
1011 | version = "0.2.17"
1012 | source = "registry+https://github.com/rust-lang/crates.io-index"
1013 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
1014 |
1015 | [[package]]
1016 | name = "proc-macro2"
1017 | version = "1.0.86"
1018 | source = "registry+https://github.com/rust-lang/crates.io-index"
1019 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
1020 | dependencies = [
1021 | "unicode-ident",
1022 | ]
1023 |
1024 | [[package]]
1025 | name = "quinn"
1026 | version = "0.11.2"
1027 | source = "registry+https://github.com/rust-lang/crates.io-index"
1028 | checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad"
1029 | dependencies = [
1030 | "bytes",
1031 | "pin-project-lite",
1032 | "quinn-proto",
1033 | "quinn-udp",
1034 | "rustc-hash",
1035 | "rustls",
1036 | "thiserror",
1037 | "tokio",
1038 | "tracing",
1039 | ]
1040 |
1041 | [[package]]
1042 | name = "quinn-proto"
1043 | version = "0.11.3"
1044 | source = "registry+https://github.com/rust-lang/crates.io-index"
1045 | checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe"
1046 | dependencies = [
1047 | "bytes",
1048 | "rand",
1049 | "ring",
1050 | "rustc-hash",
1051 | "rustls",
1052 | "slab",
1053 | "thiserror",
1054 | "tinyvec",
1055 | "tracing",
1056 | ]
1057 |
1058 | [[package]]
1059 | name = "quinn-udp"
1060 | version = "0.5.2"
1061 | source = "registry+https://github.com/rust-lang/crates.io-index"
1062 | checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46"
1063 | dependencies = [
1064 | "libc",
1065 | "once_cell",
1066 | "socket2",
1067 | "tracing",
1068 | "windows-sys 0.52.0",
1069 | ]
1070 |
1071 | [[package]]
1072 | name = "quote"
1073 | version = "1.0.36"
1074 | source = "registry+https://github.com/rust-lang/crates.io-index"
1075 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
1076 | dependencies = [
1077 | "proc-macro2",
1078 | ]
1079 |
1080 | [[package]]
1081 | name = "rand"
1082 | version = "0.8.5"
1083 | source = "registry+https://github.com/rust-lang/crates.io-index"
1084 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
1085 | dependencies = [
1086 | "libc",
1087 | "rand_chacha",
1088 | "rand_core",
1089 | ]
1090 |
1091 | [[package]]
1092 | name = "rand_chacha"
1093 | version = "0.3.1"
1094 | source = "registry+https://github.com/rust-lang/crates.io-index"
1095 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1096 | dependencies = [
1097 | "ppv-lite86",
1098 | "rand_core",
1099 | ]
1100 |
1101 | [[package]]
1102 | name = "rand_core"
1103 | version = "0.6.4"
1104 | source = "registry+https://github.com/rust-lang/crates.io-index"
1105 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
1106 | dependencies = [
1107 | "getrandom",
1108 | ]
1109 |
1110 | [[package]]
1111 | name = "rayon"
1112 | version = "1.10.0"
1113 | source = "registry+https://github.com/rust-lang/crates.io-index"
1114 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
1115 | dependencies = [
1116 | "either",
1117 | "rayon-core",
1118 | ]
1119 |
1120 | [[package]]
1121 | name = "rayon-core"
1122 | version = "1.12.1"
1123 | source = "registry+https://github.com/rust-lang/crates.io-index"
1124 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
1125 | dependencies = [
1126 | "crossbeam-deque",
1127 | "crossbeam-utils",
1128 | ]
1129 |
1130 | [[package]]
1131 | name = "redox_syscall"
1132 | version = "0.5.3"
1133 | source = "registry+https://github.com/rust-lang/crates.io-index"
1134 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
1135 | dependencies = [
1136 | "bitflags 2.6.0",
1137 | ]
1138 |
1139 | [[package]]
1140 | name = "regex"
1141 | version = "1.10.5"
1142 | source = "registry+https://github.com/rust-lang/crates.io-index"
1143 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
1144 | dependencies = [
1145 | "aho-corasick",
1146 | "memchr",
1147 | "regex-automata",
1148 | "regex-syntax",
1149 | ]
1150 |
1151 | [[package]]
1152 | name = "regex-automata"
1153 | version = "0.4.7"
1154 | source = "registry+https://github.com/rust-lang/crates.io-index"
1155 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
1156 | dependencies = [
1157 | "aho-corasick",
1158 | "memchr",
1159 | "regex-syntax",
1160 | ]
1161 |
1162 | [[package]]
1163 | name = "regex-syntax"
1164 | version = "0.8.4"
1165 | source = "registry+https://github.com/rust-lang/crates.io-index"
1166 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
1167 |
1168 | [[package]]
1169 | name = "reqwest"
1170 | version = "0.12.5"
1171 | source = "registry+https://github.com/rust-lang/crates.io-index"
1172 | checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
1173 | dependencies = [
1174 | "base64",
1175 | "bytes",
1176 | "encoding_rs",
1177 | "futures-channel",
1178 | "futures-core",
1179 | "futures-util",
1180 | "h2 0.4.5",
1181 | "http 1.1.0",
1182 | "http-body 1.0.0",
1183 | "http-body-util",
1184 | "hyper 1.4.1",
1185 | "hyper-rustls",
1186 | "hyper-tls",
1187 | "hyper-util",
1188 | "ipnet",
1189 | "js-sys",
1190 | "log",
1191 | "mime",
1192 | "native-tls",
1193 | "once_cell",
1194 | "percent-encoding",
1195 | "pin-project-lite",
1196 | "quinn",
1197 | "rustls",
1198 | "rustls-pemfile",
1199 | "rustls-pki-types",
1200 | "serde",
1201 | "serde_json",
1202 | "serde_urlencoded",
1203 | "sync_wrapper",
1204 | "system-configuration",
1205 | "tokio",
1206 | "tokio-native-tls",
1207 | "tokio-rustls",
1208 | "tower-service",
1209 | "url",
1210 | "wasm-bindgen",
1211 | "wasm-bindgen-futures",
1212 | "web-sys",
1213 | "webpki-roots",
1214 | "winreg",
1215 | ]
1216 |
1217 | [[package]]
1218 | name = "ring"
1219 | version = "0.17.7"
1220 | source = "registry+https://github.com/rust-lang/crates.io-index"
1221 | checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
1222 | dependencies = [
1223 | "cc",
1224 | "getrandom",
1225 | "libc",
1226 | "spin",
1227 | "untrusted",
1228 | "windows-sys 0.48.0",
1229 | ]
1230 |
1231 | [[package]]
1232 | name = "roxmltree"
1233 | version = "0.20.0"
1234 | source = "registry+https://github.com/rust-lang/crates.io-index"
1235 | checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
1236 |
1237 | [[package]]
1238 | name = "rustc-demangle"
1239 | version = "0.1.24"
1240 | source = "registry+https://github.com/rust-lang/crates.io-index"
1241 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
1242 |
1243 | [[package]]
1244 | name = "rustc-hash"
1245 | version = "1.1.0"
1246 | source = "registry+https://github.com/rust-lang/crates.io-index"
1247 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
1248 |
1249 | [[package]]
1250 | name = "rustix"
1251 | version = "0.38.31"
1252 | source = "registry+https://github.com/rust-lang/crates.io-index"
1253 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
1254 | dependencies = [
1255 | "bitflags 2.6.0",
1256 | "errno",
1257 | "libc",
1258 | "linux-raw-sys",
1259 | "windows-sys 0.52.0",
1260 | ]
1261 |
1262 | [[package]]
1263 | name = "rustls"
1264 | version = "0.23.11"
1265 | source = "registry+https://github.com/rust-lang/crates.io-index"
1266 | checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0"
1267 | dependencies = [
1268 | "once_cell",
1269 | "ring",
1270 | "rustls-pki-types",
1271 | "rustls-webpki",
1272 | "subtle",
1273 | "zeroize",
1274 | ]
1275 |
1276 | [[package]]
1277 | name = "rustls-pemfile"
1278 | version = "2.1.2"
1279 | source = "registry+https://github.com/rust-lang/crates.io-index"
1280 | checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
1281 | dependencies = [
1282 | "base64",
1283 | "rustls-pki-types",
1284 | ]
1285 |
1286 | [[package]]
1287 | name = "rustls-pki-types"
1288 | version = "1.7.0"
1289 | source = "registry+https://github.com/rust-lang/crates.io-index"
1290 | checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
1291 |
1292 | [[package]]
1293 | name = "rustls-webpki"
1294 | version = "0.102.5"
1295 | source = "registry+https://github.com/rust-lang/crates.io-index"
1296 | checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78"
1297 | dependencies = [
1298 | "ring",
1299 | "rustls-pki-types",
1300 | "untrusted",
1301 | ]
1302 |
1303 | [[package]]
1304 | name = "ryu"
1305 | version = "1.0.18"
1306 | source = "registry+https://github.com/rust-lang/crates.io-index"
1307 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
1308 |
1309 | [[package]]
1310 | name = "schannel"
1311 | version = "0.1.23"
1312 | source = "registry+https://github.com/rust-lang/crates.io-index"
1313 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
1314 | dependencies = [
1315 | "windows-sys 0.52.0",
1316 | ]
1317 |
1318 | [[package]]
1319 | name = "scopeguard"
1320 | version = "1.2.0"
1321 | source = "registry+https://github.com/rust-lang/crates.io-index"
1322 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1323 |
1324 | [[package]]
1325 | name = "security-framework"
1326 | version = "2.9.2"
1327 | source = "registry+https://github.com/rust-lang/crates.io-index"
1328 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
1329 | dependencies = [
1330 | "bitflags 1.3.2",
1331 | "core-foundation",
1332 | "core-foundation-sys",
1333 | "libc",
1334 | "security-framework-sys",
1335 | ]
1336 |
1337 | [[package]]
1338 | name = "security-framework-sys"
1339 | version = "2.9.1"
1340 | source = "registry+https://github.com/rust-lang/crates.io-index"
1341 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
1342 | dependencies = [
1343 | "core-foundation-sys",
1344 | "libc",
1345 | ]
1346 |
1347 | [[package]]
1348 | name = "serde"
1349 | version = "1.0.204"
1350 | source = "registry+https://github.com/rust-lang/crates.io-index"
1351 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
1352 | dependencies = [
1353 | "serde_derive",
1354 | ]
1355 |
1356 | [[package]]
1357 | name = "serde_derive"
1358 | version = "1.0.204"
1359 | source = "registry+https://github.com/rust-lang/crates.io-index"
1360 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
1361 | dependencies = [
1362 | "proc-macro2",
1363 | "quote",
1364 | "syn",
1365 | ]
1366 |
1367 | [[package]]
1368 | name = "serde_json"
1369 | version = "1.0.120"
1370 | source = "registry+https://github.com/rust-lang/crates.io-index"
1371 | checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
1372 | dependencies = [
1373 | "itoa",
1374 | "ryu",
1375 | "serde",
1376 | ]
1377 |
1378 | [[package]]
1379 | name = "serde_urlencoded"
1380 | version = "0.7.1"
1381 | source = "registry+https://github.com/rust-lang/crates.io-index"
1382 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
1383 | dependencies = [
1384 | "form_urlencoded",
1385 | "itoa",
1386 | "ryu",
1387 | "serde",
1388 | ]
1389 |
1390 | [[package]]
1391 | name = "similar"
1392 | version = "2.6.0"
1393 | source = "registry+https://github.com/rust-lang/crates.io-index"
1394 | checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
1395 |
1396 | [[package]]
1397 | name = "slab"
1398 | version = "0.4.9"
1399 | source = "registry+https://github.com/rust-lang/crates.io-index"
1400 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
1401 | dependencies = [
1402 | "autocfg",
1403 | ]
1404 |
1405 | [[package]]
1406 | name = "smallvec"
1407 | version = "1.13.2"
1408 | source = "registry+https://github.com/rust-lang/crates.io-index"
1409 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
1410 |
1411 | [[package]]
1412 | name = "socket2"
1413 | version = "0.5.7"
1414 | source = "registry+https://github.com/rust-lang/crates.io-index"
1415 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
1416 | dependencies = [
1417 | "libc",
1418 | "windows-sys 0.52.0",
1419 | ]
1420 |
1421 | [[package]]
1422 | name = "speedtest-rs"
1423 | version = "0.2.0"
1424 | dependencies = [
1425 | "chrono",
1426 | "clap",
1427 | "csv",
1428 | "env_logger",
1429 | "iter-read",
1430 | "log",
1431 | "md5",
1432 | "mockito",
1433 | "rayon",
1434 | "reqwest",
1435 | "roxmltree",
1436 | "serde",
1437 | "url",
1438 | ]
1439 |
1440 | [[package]]
1441 | name = "spin"
1442 | version = "0.9.8"
1443 | source = "registry+https://github.com/rust-lang/crates.io-index"
1444 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
1445 |
1446 | [[package]]
1447 | name = "strsim"
1448 | version = "0.11.1"
1449 | source = "registry+https://github.com/rust-lang/crates.io-index"
1450 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1451 |
1452 | [[package]]
1453 | name = "subtle"
1454 | version = "2.6.1"
1455 | source = "registry+https://github.com/rust-lang/crates.io-index"
1456 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
1457 |
1458 | [[package]]
1459 | name = "syn"
1460 | version = "2.0.72"
1461 | source = "registry+https://github.com/rust-lang/crates.io-index"
1462 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
1463 | dependencies = [
1464 | "proc-macro2",
1465 | "quote",
1466 | "unicode-ident",
1467 | ]
1468 |
1469 | [[package]]
1470 | name = "sync_wrapper"
1471 | version = "1.0.1"
1472 | source = "registry+https://github.com/rust-lang/crates.io-index"
1473 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
1474 |
1475 | [[package]]
1476 | name = "system-configuration"
1477 | version = "0.5.1"
1478 | source = "registry+https://github.com/rust-lang/crates.io-index"
1479 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
1480 | dependencies = [
1481 | "bitflags 1.3.2",
1482 | "core-foundation",
1483 | "system-configuration-sys",
1484 | ]
1485 |
1486 | [[package]]
1487 | name = "system-configuration-sys"
1488 | version = "0.5.0"
1489 | source = "registry+https://github.com/rust-lang/crates.io-index"
1490 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
1491 | dependencies = [
1492 | "core-foundation-sys",
1493 | "libc",
1494 | ]
1495 |
1496 | [[package]]
1497 | name = "tempfile"
1498 | version = "3.10.0"
1499 | source = "registry+https://github.com/rust-lang/crates.io-index"
1500 | checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
1501 | dependencies = [
1502 | "cfg-if",
1503 | "fastrand",
1504 | "rustix",
1505 | "windows-sys 0.52.0",
1506 | ]
1507 |
1508 | [[package]]
1509 | name = "thiserror"
1510 | version = "1.0.62"
1511 | source = "registry+https://github.com/rust-lang/crates.io-index"
1512 | checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
1513 | dependencies = [
1514 | "thiserror-impl",
1515 | ]
1516 |
1517 | [[package]]
1518 | name = "thiserror-impl"
1519 | version = "1.0.62"
1520 | source = "registry+https://github.com/rust-lang/crates.io-index"
1521 | checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
1522 | dependencies = [
1523 | "proc-macro2",
1524 | "quote",
1525 | "syn",
1526 | ]
1527 |
1528 | [[package]]
1529 | name = "tinyvec"
1530 | version = "1.8.0"
1531 | source = "registry+https://github.com/rust-lang/crates.io-index"
1532 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
1533 | dependencies = [
1534 | "tinyvec_macros",
1535 | ]
1536 |
1537 | [[package]]
1538 | name = "tinyvec_macros"
1539 | version = "0.1.1"
1540 | source = "registry+https://github.com/rust-lang/crates.io-index"
1541 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1542 |
1543 | [[package]]
1544 | name = "tokio"
1545 | version = "1.39.1"
1546 | source = "registry+https://github.com/rust-lang/crates.io-index"
1547 | checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a"
1548 | dependencies = [
1549 | "backtrace",
1550 | "bytes",
1551 | "libc",
1552 | "mio",
1553 | "parking_lot",
1554 | "pin-project-lite",
1555 | "socket2",
1556 | "windows-sys 0.52.0",
1557 | ]
1558 |
1559 | [[package]]
1560 | name = "tokio-native-tls"
1561 | version = "0.3.1"
1562 | source = "registry+https://github.com/rust-lang/crates.io-index"
1563 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
1564 | dependencies = [
1565 | "native-tls",
1566 | "tokio",
1567 | ]
1568 |
1569 | [[package]]
1570 | name = "tokio-rustls"
1571 | version = "0.26.0"
1572 | source = "registry+https://github.com/rust-lang/crates.io-index"
1573 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
1574 | dependencies = [
1575 | "rustls",
1576 | "rustls-pki-types",
1577 | "tokio",
1578 | ]
1579 |
1580 | [[package]]
1581 | name = "tokio-util"
1582 | version = "0.7.11"
1583 | source = "registry+https://github.com/rust-lang/crates.io-index"
1584 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
1585 | dependencies = [
1586 | "bytes",
1587 | "futures-core",
1588 | "futures-sink",
1589 | "pin-project-lite",
1590 | "tokio",
1591 | ]
1592 |
1593 | [[package]]
1594 | name = "tower"
1595 | version = "0.4.13"
1596 | source = "registry+https://github.com/rust-lang/crates.io-index"
1597 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
1598 | dependencies = [
1599 | "futures-core",
1600 | "futures-util",
1601 | "pin-project",
1602 | "pin-project-lite",
1603 | "tokio",
1604 | "tower-layer",
1605 | "tower-service",
1606 | ]
1607 |
1608 | [[package]]
1609 | name = "tower-layer"
1610 | version = "0.3.2"
1611 | source = "registry+https://github.com/rust-lang/crates.io-index"
1612 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
1613 |
1614 | [[package]]
1615 | name = "tower-service"
1616 | version = "0.3.2"
1617 | source = "registry+https://github.com/rust-lang/crates.io-index"
1618 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
1619 |
1620 | [[package]]
1621 | name = "tracing"
1622 | version = "0.1.40"
1623 | source = "registry+https://github.com/rust-lang/crates.io-index"
1624 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
1625 | dependencies = [
1626 | "pin-project-lite",
1627 | "tracing-attributes",
1628 | "tracing-core",
1629 | ]
1630 |
1631 | [[package]]
1632 | name = "tracing-attributes"
1633 | version = "0.1.27"
1634 | source = "registry+https://github.com/rust-lang/crates.io-index"
1635 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
1636 | dependencies = [
1637 | "proc-macro2",
1638 | "quote",
1639 | "syn",
1640 | ]
1641 |
1642 | [[package]]
1643 | name = "tracing-core"
1644 | version = "0.1.32"
1645 | source = "registry+https://github.com/rust-lang/crates.io-index"
1646 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
1647 | dependencies = [
1648 | "once_cell",
1649 | ]
1650 |
1651 | [[package]]
1652 | name = "try-lock"
1653 | version = "0.2.5"
1654 | source = "registry+https://github.com/rust-lang/crates.io-index"
1655 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
1656 |
1657 | [[package]]
1658 | name = "unicode-bidi"
1659 | version = "0.3.15"
1660 | source = "registry+https://github.com/rust-lang/crates.io-index"
1661 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
1662 |
1663 | [[package]]
1664 | name = "unicode-ident"
1665 | version = "1.0.12"
1666 | source = "registry+https://github.com/rust-lang/crates.io-index"
1667 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
1668 |
1669 | [[package]]
1670 | name = "unicode-normalization"
1671 | version = "0.1.23"
1672 | source = "registry+https://github.com/rust-lang/crates.io-index"
1673 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
1674 | dependencies = [
1675 | "tinyvec",
1676 | ]
1677 |
1678 | [[package]]
1679 | name = "untrusted"
1680 | version = "0.9.0"
1681 | source = "registry+https://github.com/rust-lang/crates.io-index"
1682 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1683 |
1684 | [[package]]
1685 | name = "url"
1686 | version = "2.5.2"
1687 | source = "registry+https://github.com/rust-lang/crates.io-index"
1688 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
1689 | dependencies = [
1690 | "form_urlencoded",
1691 | "idna",
1692 | "percent-encoding",
1693 | ]
1694 |
1695 | [[package]]
1696 | name = "utf8parse"
1697 | version = "0.2.2"
1698 | source = "registry+https://github.com/rust-lang/crates.io-index"
1699 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1700 |
1701 | [[package]]
1702 | name = "vcpkg"
1703 | version = "0.2.15"
1704 | source = "registry+https://github.com/rust-lang/crates.io-index"
1705 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1706 |
1707 | [[package]]
1708 | name = "want"
1709 | version = "0.3.1"
1710 | source = "registry+https://github.com/rust-lang/crates.io-index"
1711 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
1712 | dependencies = [
1713 | "try-lock",
1714 | ]
1715 |
1716 | [[package]]
1717 | name = "wasi"
1718 | version = "0.11.0+wasi-snapshot-preview1"
1719 | source = "registry+https://github.com/rust-lang/crates.io-index"
1720 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1721 |
1722 | [[package]]
1723 | name = "wasm-bindgen"
1724 | version = "0.2.92"
1725 | source = "registry+https://github.com/rust-lang/crates.io-index"
1726 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
1727 | dependencies = [
1728 | "cfg-if",
1729 | "wasm-bindgen-macro",
1730 | ]
1731 |
1732 | [[package]]
1733 | name = "wasm-bindgen-backend"
1734 | version = "0.2.92"
1735 | source = "registry+https://github.com/rust-lang/crates.io-index"
1736 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
1737 | dependencies = [
1738 | "bumpalo",
1739 | "log",
1740 | "once_cell",
1741 | "proc-macro2",
1742 | "quote",
1743 | "syn",
1744 | "wasm-bindgen-shared",
1745 | ]
1746 |
1747 | [[package]]
1748 | name = "wasm-bindgen-futures"
1749 | version = "0.4.41"
1750 | source = "registry+https://github.com/rust-lang/crates.io-index"
1751 | checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97"
1752 | dependencies = [
1753 | "cfg-if",
1754 | "js-sys",
1755 | "wasm-bindgen",
1756 | "web-sys",
1757 | ]
1758 |
1759 | [[package]]
1760 | name = "wasm-bindgen-macro"
1761 | version = "0.2.92"
1762 | source = "registry+https://github.com/rust-lang/crates.io-index"
1763 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
1764 | dependencies = [
1765 | "quote",
1766 | "wasm-bindgen-macro-support",
1767 | ]
1768 |
1769 | [[package]]
1770 | name = "wasm-bindgen-macro-support"
1771 | version = "0.2.92"
1772 | source = "registry+https://github.com/rust-lang/crates.io-index"
1773 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
1774 | dependencies = [
1775 | "proc-macro2",
1776 | "quote",
1777 | "syn",
1778 | "wasm-bindgen-backend",
1779 | "wasm-bindgen-shared",
1780 | ]
1781 |
1782 | [[package]]
1783 | name = "wasm-bindgen-shared"
1784 | version = "0.2.92"
1785 | source = "registry+https://github.com/rust-lang/crates.io-index"
1786 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
1787 |
1788 | [[package]]
1789 | name = "web-sys"
1790 | version = "0.3.68"
1791 | source = "registry+https://github.com/rust-lang/crates.io-index"
1792 | checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446"
1793 | dependencies = [
1794 | "js-sys",
1795 | "wasm-bindgen",
1796 | ]
1797 |
1798 | [[package]]
1799 | name = "webpki-roots"
1800 | version = "0.26.3"
1801 | source = "registry+https://github.com/rust-lang/crates.io-index"
1802 | checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
1803 | dependencies = [
1804 | "rustls-pki-types",
1805 | ]
1806 |
1807 | [[package]]
1808 | name = "windows-core"
1809 | version = "0.52.0"
1810 | source = "registry+https://github.com/rust-lang/crates.io-index"
1811 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
1812 | dependencies = [
1813 | "windows-targets 0.52.6",
1814 | ]
1815 |
1816 | [[package]]
1817 | name = "windows-sys"
1818 | version = "0.48.0"
1819 | source = "registry+https://github.com/rust-lang/crates.io-index"
1820 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1821 | dependencies = [
1822 | "windows-targets 0.48.5",
1823 | ]
1824 |
1825 | [[package]]
1826 | name = "windows-sys"
1827 | version = "0.52.0"
1828 | source = "registry+https://github.com/rust-lang/crates.io-index"
1829 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1830 | dependencies = [
1831 | "windows-targets 0.52.6",
1832 | ]
1833 |
1834 | [[package]]
1835 | name = "windows-targets"
1836 | version = "0.48.5"
1837 | source = "registry+https://github.com/rust-lang/crates.io-index"
1838 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1839 | dependencies = [
1840 | "windows_aarch64_gnullvm 0.48.5",
1841 | "windows_aarch64_msvc 0.48.5",
1842 | "windows_i686_gnu 0.48.5",
1843 | "windows_i686_msvc 0.48.5",
1844 | "windows_x86_64_gnu 0.48.5",
1845 | "windows_x86_64_gnullvm 0.48.5",
1846 | "windows_x86_64_msvc 0.48.5",
1847 | ]
1848 |
1849 | [[package]]
1850 | name = "windows-targets"
1851 | version = "0.52.6"
1852 | source = "registry+https://github.com/rust-lang/crates.io-index"
1853 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1854 | dependencies = [
1855 | "windows_aarch64_gnullvm 0.52.6",
1856 | "windows_aarch64_msvc 0.52.6",
1857 | "windows_i686_gnu 0.52.6",
1858 | "windows_i686_gnullvm",
1859 | "windows_i686_msvc 0.52.6",
1860 | "windows_x86_64_gnu 0.52.6",
1861 | "windows_x86_64_gnullvm 0.52.6",
1862 | "windows_x86_64_msvc 0.52.6",
1863 | ]
1864 |
1865 | [[package]]
1866 | name = "windows_aarch64_gnullvm"
1867 | version = "0.48.5"
1868 | source = "registry+https://github.com/rust-lang/crates.io-index"
1869 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1870 |
1871 | [[package]]
1872 | name = "windows_aarch64_gnullvm"
1873 | version = "0.52.6"
1874 | source = "registry+https://github.com/rust-lang/crates.io-index"
1875 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1876 |
1877 | [[package]]
1878 | name = "windows_aarch64_msvc"
1879 | version = "0.48.5"
1880 | source = "registry+https://github.com/rust-lang/crates.io-index"
1881 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1882 |
1883 | [[package]]
1884 | name = "windows_aarch64_msvc"
1885 | version = "0.52.6"
1886 | source = "registry+https://github.com/rust-lang/crates.io-index"
1887 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1888 |
1889 | [[package]]
1890 | name = "windows_i686_gnu"
1891 | version = "0.48.5"
1892 | source = "registry+https://github.com/rust-lang/crates.io-index"
1893 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1894 |
1895 | [[package]]
1896 | name = "windows_i686_gnu"
1897 | version = "0.52.6"
1898 | source = "registry+https://github.com/rust-lang/crates.io-index"
1899 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1900 |
1901 | [[package]]
1902 | name = "windows_i686_gnullvm"
1903 | version = "0.52.6"
1904 | source = "registry+https://github.com/rust-lang/crates.io-index"
1905 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1906 |
1907 | [[package]]
1908 | name = "windows_i686_msvc"
1909 | version = "0.48.5"
1910 | source = "registry+https://github.com/rust-lang/crates.io-index"
1911 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1912 |
1913 | [[package]]
1914 | name = "windows_i686_msvc"
1915 | version = "0.52.6"
1916 | source = "registry+https://github.com/rust-lang/crates.io-index"
1917 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1918 |
1919 | [[package]]
1920 | name = "windows_x86_64_gnu"
1921 | version = "0.48.5"
1922 | source = "registry+https://github.com/rust-lang/crates.io-index"
1923 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1924 |
1925 | [[package]]
1926 | name = "windows_x86_64_gnu"
1927 | version = "0.52.6"
1928 | source = "registry+https://github.com/rust-lang/crates.io-index"
1929 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1930 |
1931 | [[package]]
1932 | name = "windows_x86_64_gnullvm"
1933 | version = "0.48.5"
1934 | source = "registry+https://github.com/rust-lang/crates.io-index"
1935 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1936 |
1937 | [[package]]
1938 | name = "windows_x86_64_gnullvm"
1939 | version = "0.52.6"
1940 | source = "registry+https://github.com/rust-lang/crates.io-index"
1941 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1942 |
1943 | [[package]]
1944 | name = "windows_x86_64_msvc"
1945 | version = "0.48.5"
1946 | source = "registry+https://github.com/rust-lang/crates.io-index"
1947 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1948 |
1949 | [[package]]
1950 | name = "windows_x86_64_msvc"
1951 | version = "0.52.6"
1952 | source = "registry+https://github.com/rust-lang/crates.io-index"
1953 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1954 |
1955 | [[package]]
1956 | name = "winreg"
1957 | version = "0.52.0"
1958 | source = "registry+https://github.com/rust-lang/crates.io-index"
1959 | checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
1960 | dependencies = [
1961 | "cfg-if",
1962 | "windows-sys 0.48.0",
1963 | ]
1964 |
1965 | [[package]]
1966 | name = "zeroize"
1967 | version = "1.8.1"
1968 | source = "registry+https://github.com/rust-lang/crates.io-index"
1969 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
1970 |
--------------------------------------------------------------------------------