From e833f6dad4457695721079a07b47a3a64101aeb5 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 11 Jul 2026 12:02:47 +0800 Subject: [PATCH 1/4] fix(s3): harden transport and object handling --- Cargo.lock | 1532 +++++++++++++++++++-------------------- Cargo.toml | 12 +- crates/s3/Cargo.toml | 2 + crates/s3/src/admin.rs | 57 +- crates/s3/src/client.rs | 480 ++++++++++-- 5 files changed, 1230 insertions(+), 853 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02083af..c743700 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -34,15 +34,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -69,9 +69,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arc-swap" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] [[package]] name = "assert-json-diff" @@ -102,15 +111,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.8.12" +version = "1.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" +checksum = "e33f815b73a3899c03b380d543532e5865f230dce9678d108dc10732a8682275" dependencies = [ "aws-credential-types", "aws-runtime", @@ -122,13 +131,14 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", "fastrand", "hex", - "http 1.4.0", - "ring", + "http 1.4.2", + "sha1 0.10.6", "time", "tokio", "tracing", @@ -138,9 +148,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.11" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -150,9 +160,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -160,21 +170,22 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] name = "aws-runtime" -version = "1.5.17" +version = "1.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81b5b2898f6798ad58f484856768bca817e3cd9de0974c24ae0f1113fe88f1b" +checksum = "6c9b9de216a988dd54b754a82a7660cfe14cee4f6782ae4524470972fa0ccb39" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -186,9 +197,12 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "bytes-utils", "fastrand", "http 0.2.12", + "http 1.4.2", "http-body 0.4.6", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", "tracing", @@ -197,10 +211,11 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.119.0" +version = "1.137.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c" +checksum = "c2dd7213994e2ff9382ff100403b78c30d1b74cdfcd8fa9d0d1dc3a94a5c4874" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-sigv4", @@ -209,6 +224,7 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -217,29 +233,31 @@ dependencies = [ "bytes", "fastrand", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", + "http 1.4.2", + "http-body 1.0.1", "lru", "percent-encoding", "regex-lite", - "sha2", + "sha2 0.11.0", "tracing", "url", ] [[package]] name = "aws-sdk-sso" -version = "1.91.0" +version = "1.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee6402a36f27b52fe67661c6732d684b2635152b676aa2babbfb5204f99115d" +checksum = "8c82b3ac19f1431854f7ace3a7531674633e286bfdde21976893bfee36fd493b" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -247,21 +265,24 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.2", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.93.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45a7f750bbd170ee3677671ad782d90b894548f4e4ae168302c57ec9de5cb3e" +checksum = "321000d2b4c5519ee573f73167f612efd7329322d9b26969ad1979f0427f1913" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -269,21 +290,24 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.2", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.95.0" +version = "1.107.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55542378e419558e6b1f398ca70adb0b2088077e79ad9f14eb09441f2f7b2164" +checksum = "3d0d328ba962af23ecfa3c9f23b98d3d35e325fa218d7f13d17a6bf522f8a560" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -292,15 +316,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", + "http 1.4.2", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.7" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -308,16 +333,15 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "bytes", - "crypto-bigint 0.5.5", + "crypto-bigint", "form_urlencoded", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", - "http 1.4.0", + "http 1.4.2", "p256", "percent-encoding", - "ring", - "sha2", + "sha2 0.11.0", "subtle", "time", "tracing", @@ -326,9 +350,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.7" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -337,29 +361,30 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.12" +version = "0.64.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" +checksum = "e9e8e65f4f81fcccdeb6c3eca2af17ac21d421a1786a26a394aecf421d616d3a" dependencies = [ "aws-smithy-http", "aws-smithy-types", "bytes", "crc-fast", "hex", - "http 0.2.12", - "http-body 0.4.6", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", "md-5", "pin-project-lite", - "sha1", - "sha2", + "sha1 0.11.0", + "sha2 0.11.0", "tracing", ] [[package]] name = "aws-smithy-eventstream" -version = "0.60.14" +version = "0.60.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc12f8b310e38cad85cf3bef45ad236f470717393c613266ce0a89512286b650" +checksum = "78d8391e65fcea47c586a22e1a41f173b38615b112b2c6b7a44e80cec3e6b706" dependencies = [ "aws-smithy-types", "bytes", @@ -368,9 +393,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.6" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -379,9 +404,9 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", "percent-encoding", "pin-project-lite", "pin-utils", @@ -390,63 +415,59 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.5" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a" +checksum = "5c3ef8931ad1c98aa6a55b4256f847f3116090819844e0dd41ea682cac5dd2d3" dependencies = [ "aws-smithy-async", "aws-smithy-protocol-test", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", - "h2 0.3.27", - "h2 0.4.13", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", + "h2", + "http 1.4.2", "http-body 1.0.1", - "hyper 0.14.32", - "hyper 1.8.1", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "indexmap", "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.36", + "rustls", "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.61.9" +version = "0.62.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-protocol-test" -version = "0.63.7" +version = "0.63.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01317a9e3c5c06f1af35001ef0c873c1e34e458c20b2ee1eee0fb431e6dbb010" +checksum = "5b227aa94af99a8e5ee52551cc7e3ee30a217019ef99207b6f0b7a1527685941" dependencies = [ "assert-json-diff", "aws-smithy-runtime-api", @@ -463,9 +484,9 @@ dependencies = [ [[package]] name = "aws-smithy-query" -version = "0.60.9" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" dependencies = [ "aws-smithy-types", "urlencoding", @@ -473,22 +494,24 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.5" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a392db6c583ea4a912538afb86b7be7c5d8887d91604f50eb55c262ee1b4a5f5" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.2", "http-body 0.4.6", "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", "tokio", @@ -497,33 +520,56 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.9.3" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0d43d899f9e508300e587bf582ba54c27a452dd0a9ea294690669138ae14a2" +checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.0", + "http 1.4.2", "pin-project-lite", "tokio", "tracing", "zeroize", ] +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.2", +] + [[package]] name = "aws-smithy-types" -version = "1.3.5" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905cb13a9895626d49cf2ced759b062d913834c7482c38e49557eac4e6193f01" +checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85" dependencies = [ "base64-simd", "bytes", "bytes-utils", "futures-core", "http 0.2.12", - "http 1.4.0", + "http 1.4.2", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -540,22 +586,23 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.13" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.11" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -563,9 +610,9 @@ dependencies = [ [[package]] name = "base16ct" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" @@ -591,9 +638,15 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.10.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -604,6 +657,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bs58" version = "0.5.1" @@ -615,15 +677,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytes-utils" @@ -656,14 +718,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -678,11 +740,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "num-traits", ] @@ -716,9 +789,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -726,9 +799,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -738,18 +811,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.65" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -759,24 +832,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "comfy-table" @@ -791,25 +870,12 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "console" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", "unicode-width", "windows-sys 0.61.2", ] @@ -820,6 +886,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.10.1" @@ -846,31 +918,22 @@ dependencies = [ ] [[package]] -name = "crc" -version = "3.4.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ - "crc-catalog", + "libc", ] -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc-fast" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" dependencies = [ - "crc", - "digest", - "rand", - "regex", - "rustversion", + "digest 0.10.7", + "spin", ] [[package]] @@ -888,7 +951,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.13.0", "crossterm_winapi", "document-features", "parking_lot", @@ -913,9 +976,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.4.9" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -924,49 +987,86 @@ dependencies = [ ] [[package]] -name = "crypto-bigint" -version = "0.5.5" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "rand_core 0.6.4", - "subtle", + "generic-array", + "typenum", ] [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", ] [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "defmt" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] [[package]] name = "der" -version = "0.6.1" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", + "pem-rfc7468", "zeroize", ] [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] name = "diff" @@ -980,11 +1080,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1008,9 +1121,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1040,35 +1153,37 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "ecdsa" -version = "0.14.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", + "spki", ] [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" -version = "0.12.3" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", - "crypto-bigint 0.4.9", - "der", - "digest", + "crypto-bigint", + "digest 0.10.7", "ff", "generic-array", "group", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -1100,15 +1215,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle", @@ -1116,9 +1231,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -1128,9 +1243,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" @@ -1143,9 +1258,12 @@ dependencies = [ [[package]] name = "fragile" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] [[package]] name = "fs_extra" @@ -1155,9 +1273,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1170,9 +1288,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1180,15 +1298,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1197,15 +1315,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1214,21 +1332,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1238,7 +1356,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1250,6 +1367,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1267,15 +1385,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasip2", + "rand_core 0.10.1", "wasm-bindgen", ] @@ -1287,9 +1405,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "group" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", "rand_core 0.6.4", @@ -1298,35 +1416,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.2", "indexmap", "slab", "tokio", @@ -1347,9 +1446,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", @@ -1358,9 +1457,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1380,7 +1479,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -1396,9 +1504,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1422,7 +1530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.2", ] [[package]] @@ -1433,7 +1541,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "pin-project-lite", ] @@ -1444,12 +1552,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "humansize" version = "2.1.3" @@ -1460,46 +1562,30 @@ dependencies = [ ] [[package]] -name = "hyper" -version = "0.14.32" +name = "hybrid-array" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", + "typenum", ] [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", + "h2", + "http 1.4.2", "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1507,56 +1593,39 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http 1.4.2", + "hyper", "hyper-util", - "rustls 0.23.36", + "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", "webpki-roots", ] [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", - "hyper 1.8.1", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2", "tokio", "tower-service", "tracing", @@ -1564,12 +1633,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1577,9 +1647,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1590,9 +1660,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1604,15 +1674,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1624,15 +1694,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1656,9 +1726,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1666,23 +1736,23 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "indicatif" -version = "0.18.3" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ - "console 0.16.2", + "console", "portable-atomic", "unicode-width", "unit-prefix", @@ -1691,11 +1761,11 @@ dependencies = [ [[package]] name = "insta" -version = "1.46.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ - "console 0.15.11", + "console", "once_cell", "serde", "similar", @@ -1704,19 +1774,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1726,30 +1786,31 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" dependencies = [ + "defmt", "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", @@ -1758,9 +1819,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" [[package]] name = "jiff-tzdb-platform" @@ -1773,21 +1834,22 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1799,37 +1861,36 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags", "libc", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -1848,17 +1909,17 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" -version = "0.12.5" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -1878,19 +1939,19 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest", + "digest 0.11.3", ] [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mime" @@ -1916,9 +1977,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1972,9 +2033,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -1982,9 +2043,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2017,9 +2078,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2029,9 +2090,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "option-ext" @@ -2047,13 +2108,14 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "p256" -version = "0.11.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ "ecdsa", "elliptic-curve", - "sha2", + "primeorder", + "sha2 0.10.9", ] [[package]] @@ -2079,6 +2141,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2087,9 +2158,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2099,34 +2170,40 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2137,20 +2214,11 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "predicates-core", @@ -2158,15 +2226,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -2182,20 +2250,29 @@ dependencies = [ "yansi", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", "serde", @@ -2203,9 +2280,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -2213,8 +2290,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.36", - "socket2 0.6.1", + "rustls", + "socket2", "thiserror", "tokio", "tracing", @@ -2223,17 +2300,18 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", "rand", + "rand_pcg", "ring", "rustc-hash", - "rustls 0.23.36", + "rustls", "rustls-pki-types", "slab", "thiserror", @@ -2244,51 +2322,42 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -2302,11 +2371,17 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "getrandom 0.3.4", + "rand_core 0.10.1", ] [[package]] @@ -2347,7 +2422,9 @@ dependencies = [ "bytes", "futures", "hex", - "http 1.4.0", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", "jiff", "mockall", "quick-xml", @@ -2355,7 +2432,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror", "tokio", @@ -2370,7 +2447,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] @@ -2384,23 +2461,11 @@ dependencies = [ "thiserror", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" dependencies = [ "aho-corasick", "memchr", @@ -2409,15 +2474,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -2428,18 +2493,19 @@ dependencies = [ "base64", "bytes", "futures-core", - "http 1.4.0", + "futures-util", + "http 1.4.2", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.36", + "rustls", "rustls-native-certs", "rustls-pki-types", "serde", @@ -2447,26 +2513,27 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] [[package]] name = "rfc6979" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", + "hmac 0.12.1", + "subtle", ] [[package]] @@ -2494,9 +2561,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2515,7 +2582,7 @@ dependencies = [ "clap", "clap_complete", "comfy-table", - "console 0.16.2", + "console", "glob", "humansize", "indicatif", @@ -2527,22 +2594,23 @@ dependencies = [ "rc-s3", "serde", "serde_json", - "shlex", + "shlex 1.3.0", "tempfile", "thiserror", "tokio", "tracing", "tracing-subscriber", + "url", "urlencoding", ] [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -2551,36 +2619,24 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.36" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2590,9 +2646,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -2600,19 +2656,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -2622,21 +2668,21 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2647,21 +2693,11 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sec1" -version = "0.3.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", @@ -2673,11 +2709,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.0", "core-foundation", "core-foundation-sys", "libc", @@ -2686,9 +2722,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2696,9 +2732,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "separator" @@ -2738,9 +2774,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap", "itoa", @@ -2752,9 +2788,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -2778,8 +2814,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2789,8 +2836,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2808,6 +2866,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2820,11 +2884,11 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -2836,41 +2900,37 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] -name = "socket2" -version = "0.6.1" +name = "spin" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" [[package]] name = "spki" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -2896,9 +2956,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -2927,12 +2987,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2946,18 +3006,18 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2975,9 +3035,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "num-conv", @@ -2989,15 +3049,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -3005,9 +3065,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3015,9 +3075,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3030,9 +3090,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3040,39 +3100,29 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.36", + "rustls", "tokio", ] @@ -3091,9 +3141,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", @@ -3101,7 +3151,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -3115,18 +3165,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -3145,20 +3195,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.0", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3218,9 +3268,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3242,9 +3292,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicase" @@ -3254,15 +3304,15 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -3314,9 +3364,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "js-sys", "wasm-bindgen", @@ -3355,20 +3405,11 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3379,22 +3420,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3402,9 +3440,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3415,18 +3453,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -3444,9 +3495,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -3485,25 +3536,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -3521,31 +3554,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3554,113 +3570,65 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xmlparser" @@ -3676,9 +3644,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3687,9 +3655,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3699,18 +3667,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", @@ -3719,18 +3687,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3740,15 +3708,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3757,9 +3725,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3768,9 +3736,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -3779,6 +3747,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 117f463..b9258b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [workspace] -resolver = "2" +resolver = "3" members = ["crates/cli", "crates/core", "crates/s3"] [workspace.package] version = "0.1.24" edition = "2024" -rust-version = "1.91" +rust-version = "1.92" license = "MIT OR Apache-2.0" repository = "https://github.com/rustfs/cli" homepage = "https://github.com/rustfs/cli" @@ -23,7 +23,7 @@ rustfs-cli = { path = "./crates/cli", version = "0.1.24" } tokio = { version = "1.49", features = ["full"] } # AWS SDK -aws-sdk-s3 = "1.119" +aws-sdk-s3 = { version = "1.119", default-features = false, features = ["sigv4a", "http-1x", "default-https-client", "rt-tokio"] } aws-config = { version = "1.8", features = ["behavior-version-latest"] } aws-credential-types = "1.2" aws-smithy-types = "1.3" @@ -37,7 +37,7 @@ clap_complete = "4.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.9" -quick-xml = { version = "0.38", features = ["serialize"] } +quick-xml = { version = "0.41", features = ["serialize"] } # Error handling thiserror = "2.0" @@ -65,9 +65,11 @@ glob = "0.3" shlex = "1.3" # HTTP client for Admin API -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "rustls-tls-webpki-roots", "json"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "rustls-tls-webpki-roots", "json", "stream"] } aws-sigv4 = "1.2" http = "1.2" +http-body = "1.0" +http-body-util = "0.1" sha2 = "0.10" hex = "0.4" urlencoding = "2.1" diff --git a/crates/s3/Cargo.toml b/crates/s3/Cargo.toml index 0c6ed91..acd05a6 100644 --- a/crates/s3/Cargo.toml +++ b/crates/s3/Cargo.toml @@ -43,6 +43,8 @@ url.workspace = true reqwest.workspace = true aws-sigv4.workspace = true http.workspace = true +http-body.workspace = true +http-body-util.workspace = true sha2.workspace = true hex.workspace = true urlencoding.workspace = true diff --git a/crates/s3/src/admin.rs b/crates/s3/src/admin.rs index f6d099a..b218afd 100644 --- a/crates/s3/src/admin.rs +++ b/crates/s3/src/admin.rs @@ -17,12 +17,12 @@ use rc_core::admin::{ UpdateGroupMembersRequest, User, UserStatus, }; use rc_core::{Alias, Error, Result}; -use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue}; +use reqwest::header::{CONTENT_TYPE, HOST, HeaderMap, HeaderName, HeaderValue}; use reqwest::{Client, Method, StatusCode}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::HashMap; -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; /// Admin API client for RustFS/MinIO-compatible servers pub struct AdminClient { @@ -40,7 +40,19 @@ impl AdminClient { let mut builder = Client::builder() .danger_accept_invalid_certs(alias.insecure) .tls_built_in_native_certs(true) - .tls_built_in_webpki_certs(true); + .tls_built_in_webpki_certs(true) + .redirect(reqwest::redirect::Policy::none()); + + if let Some(timeout) = &alias.timeout { + if timeout.connect_ms == 0 || timeout.read_ms == 0 { + return Err(Error::Config( + "Alias timeout values must be greater than zero".to_string(), + )); + } + builder = builder + .connect_timeout(Duration::from_millis(timeout.connect_ms)) + .read_timeout(Duration::from_millis(timeout.read_ms)); + } if let Some(bundle_path) = alias.ca_bundle.as_deref() { let pem = std::fs::read(bundle_path).map_err(|e| { @@ -103,6 +115,25 @@ impl AdminClient { hex::encode(hasher.finalize()) } + fn request_headers(&self, body: &[u8]) -> Result { + let mut headers = HeaderMap::new(); + let content_hash = HeaderValue::from_str(&Self::sha256_hash(body)) + .map_err(|e| Error::Auth(format!("Invalid content hash header: {e}")))?; + let host = HeaderValue::from_str(&self.get_host()) + .map_err(|e| Error::Auth(format!("Invalid host header: {e}")))?; + + headers.insert( + HeaderName::from_static("x-amz-content-sha256"), + content_hash, + ); + headers.insert(HOST, host); + if !body.is_empty() { + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + } + + Ok(headers) + } + /// Sign a request using AWS SigV4 async fn sign_request( &self, @@ -192,15 +223,7 @@ impl AdminClient { } let body_bytes = body.unwrap_or(&[]); - let content_hash = Self::sha256_hash(body_bytes); - - let mut headers = HeaderMap::new(); - headers.insert("x-amz-content-sha256", content_hash.parse().unwrap()); - headers.insert("host", self.get_host().parse().unwrap()); - - if !body_bytes.is_empty() { - headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); - } + let headers = self.request_headers(body_bytes)?; let signed_headers = self .sign_request(&method, &url, &headers, body_bytes) @@ -267,15 +290,7 @@ impl AdminClient { } let body_bytes = body.unwrap_or(&[]); - let content_hash = Self::sha256_hash(body_bytes); - - let mut headers = HeaderMap::new(); - headers.insert("x-amz-content-sha256", content_hash.parse().unwrap()); - headers.insert("host", self.get_host().parse().unwrap()); - - if !body_bytes.is_empty() { - headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); - } + let headers = self.request_headers(body_bytes)?; let signed_headers = self .sign_request(&method, &url, &headers, body_bytes) diff --git a/crates/s3/src/client.rs b/crates/s3/src/client.rs index f0cde25..4bd14fc 100644 --- a/crates/s3/src/client.rs +++ b/crates/s3/src/client.rs @@ -22,6 +22,9 @@ use aws_smithy_runtime_api::http::{Response, StatusCode}; use aws_smithy_types::body::SdkBody; use aws_smithy_types::config_bag::ConfigBag; use bytes::Bytes; +use futures::TryStreamExt as _; +use http_body::Frame; +use http_body_util::StreamBody; use jiff::Timestamp; use quick_xml::de::from_str as from_xml_str; use rc_core::{ @@ -35,8 +38,11 @@ use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue}; use serde::Deserialize; use sha2::{Digest, Sha256}; use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; use tokio::io::AsyncReadExt; use tokio::io::AsyncWrite; +use tokio::io::AsyncWriteExt; /// Keep single-part uploads small to avoid backend incompatibilities with /// streaming aws-chunked payloads. @@ -44,6 +50,7 @@ const SINGLE_PUT_OBJECT_MAX_SIZE: u64 = crate::multipart::DEFAULT_PART_SIZE; const S3_SERVICE_NAME: &str = "s3"; const S3_REPLICATION_XML_NAMESPACE: &str = "http://s3.amazonaws.com/doc/2006-03-01/"; const RUSTFS_FORCE_DELETE_HEADER: &str = "x-rustfs-force-delete"; +static DOWNLOAD_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum BucketPolicyErrorKind { @@ -65,8 +72,10 @@ impl ReqwestConnector { ca_bundle: Option<&str>, client_cert: Option<&str>, client_key: Option<&str>, + timeout: Option<&rc_core::alias::TimeoutConfig>, ) -> Result { - let client = build_reqwest_client(insecure, ca_bundle, client_cert, client_key).await?; + let client = + build_reqwest_client(insecure, ca_bundle, client_cert, client_key, timeout).await?; Ok(Self { client }) } } @@ -76,11 +85,20 @@ async fn build_reqwest_client( ca_bundle: Option<&str>, client_cert: Option<&str>, client_key: Option<&str>, + timeout: Option<&rc_core::alias::TimeoutConfig>, ) -> Result { // NOTE: When `insecure = true`, `danger_accept_invalid_certs` disables all TLS // certificate verification. Any CA bundle provided will still be added to the // trust store but is rendered ineffective for this connection. - let mut builder = reqwest::Client::builder().danger_accept_invalid_certs(insecure); + let mut builder = reqwest::Client::builder() + .danger_accept_invalid_certs(insecure) + .redirect(reqwest::redirect::Policy::none()); + + if let Some(timeout) = timeout { + builder = builder + .connect_timeout(Duration::from_millis(timeout.connect_ms)) + .read_timeout(Duration::from_millis(timeout.read_ms)); + } if let Some(bundle_path) = ca_bundle { // Use tokio::fs::read to avoid blocking the async runtime thread. @@ -123,6 +141,46 @@ fn force_path_style_for_alias(alias: &Alias) -> bool { } } +fn sdk_retry_config( + config: &rc_core::alias::RetryConfig, +) -> Result { + if config.max_attempts == 0 { + return Err(Error::Config( + "Alias retry.max_attempts must be greater than zero".to_string(), + )); + } + if config.initial_backoff_ms == 0 + || config.max_backoff_ms == 0 + || config.initial_backoff_ms > config.max_backoff_ms + { + return Err(Error::Config( + "Alias retry backoff values must be non-zero and initial_backoff_ms must not exceed max_backoff_ms" + .to_string(), + )); + } + + Ok(aws_smithy_types::retry::RetryConfigBuilder::new() + .max_attempts(config.max_attempts) + .initial_backoff(Duration::from_millis(config.initial_backoff_ms)) + .max_backoff(Duration::from_millis(config.max_backoff_ms)) + .build()) +} + +fn sdk_timeout_config( + config: &rc_core::alias::TimeoutConfig, +) -> Result { + if config.connect_ms == 0 || config.read_ms == 0 { + return Err(Error::Config( + "Alias timeout values must be greater than zero".to_string(), + )); + } + + Ok(aws_smithy_types::timeout::TimeoutConfig::builder() + .connect_timeout(Duration::from_millis(config.connect_ms)) + .read_timeout(Duration::from_millis(config.read_ms)) + .build()) +} + fn is_aliyun_oss_service_endpoint(endpoint: &str) -> bool { let Ok(url) = reqwest::Url::parse(endpoint.trim_end_matches('/')) else { return false; @@ -701,7 +759,7 @@ fn build_lifecycle_rule_filter( } impl HttpConnector for ReqwestConnector { - fn call(&self, request: HttpRequest) -> HttpConnectorFuture { + fn call(&self, mut request: HttpRequest) -> HttpConnectorFuture { let client = self.client.clone(); HttpConnectorFuture::new(async move { // Extract request parts before consuming the request @@ -709,19 +767,11 @@ impl HttpConnector for ReqwestConnector { let method_str = request.method().to_string(); let headers = request.headers().clone(); - // Try to get the body as buffered in-memory bytes. - // For streaming bodies (e.g., large file uploads), bytes() returns None and we - // return a clear error rather than silently sending an empty body, which would - // cause signature mismatches or server-side failures. - let body_bytes = match request.body().bytes() { - Some(b) => Bytes::copy_from_slice(b), - None => { - return Err(ConnectorError::user( - "Streaming request bodies are not supported in insecure/ca_bundle TLS mode; \ - use in-memory data for uploads with this connector" - .into(), - )); - } + let body = match request.body().bytes() { + Some(bytes) => reqwest::Body::from(Bytes::copy_from_slice(bytes)), + None => reqwest::Body::wrap_stream(http_body_util::BodyDataStream::new( + request.take_body(), + )), }; // Build reqwest method @@ -750,7 +800,7 @@ impl HttpConnector for ReqwestConnector { } // Set body - *req.body_mut() = Some(reqwest::Body::from(body_bytes)); + *req.body_mut() = Some(body); // Execute let resp = client @@ -762,12 +812,8 @@ impl HttpConnector for ReqwestConnector { let status = StatusCode::try_from(resp.status().as_u16()) .map_err(|e| ConnectorError::other(Box::new(e), None))?; let resp_headers = resp.headers().clone(); - let body = resp - .bytes() - .await - .map_err(|e| ConnectorError::io(Box::new(e)))?; - - let mut sdk_response = Response::new(status, SdkBody::from(body)); + let body = StreamBody::new(resp.bytes_stream().map_ok(Frame::data)); + let mut sdk_response = Response::new(status, SdkBody::from_body_1_x(body)); for (name, value) in &resp_headers { match value.to_str() { Ok(value_str) => { @@ -870,6 +916,13 @@ impl S3Client { config_loader = config_loader.credentials_provider(credentials); } + if let Some(retry) = &alias.retry { + config_loader = config_loader.retry_config(sdk_retry_config(retry)?); + } + if let Some(timeout) = &alias.timeout { + config_loader = config_loader.timeout_config(sdk_timeout_config(timeout)?); + } + // When insecure mode is enabled or a custom CA bundle is provided, use the reqwest // connector which supports danger_accept_invalid_certs and custom root certificates. if alias.insecure @@ -881,6 +934,7 @@ impl S3Client { alias.ca_bundle.as_deref(), alias.client_cert.as_deref(), alias.client_key.as_deref(), + alias.timeout.as_ref(), ) .await?; config_loader = config_loader.http_client(connector); @@ -891,6 +945,7 @@ impl S3Client { alias.ca_bundle.as_deref(), alias.client_cert.as_deref(), alias.client_key.as_deref(), + alias.timeout.as_ref(), ) .await?; let config = config_loader.load().await; @@ -940,6 +995,17 @@ impl S3Client { &self, path: &RemotePath, max_keys: Option, + ) -> Result { + self.list_object_versions_page_with_markers(path, max_keys, None, None) + .await + } + + pub async fn list_object_versions_page_with_markers( + &self, + path: &RemotePath, + max_keys: Option, + key_marker: Option<&str>, + version_id_marker: Option<&str>, ) -> Result { let mut builder = self.inner.list_object_versions().bucket(&path.bucket); @@ -950,6 +1016,12 @@ impl S3Client { if let Some(max) = max_keys { builder = builder.max_keys(max); } + if let Some(key_marker) = key_marker { + builder = builder.key_marker(key_marker); + } + if let Some(version_id_marker) = version_id_marker { + builder = builder.version_id_marker(version_id_marker); + } let response = builder.send().await.map_err(|e| { let err_str = Self::format_sdk_error(&e); @@ -1029,11 +1101,8 @@ impl S3Client { let content_length = response .content_length() .and_then(|length| u64::try_from(length).ok()); - let mut data = Vec::with_capacity( - content_length - .and_then(|length| usize::try_from(length).ok()) - .unwrap_or_default(), - ); + let initial_capacity = content_length.unwrap_or_default().min(8 * 1024 * 1024); + let mut data = Vec::with_capacity(usize::try_from(initial_capacity).unwrap_or_default()); let mut body = response.body; let mut bytes_downloaded = 0u64; @@ -1050,6 +1119,216 @@ impl S3Client { Ok(data) } + pub async fn download_object_to_path( + &self, + path: &RemotePath, + destination: &std::path::Path, + mut on_progress: impl FnMut(u64, Option) + Send, + ) -> Result { + let response = self + .inner + .get_object() + .bucket(&path.bucket) + .key(&path.key) + .send() + .await + .map_err(|error| { + let message = error.to_string(); + if message.contains("NotFound") || message.contains("NoSuchKey") { + Error::NotFound(path.to_string()) + } else { + Error::Network(message) + } + })?; + let content_length = response + .content_length() + .and_then(|length| u64::try_from(length).ok()); + let parent = destination.parent().ok_or_else(|| { + Error::General(format!( + "download destination '{}' has no parent directory", + destination.display() + )) + })?; + let file_name = destination.file_name().ok_or_else(|| { + Error::General(format!( + "download destination '{}' has no file name", + destination.display() + )) + })?; + let sequence = DOWNLOAD_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed); + let temporary = parent.join(format!( + ".{}.rc-part-{}-{sequence}", + file_name.to_string_lossy(), + std::process::id() + )); + let mut file = tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&temporary) + .await + .map_err(|error| { + Error::General(format!( + "create temporary download '{}': {error}", + temporary.display() + )) + })?; + let mut body = response.body; + let mut bytes_downloaded = 0u64; + + while let Some(chunk) = match body.try_next().await { + Ok(chunk) => chunk, + Err(error) => { + drop(file); + let _ = tokio::fs::remove_file(&temporary).await; + return Err(Error::Network(error.to_string())); + } + } { + if let Err(error) = file.write_all(&chunk).await { + drop(file); + let _ = tokio::fs::remove_file(&temporary).await; + return Err(Error::General(format!( + "write download destination '{}': {error}", + destination.display() + ))); + } + bytes_downloaded += chunk.len() as u64; + on_progress(bytes_downloaded, content_length); + } + + if let Err(error) = file.flush().await { + drop(file); + let _ = tokio::fs::remove_file(&temporary).await; + return Err(Error::General(format!( + "flush download destination '{}': {error}", + destination.display() + ))); + } + + drop(file); + let rename_result = tokio::fs::rename(&temporary, destination).await; + #[cfg(windows)] + let rename_result = match rename_result { + Ok(()) => Ok(()), + Err(_) if tokio::fs::try_exists(destination).await.unwrap_or(false) => { + tokio::fs::remove_file(destination).await?; + tokio::fs::rename(&temporary, destination).await + } + Err(error) => Err(error), + }; + if let Err(error) = rename_result { + let _ = tokio::fs::remove_file(&temporary).await; + return Err(Error::General(format!( + "replace download destination '{}': {error}", + destination.display() + ))); + } + + Ok(bytes_downloaded) + } + + pub async fn write_object_to( + &self, + path: &RemotePath, + writer: &mut W, + max_bytes: Option, + ) -> Result + where + W: AsyncWrite + Unpin + Send, + { + if matches!(max_bytes, Some(0)) { + return Ok(0); + } + + let mut request = self.inner.get_object().bucket(&path.bucket).key(&path.key); + if let Some(max_bytes) = max_bytes { + request = request.range(format!("bytes=0-{}", max_bytes - 1)); + } + let response = request.send().await.map_err(|error| { + let message = error.to_string(); + if message.contains("NotFound") || message.contains("NoSuchKey") { + Error::NotFound(path.to_string()) + } else { + Error::Network(message) + } + })?; + let mut body = response.body; + let mut bytes_written = 0u64; + + while let Some(chunk) = body + .try_next() + .await + .map_err(|error| Error::Network(error.to_string()))? + { + let remaining = max_bytes + .map(|limit| limit.saturating_sub(bytes_written)) + .unwrap_or(chunk.len() as u64); + let write_len = chunk.len().min(remaining as usize); + writer.write_all(&chunk[..write_len]).await?; + bytes_written += write_len as u64; + if max_bytes.is_some_and(|limit| bytes_written >= limit) { + break; + } + } + writer.flush().await?; + + Ok(bytes_written) + } + + pub async fn put_object_if_absent( + &self, + path: &RemotePath, + data: Vec, + content_type: Option<&str>, + ) -> Result { + let size = data.len() as i64; + let mut request = self + .inner + .put_object() + .bucket(&path.bucket) + .key(&path.key) + .if_none_match("*") + .body(aws_sdk_s3::primitives::ByteStream::from(data)); + if let Some(content_type) = content_type { + request = request.content_type(content_type); + } + + let response = request.send().await.map_err(|error| { + if let aws_sdk_s3::error::SdkError::ServiceError(service_error) = &error + && service_error.raw().status().as_u16() == 412 + { + Error::Conflict(format!("Object already exists: {path}")) + } else { + Error::Network(Self::format_sdk_error(&error)) + } + })?; + let mut info = ObjectInfo::file(&path.key, size); + info.etag = response + .e_tag() + .map(|etag| etag.trim_matches('"').to_string()); + info.last_modified = Some(jiff::Timestamp::now()); + Ok(info) + } + + pub async fn delete_object_if_match(&self, path: &RemotePath, etag: &str) -> Result<()> { + self.inner + .delete_object() + .bucket(&path.bucket) + .key(&path.key) + .if_match(etag) + .send() + .await + .map_err(|error| { + if let aws_sdk_s3::error::SdkError::ServiceError(service_error) = &error + && service_error.raw().status().as_u16() == 412 + { + Error::Conflict(format!("Object changed before deletion: {path}")) + } else { + Error::Network(Self::format_sdk_error(&error)) + } + })?; + Ok(()) + } + /// Delete an object with RustFS-specific request options. pub async fn delete_object_with_options( &self, @@ -1640,6 +1919,10 @@ impl S3Client { let part_size = config.calculate_part_size(file_size); let part_buffer_size = usize::try_from(part_size) .map_err(|_| Error::General(format!("invalid part size: {part_size}")))?; + let mut file = tokio::fs::File::open(file_path) + .await + .map_err(|e| Error::General(format!("open file '{}': {e}", file_path.display())))?; + let mut chunk = vec![0u8; part_buffer_size]; tracing::debug!(file_size, part_size, "Starting multipart upload"); @@ -1674,16 +1957,19 @@ impl S3Client { tracing::debug!(upload_id = %upload_id, "Multipart upload initiated"); - let mut file = tokio::fs::File::open(file_path) - .await - .map_err(|e| Error::General(format!("open file '{}': {e}", file_path.display())))?; let mut completed_parts = Vec::new(); let mut part_number: i32 = 1; - let mut chunk = vec![0u8; part_buffer_size]; let mut bytes_uploaded: u64 = 0; loop { - let bytes_read = Self::read_next_part(&mut file, file_path, &mut chunk).await?; + let bytes_read = match Self::read_next_part(&mut file, file_path, &mut chunk).await { + Ok(bytes_read) => bytes_read, + Err(error) => { + self.abort_multipart_upload_best_effort(path, &upload_id) + .await; + return Err(error); + } + }; if bytes_read == 0 { break; } @@ -1844,15 +2130,33 @@ fn build_tagging( .map_err(|e| Error::General(format!("invalid tagging payload: {e}"))) } +fn validate_continuation_token( + truncated: bool, + current: Option<&str>, + next: Option<&str>, +) -> Result<()> { + if truncated && (next.is_none() || next == current) { + return Err(Error::Network( + "S3 returned a truncated object listing without a new continuation token".to_string(), + )); + } + + Ok(()) +} + #[async_trait] impl ObjectStore for S3Client { async fn list_buckets(&self) -> Result> { - let response = self - .inner - .list_buckets() - .send() - .await - .map_err(|e| Error::Network(Self::format_sdk_error(&e)))?; + let response = self.inner.list_buckets().send().await.map_err(|e| { + let message = Self::format_sdk_error(&e); + if let aws_sdk_s3::error::SdkError::ServiceError(service_error) = &e + && matches!(service_error.raw().status().as_u16(), 401 | 403) + { + Error::Auth(message) + } else { + Error::Network(message) + } + })?; let buckets = response .buckets() @@ -1939,10 +2243,18 @@ impl ObjectStore for S3Client { items.push(info); } + let truncated = response.is_truncated().unwrap_or(false); + let continuation_token = response.next_continuation_token().map(ToString::to_string); + validate_continuation_token( + truncated, + options.continuation_token.as_deref(), + continuation_token.as_deref(), + )?; + Ok(ListResult { items, - truncated: response.is_truncated().unwrap_or(false), - continuation_token: response.next_continuation_token().map(|s| s.to_string()), + truncated, + continuation_token, }) } @@ -3131,6 +3443,7 @@ mod tests { .endpoint_url(endpoint) .region(aws_sdk_s3::config::Region::new("us-east-1")) .force_path_style(true) + .retry_config(aws_smithy_types::retry::RetryConfig::disabled()) .behavior_version_latest() .http_client(http_client); @@ -4115,7 +4428,7 @@ mod tests { #[tokio::test] async fn reqwest_connector_insecure_without_ca_bundle_succeeds() { // When insecure is true and no CA bundle is provided, the connector should be created. - let connector = ReqwestConnector::new(true, None, None, None).await; + let connector = ReqwestConnector::new(true, None, None, None, None).await; assert!( connector.is_ok(), "Expected insecure connector creation to succeed" @@ -4125,7 +4438,7 @@ mod tests { #[tokio::test] async fn reqwest_connector_invalid_ca_bundle_path_surfaces_error() { // Use an obviously invalid path (empty string) to trigger a read error. - let result = ReqwestConnector::new(false, Some(""), None, None).await; + let result = ReqwestConnector::new(false, Some(""), None, None, None).await; match result { Err(Error::Network(msg)) => { assert!( @@ -4167,6 +4480,35 @@ mod tests { assert_eq!(request.headers().get("x-rustfs-force-delete"), Some("true")); } + #[tokio::test] + async fn conditional_mirror_writes_and_deletes_set_precondition_headers() { + let put_response = http::Response::builder() + .status(200) + .body(SdkBody::from("")) + .expect("build put response"); + let (put_client, put_request_receiver) = test_s3_client(Some(put_response)); + let path = RemotePath::new("test", "bucket", "key.txt"); + + put_client + .put_object_if_absent(&path, b"data".to_vec(), None) + .await + .expect("conditional put"); + let put_request = put_request_receiver.expect_request(); + assert_eq!(put_request.headers().get("if-none-match"), Some("*")); + + let delete_response = http::Response::builder() + .status(204) + .body(SdkBody::from("")) + .expect("build delete response"); + let (delete_client, delete_request_receiver) = test_s3_client(Some(delete_response)); + delete_client + .delete_object_if_match(&path, "etag-value") + .await + .expect("conditional delete"); + let delete_request = delete_request_receiver.expect_request(); + assert_eq!(delete_request.headers().get("if-match"), Some("etag-value")); + } + #[tokio::test] async fn custom_headers_are_added_before_sending_sdk_requests() { let (client, request_receiver) = test_s3_client_with_endpoint_and_headers( @@ -4195,6 +4537,29 @@ mod tests { ); } + #[tokio::test] + async fn write_object_to_limits_download_with_range_header() { + let response = http::Response::builder() + .status(206) + .header("content-length", "3") + .header("content-range", "bytes 0-2/8") + .body(SdkBody::from("abc")) + .expect("build ranged get response"); + let (client, request_receiver) = test_s3_client(Some(response)); + let path = RemotePath::new("test", "bucket", "key.txt"); + let mut output = Vec::new(); + + let written = client + .write_object_to(&path, &mut output, Some(3)) + .await + .expect("stream ranged object"); + + let request = request_receiver.expect_request(); + assert_eq!(request.headers().get("range"), Some("bytes=0-2")); + assert_eq!(written, 3); + assert_eq!(output, b"abc"); + } + #[tokio::test] async fn custom_headers_are_not_required_by_presigned_urls() { let (client, _request_receiver) = test_s3_client_with_endpoint_and_headers( @@ -4575,11 +4940,36 @@ mod tests { let result = client.list_buckets().await; match result { - Err(Error::Network(message)) => assert!(message.contains("InvalidAccessKeyId")), - other => panic!("Expected Network for list buckets failure, got: {other:?}"), + Err(Error::Auth(message)) => assert!(message.contains("InvalidAccessKeyId")), + other => panic!("Expected Auth for list buckets failure, got: {other:?}"), } } + #[test] + fn truncated_listing_requires_a_new_continuation_token() { + assert!(validate_continuation_token(false, None, None).is_ok()); + assert!(validate_continuation_token(true, None, Some("next")).is_ok()); + assert!(validate_continuation_token(true, Some("current"), None).is_err()); + assert!(validate_continuation_token(true, Some("current"), Some("current")).is_err()); + } + + #[test] + fn alias_retry_and_timeout_configs_are_validated() { + let retry = rc_core::alias::RetryConfig { + max_attempts: 0, + ..Default::default() + }; + let timeout = rc_core::alias::TimeoutConfig { + connect_ms: 0, + ..Default::default() + }; + + assert!(sdk_retry_config(&retry).is_err()); + assert!(sdk_timeout_config(&timeout).is_err()); + assert!(sdk_retry_config(&Default::default()).is_ok()); + assert!(sdk_timeout_config(&Default::default()).is_ok()); + } + #[tokio::test] async fn create_bucket_preserves_service_error_code() { let response = http::Response::builder() From 42033e4721883b24237bdbb0d0dc92f217b989c2 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 11 Jul 2026 12:02:59 +0800 Subject: [PATCH 2/4] fix(cli): harden command safety and behavior --- crates/cli/Cargo.toml | 1 + crates/cli/src/commands/alias.rs | 10 +- crates/cli/src/commands/anonymous.rs | 5 - crates/cli/src/commands/cat.rs | 22 +- crates/cli/src/commands/cp.rs | 220 ++++++++++++++++-- crates/cli/src/commands/diff.rs | 34 ++- crates/cli/src/commands/find.rs | 34 ++- crates/cli/src/commands/head.rs | 70 +++--- crates/cli/src/commands/ls.rs | 71 +++++- crates/cli/src/commands/mb.rs | 7 + crates/cli/src/commands/mirror.rs | 311 ++++++++++++++++++-------- crates/cli/src/commands/mod.rs | 118 +++++++++- crates/cli/src/commands/mv.rs | 310 +++++++++++++++++++++----- crates/cli/src/commands/pipe.rs | 6 + crates/cli/src/commands/rb.rs | 22 +- crates/cli/src/commands/rm.rs | 68 +++++- crates/cli/src/commands/share.rs | 17 +- crates/cli/src/commands/stat.rs | 9 +- crates/cli/src/output/formatter.rs | 64 +++++- crates/cli/tests/golden.rs | 97 +++----- crates/cli/tests/integration.rs | 320 ++++++--------------------- 21 files changed, 1224 insertions(+), 592 deletions(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index e65e2d4..ae01917 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -52,6 +52,7 @@ mime_guess.workspace = true glob.workspace = true shlex.workspace = true urlencoding.workspace = true +url.workspace = true [features] default = [] diff --git a/crates/cli/src/commands/alias.rs b/crates/cli/src/commands/alias.rs index 04e1793..7cd90e4 100644 --- a/crates/cli/src/commands/alias.rs +++ b/crates/cli/src/commands/alias.rs @@ -55,7 +55,7 @@ pub struct SetArgs { #[arg(long, default_value = "us-east-1")] pub region: String, - /// Signature version: v4 or v2 (default: v4) + /// Signature version (only v4 is currently supported) #[arg(long, default_value = "v4")] pub signature: String, @@ -189,7 +189,13 @@ async fn execute_set(args: SetArgs, manager: &AliasManager, formatter: &Formatte } // Validate signature version - if args.signature != "v4" && args.signature != "v2" { + if args.signature == "v2" { + return formatter.fail( + ExitCode::UnsupportedFeature, + "Only SigV4 aliases are currently supported", + ); + } + if args.signature != "v4" { return formatter.fail(ExitCode::UsageError, "Signature must be 'v4' or 'v2'"); } diff --git a/crates/cli/src/commands/anonymous.rs b/crates/cli/src/commands/anonymous.rs index a269333..b78c6e0 100644 --- a/crates/cli/src/commands/anonymous.rs +++ b/crates/cli/src/commands/anonymous.rs @@ -735,11 +735,6 @@ async fn list_keys_for_prefix( let mut options = ListOptions { recursive, max_keys: Some(1000), - prefix: if prefix.is_empty() { - None - } else { - Some(prefix.to_string()) - }, ..Default::default() }; diff --git a/crates/cli/src/commands/cat.rs b/crates/cli/src/commands/cat.rs index 277ecd2..9fa3703 100644 --- a/crates/cli/src/commands/cat.rs +++ b/crates/cli/src/commands/cat.rs @@ -3,9 +3,8 @@ //! Outputs the entire content of an object to stdout. use clap::Args; -use rc_core::{AliasManager, ObjectStore as _, RemotePath}; +use rc_core::{AliasManager, RemotePath}; use rc_s3::S3Client; -use std::io::{self, Write}; use crate::exit_code::ExitCode; use crate::output::{Formatter, OutputConfig}; @@ -33,6 +32,13 @@ pub struct CatArgs { pub async fn execute(args: CatArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.enc_key.is_some() || args.rewind.is_some() || args.version_id.is_some() { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--enc-key, --rewind, and --version-id are not implemented for cat", + ); + } + // Parse the path let (alias_name, bucket, key) = match parse_cat_path(&args.path) { Ok(parsed) => parsed, @@ -71,15 +77,9 @@ pub async fn execute(args: CatArgs, output_config: OutputConfig) -> ExitCode { let path = RemotePath::new(&alias_name, &bucket, &key); // Get object content - match client.get_object(&path).await { - Ok(data) => { - // Write directly to stdout (not through formatter to preserve binary data) - if let Err(e) = io::stdout().write_all(&data) { - formatter.error(&format!("Failed to write to stdout: {e}")); - return ExitCode::GeneralError; - } - ExitCode::Success - } + let mut stdout = tokio::io::stdout(); + match client.write_object_to(&path, &mut stdout, None).await { + Ok(_) => ExitCode::Success, Err(e) => { let err_str = e.to_string(); if err_str.contains("NotFound") || err_str.contains("NoSuchKey") { diff --git a/crates/cli/src/commands/cp.rs b/crates/cli/src/commands/cp.rs index 0b1eaef..4b50fd8 100644 --- a/crates/cli/src/commands/cp.rs +++ b/crates/cli/src/commands/cp.rs @@ -83,6 +83,20 @@ struct CpOutput { /// Execute the cp command pub async fn execute(args: CpArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + + if args.preserve { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--preserve is not implemented; refusing to continue with a silently ignored option", + ); + } + if args.storage_class.is_some() { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--storage-class is not implemented; refusing to continue with a silently ignored option", + ); + } + let alias_manager = AliasManager::new().ok(); // Parse source and target paths @@ -119,6 +133,12 @@ pub async fn execute(args: CpArgs, output_config: OutputConfig) -> ExitCode { copy_s3_to_local(src, dst, &args, &formatter).await } (ParsedPath::Remote(src), ParsedPath::Remote(dst)) => { + if args.recursive { + return formatter.fail( + ExitCode::UnsupportedFeature, + "Recursive S3-to-S3 copy is not implemented", + ); + } // S3 to S3 copy_s3_to_s3(src, dst, &args, &formatter).await } @@ -495,7 +515,7 @@ async fn copy_s3_to_local( } } -async fn download_file( +pub(super) async fn download_file( client: &S3Client, src: &RemotePath, dst: &Path, @@ -507,6 +527,15 @@ async fn download_file( // Determine destination path let dst_path = if dst.is_dir() || dst.to_string_lossy().ends_with('/') { let filename = src.key.rsplit('/').next().unwrap_or(&src.key); + let filename = match safe_download_relative_path(filename, "") { + Ok(filename) => filename, + Err(error) => { + return formatter.fail( + ExitCode::UsageError, + &format!("Unsafe object key '{}': {error}", src.key), + ); + } + }; dst.join(filename) } else { dst.to_path_buf() @@ -546,7 +575,7 @@ async fn download_file( // Download object let result = client - .get_object_with_progress(src, |bytes_downloaded, total_size| { + .download_object_to_path(src, &dst_path, |bytes_downloaded, total_size| { update_download_progress(&mut progress, &output_config, bytes_downloaded, total_size); }) .await; @@ -556,15 +585,8 @@ async fn download_file( } match result { - Ok(data) => { - let size = data.len() as i64; - - if let Err(e) = std::fs::write(&dst_path, &data) { - return formatter.fail( - ExitCode::GeneralError, - &format!("Failed to write {dst_display}: {e}"), - ); - } + Ok(size) => { + let size = size as i64; if formatter.is_json() { let output = CpOutput { @@ -631,13 +653,34 @@ async fn download_prefix( } // Calculate relative path from prefix - let relative_key = item - .key - .strip_prefix(&src.key) - .unwrap_or(&item.key) - .trim_start_matches('/'); - let dst_path = - dst.join(relative_key.replace('/', std::path::MAIN_SEPARATOR_STR)); + let relative_path = match safe_download_relative_path(&item.key, &src.key) { + Ok(path) => path, + Err(error) => { + error_count += 1; + formatter.error(&format!( + "Refusing unsafe object key '{}': {error}", + item.key + )); + if !args.continue_on_error { + return ExitCode::UsageError; + } + continue; + } + }; + let dst_path = match safe_download_destination(dst, &relative_path).await { + Ok(path) => path, + Err(error) => { + error_count += 1; + formatter.error(&format!( + "Refusing unsafe destination for '{}': {error}", + item.key + )); + if !args.continue_on_error { + return ExitCode::UsageError; + } + continue; + } + }; let obj_src = RemotePath::new(&src.alias, &src.bucket, &item.key); let result = download_file(client, &obj_src, &dst_path, args, formatter).await; @@ -683,6 +726,76 @@ async fn download_prefix( } } +pub(super) fn safe_download_relative_path(key: &str, prefix: &str) -> Result { + let relative = key + .strip_prefix(prefix) + .ok_or_else(|| format!("key is outside requested prefix '{prefix}'"))? + .trim_start_matches('/'); + + let mut path = PathBuf::new(); + for component in relative.split(['/', '\\']) { + if component.is_empty() { + continue; + } + if matches!(component, "." | "..") { + return Err("path traversal components are not allowed".to_string()); + } + if component.contains(':') { + return Err("colon characters are not allowed in download paths".to_string()); + } + if component.ends_with(['.', ' ']) { + return Err("download path components must not end in a dot or space".to_string()); + } + let stem = component.split('.').next().unwrap_or_default(); + let stem = stem.to_ascii_uppercase(); + if matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") + || (stem.len() == 4 + && (stem.starts_with("COM") || stem.starts_with("LPT")) + && matches!(stem.as_bytes()[3], b'1'..=b'9')) + { + return Err("reserved Windows device names are not allowed".to_string()); + } + path.push(component); + } + + if path.as_os_str().is_empty() { + return Err("object key does not contain a file path".to_string()); + } + + Ok(path) +} + +pub(super) async fn safe_download_destination( + root: &Path, + relative: &Path, +) -> Result { + let mut destination = root.to_path_buf(); + for component in relative.components() { + let std::path::Component::Normal(component) = component else { + return Err("destination path contains a non-normal component".to_string()); + }; + destination.push(component); + match tokio::fs::symlink_metadata(&destination).await { + Ok(metadata) if metadata.file_type().is_symlink() => { + return Err(format!( + "destination component '{}' is a symbolic link", + destination.display() + )); + } + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(format!( + "failed to inspect destination '{}': {error}", + destination.display() + )); + } + } + } + + Ok(destination) +} + async fn copy_s3_to_s3( src: &RemotePath, dst: &RemotePath, @@ -746,6 +859,33 @@ async fn copy_s3_to_s3( return ExitCode::Success; } + match client.head_object(src).await { + Ok(info) + if info + .size_bytes + .is_some_and(|size| size > 5 * 1024 * 1024 * 1024) => + { + return formatter.fail( + ExitCode::UnsupportedFeature, + "Objects larger than 5 GiB require multipart copy, which is not implemented", + ); + } + Ok(_) => {} + Err(rc_core::Error::NotFound(_)) => { + return formatter.fail_with_suggestion( + ExitCode::NotFound, + &format!("Source not found: {src_display}"), + "Check the source bucket and object key, then retry the copy command.", + ); + } + Err(error) => { + return formatter.fail( + ExitCode::NetworkError, + &format!("Failed to inspect source object: {error}"), + ); + } + } + match client.copy_object(src, dst, encryption.as_ref()).await { Ok(info) => { if formatter.is_json() { @@ -972,6 +1112,50 @@ mod tests { assert!(!progress.is_visible()); } + #[test] + fn download_relative_path_preserves_safe_nested_keys() { + let relative = safe_download_relative_path("reports/2026/july/data.csv", "reports/") + .expect("safe key should resolve"); + + assert_eq!( + relative, + PathBuf::from("2026").join("july").join("data.csv") + ); + } + + #[test] + fn download_relative_path_rejects_traversal_and_absolute_keys() { + for key in [ + "reports/../../escaped", + "reports/..\\..\\escaped", + "reports/C:/escaped", + "reports/safe:stream", + "reports/CON.txt", + "reports/trailing.", + "/absolute/path", + ] { + assert!( + safe_download_relative_path(key, "reports/").is_err(), + "unsafe key should be rejected: {key}" + ); + } + } + + #[cfg(unix)] + #[tokio::test] + async fn download_destination_rejects_existing_symlink_components() { + use std::os::unix::fs::symlink; + + let root = tempfile::tempdir().expect("create destination root"); + let outside = tempfile::tempdir().expect("create outside directory"); + symlink(outside.path(), root.path().join("linked")).expect("create test symlink"); + + let result = + safe_download_destination(root.path(), &PathBuf::from("linked/file.txt")).await; + + assert!(result.is_err()); + } + #[test] fn test_select_upload_content_type_uses_guess_for_small_files() { let selected = diff --git a/crates/cli/src/commands/diff.rs b/crates/cli/src/commands/diff.rs index c0148ee..a71143c 100644 --- a/crates/cli/src/commands/diff.rs +++ b/crates/cli/src/commands/diff.rs @@ -216,7 +216,10 @@ pub async fn execute(args: DiffArgs, output_config: OutputConfig) -> ExitCode { DiffStatus::OnlySecond => entry.second_size.map(format_size).unwrap_or_default(), }; - formatter.println(&format!("{status_char} {:<50} {size_info}", entry.key)); + formatter.println(&format!( + "{status_char} {:<50} {size_info}", + formatter.sanitize_text(&entry.key) + )); } // Print summary @@ -311,7 +314,10 @@ fn compare_objects( if let Some(second_info) = second.get(key) { // Object exists in both let is_same = first_info.size == second_info.size - && (first_info.etag == second_info.etag || first_info.etag.is_none()); + && matches!( + (&first_info.etag, &second_info.etag), + (Some(first_etag), Some(second_etag)) if first_etag == second_etag + ); let status = if is_same { DiffStatus::Same @@ -423,6 +429,30 @@ mod tests { assert_eq!(entries[0].status, DiffStatus::Different); } + #[test] + fn test_compare_objects_missing_etag_is_different() { + let first = HashMap::from([( + "file.txt".to_string(), + FileInfo { + size: Some(100), + modified: None, + etag: None, + }, + )]); + let second = HashMap::from([( + "file.txt".to_string(), + FileInfo { + size: Some(100), + modified: None, + etag: Some("second-etag".to_string()), + }, + )]); + + let entries = compare_objects(&first, &second, false); + + assert_eq!(entries[0].status, DiffStatus::Different); + } + #[test] fn test_compare_objects_only_first() { let mut first = HashMap::new(); diff --git a/crates/cli/src/commands/find.rs b/crates/cli/src/commands/find.rs index bc31cc1..bc4df5e 100644 --- a/crates/cli/src/commands/find.rs +++ b/crates/cli/src/commands/find.rs @@ -329,6 +329,9 @@ fn parse_size(s: &str) -> Result { let num: i64 = num_str .parse() .map_err(|_| format!("Invalid size number: {num_str}"))?; + if num < 0 { + return Err("Size cannot be negative".to_string()); + } let multiplier = match suffix.to_uppercase().as_str() { "" | "B" => 1, @@ -339,7 +342,8 @@ fn parse_size(s: &str) -> Result { _ => return Err(format!("Unknown size suffix: {suffix}")), }; - Ok(num * multiplier) + num.checked_mul(multiplier) + .ok_or_else(|| "Size is too large".to_string()) } /// Parse duration string and return timestamp that far in the past @@ -359,15 +363,21 @@ fn parse_duration_ago(s: &str, now: jiff::Timestamp) -> Result num, - "m" => num * 60, - "h" => num * 3600, - "d" => num * 86400, - "w" => num * 604800, + let multiplier = match suffix.to_lowercase().as_str() { + "s" => 1, + "m" => 60, + "h" => 3600, + "d" => 86400, + "w" => 604800, _ => return Err(format!("Unknown duration suffix: {suffix}")), }; + let seconds = num + .checked_mul(multiplier) + .ok_or_else(|| "Duration is too large".to_string())?; let duration = jiff::Span::new().seconds(seconds); now.checked_sub(duration) @@ -502,6 +512,16 @@ mod tests { assert!(parse_size("").is_err()); assert!(parse_size("abc").is_err()); assert!(parse_size("1X").is_err()); + assert!(parse_size("-1K").is_err()); + assert!(parse_size("8388608T").is_err()); + } + + #[test] + fn test_parse_duration_rejects_negative_and_overflowing_values() { + let now = jiff::Timestamp::now(); + + assert!(parse_duration_ago("-1h", now).is_err()); + assert!(parse_duration_ago("2562047788015216h", now).is_err()); } #[test] diff --git a/crates/cli/src/commands/head.rs b/crates/cli/src/commands/head.rs index db56b65..7bb1d34 100644 --- a/crates/cli/src/commands/head.rs +++ b/crates/cli/src/commands/head.rs @@ -33,6 +33,13 @@ pub struct HeadArgs { pub async fn execute(args: HeadArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.version_id.is_some() { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--version-id is not implemented for head", + ); + } + // Parse the path let (alias_name, bucket, key) = match parse_head_path(&args.path) { Ok(parsed) => parsed, @@ -70,47 +77,44 @@ pub async fn execute(args: HeadArgs, output_config: OutputConfig) -> ExitCode { let path = RemotePath::new(&alias_name, &bucket, &key); + if let Some(num_bytes) = args.bytes { + let mut stdout = tokio::io::stdout(); + return match client + .write_object_to(&path, &mut stdout, Some(num_bytes as u64)) + .await + { + Ok(_) => ExitCode::Success, + Err(e) => output_get_error(&formatter, &args.path, e), + }; + } + // Get object content match client.get_object(&path).await { Ok(data) => { - let output = if let Some(num_bytes) = args.bytes { - // Output first N bytes - let end = num_bytes.min(data.len()); - &data[..end] - } else { - // Output first N lines - let content = String::from_utf8_lossy(&data); - let lines: Vec<&str> = content.lines().take(args.lines).collect(); - let result = lines.join("\n"); - - // Write string content and add newline - if let Err(e) = writeln!(io::stdout(), "{result}") { - formatter.error(&format!("Failed to write to stdout: {e}")); - return ExitCode::GeneralError; - } - return ExitCode::Success; - }; - - // Write bytes directly to stdout - if let Err(e) = io::stdout().write_all(output) { + let content = String::from_utf8_lossy(&data); + let lines: Vec<&str> = content.lines().take(args.lines).collect(); + let result = lines.join("\n"); + if let Err(e) = writeln!(io::stdout(), "{result}") { formatter.error(&format!("Failed to write to stdout: {e}")); return ExitCode::GeneralError; } ExitCode::Success } - Err(e) => { - let err_str = e.to_string(); - if err_str.contains("NotFound") || err_str.contains("NoSuchKey") { - formatter.error(&format!("Object not found: {}", args.path)); - ExitCode::NotFound - } else if err_str.contains("AccessDenied") { - formatter.error(&format!("Access denied: {}", args.path)); - ExitCode::AuthError - } else { - formatter.error(&format!("Failed to get object: {e}")); - ExitCode::NetworkError - } - } + Err(e) => output_get_error(&formatter, &args.path, e), + } +} + +fn output_get_error(formatter: &Formatter, path: &str, error: rc_core::Error) -> ExitCode { + let message = error.to_string(); + if message.contains("NotFound") || message.contains("NoSuchKey") { + formatter.error(&format!("Object not found: {path}")); + ExitCode::NotFound + } else if message.contains("AccessDenied") { + formatter.error(&format!("Access denied: {path}")); + ExitCode::AuthError + } else { + formatter.error(&format!("Failed to get object: {error}")); + ExitCode::NetworkError } } diff --git a/crates/cli/src/commands/ls.rs b/crates/cli/src/commands/ls.rs index 0f50d03..392a822 100644 --- a/crates/cli/src/commands/ls.rs +++ b/crates/cli/src/commands/ls.rs @@ -92,6 +92,13 @@ struct VersionSummary { pub async fn execute(args: LsArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.incomplete { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--incomplete is not implemented", + ); + } + // Parse the path let (alias_name, bucket, prefix) = match parse_ls_path(&args.path) { Ok(parsed) => parsed, @@ -137,7 +144,9 @@ pub async fn execute(args: LsArgs, output_config: OutputConfig) -> ExitCode { }; } - let bucket = bucket.unwrap(); + let Some(bucket) = bucket else { + return formatter.fail(ExitCode::UsageError, "Bucket name is required"); + }; let path = RemotePath::new(&alias_name, &bucket, prefix.unwrap_or_default()); if args.versions { @@ -154,7 +163,7 @@ async fn list_object_versions( summarize: bool, formatter: &Formatter, ) -> ExitCode { - match client.list_object_versions_page(path, Some(1000)).await { + match list_all_object_versions(client, path).await { Ok(result) => { let versions = result.items.clone(); let total_size: i64 = versions.iter().filter_map(|v| v.size_bytes).sum(); @@ -176,8 +185,10 @@ async fn list_object_versions( formatter.println(&format!( "{latest} {:<40} {:>10} {:>12}{marker}", - version.key, - version.version_id.chars().take(10).collect::(), + formatter.sanitize_text(&version.key), + formatter.sanitize_text( + &version.version_id.chars().take(10).collect::() + ), size )); } @@ -203,6 +214,52 @@ async fn list_object_versions( } } +async fn list_all_object_versions( + client: &S3Client, + path: &RemotePath, +) -> Result { + let mut items = Vec::new(); + let mut key_marker: Option = None; + let mut version_id_marker: Option = None; + + loop { + let page = client + .list_object_versions_page_with_markers( + path, + Some(1000), + key_marker.as_deref(), + version_id_marker.as_deref(), + ) + .await?; + items.extend(page.items); + + if !page.truncated { + return Ok(ObjectVersionListResult { + items, + truncated: false, + continuation_token: None, + version_id_marker: None, + }); + } + + let next_key_marker = page.continuation_token.ok_or_else(|| { + Error::Network( + "S3 returned a truncated version listing without a key marker".to_string(), + ) + })?; + let next_version_id_marker = page.version_id_marker; + if key_marker.as_deref() == Some(next_key_marker.as_str()) + && version_id_marker == next_version_id_marker + { + return Err(Error::Network( + "S3 returned a truncated version listing without advancing its markers".to_string(), + )); + } + key_marker = Some(next_key_marker); + version_id_marker = next_version_id_marker; + } +} + fn ls_version_output(result: ObjectVersionListResult, summarize: bool) -> LsVersionOutput { let total_versions = result.items.len(); let total_size_bytes: i64 = result.items.iter().filter_map(|v| v.size_bytes).sum(); @@ -291,7 +348,11 @@ async fn list_buckets(client: &S3Client, formatter: &Formatter, summarize: bool) } Err(e) => { formatter.error(&format!("Failed to list buckets: {e}")); - ExitCode::NetworkError + match e { + Error::Auth(_) => ExitCode::AuthError, + Error::NotFound(_) => ExitCode::NotFound, + _ => ExitCode::NetworkError, + } } } } diff --git a/crates/cli/src/commands/mb.rs b/crates/cli/src/commands/mb.rs index aa6dc9c..3c3c7cd 100644 --- a/crates/cli/src/commands/mb.rs +++ b/crates/cli/src/commands/mb.rs @@ -52,6 +52,13 @@ struct MbOutput { pub async fn execute(args: MbArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.region.is_some() || args.with_lock || args.with_versioning { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--region, --with-lock, and --with-versioning are not implemented for bucket creation", + ); + } + // Parse the target path let (alias_name, bucket) = match parse_mb_path(&args.target) { Ok(parsed) => parsed, diff --git a/crates/cli/src/commands/mirror.rs b/crates/cli/src/commands/mirror.rs index 582ade1..167b0bb 100644 --- a/crates/cli/src/commands/mirror.rs +++ b/crates/cli/src/commands/mirror.rs @@ -9,8 +9,7 @@ use rc_s3::S3Client; use serde::Serialize; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::Semaphore; -use tokio::task::JoinSet; +use tokio::task::{JoinError, JoinSet}; use crate::commands::diff::{DiffEntry, DiffStatus}; use crate::exit_code::ExitCode; @@ -64,6 +63,14 @@ struct FileInfo { etag: Option, } +struct MirrorTaskContext<'a> { + success_marker: &'a str, + operation: &'a str, + quiet: bool, + formatter: &'a Formatter, + progress: Option<&'a ProgressBar>, +} + trait MirrorCopySource { async fn head_object_for_mirror( &self, @@ -79,6 +86,7 @@ trait MirrorCopyTarget { path: &RemotePath, data: Vec, content_type: Option<&str>, + if_absent: bool, ) -> rc_core::Result; } @@ -101,8 +109,13 @@ impl MirrorCopyTarget for S3Client { path: &RemotePath, data: Vec, content_type: Option<&str>, + if_absent: bool, ) -> rc_core::Result { - rc_core::ObjectStore::put_object(self, path, data, content_type, None).await + if if_absent { + self.put_object_if_absent(path, data, content_type).await + } else { + rc_core::ObjectStore::put_object(self, path, data, content_type, None).await + } } } @@ -110,8 +123,8 @@ impl MirrorCopyTarget for S3Client { pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); - if args.parallel == 0 { - formatter.error("--parallel must be greater than 0"); + if !(1..=256).contains(&args.parallel) { + formatter.error("--parallel must be between 1 and 256"); return ExitCode::UsageError; } @@ -162,6 +175,16 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode } }; + if mirror_locations_overlap( + &source_path, + &target_path, + &source_alias.endpoint, + &target_alias.endpoint, + ) { + formatter.error("Mirror source and destination prefixes must not overlap"); + return ExitCode::UsageError; + } + let source_client = Arc::new(match S3Client::new(source_alias).await { Ok(c) => c, Err(e) => { @@ -198,8 +221,8 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode // Compare and determine operations let diff_entries = compare_objects_internal(&source_objects, &target_objects); - let mut to_copy: Vec<(&str, &FileInfo)> = Vec::new(); - let mut to_remove: Vec<&str> = Vec::new(); + let mut to_copy: Vec<(&str, &FileInfo, bool)> = Vec::new(); + let mut to_remove: Vec<(&str, &FileInfo)> = Vec::new(); let mut skipped = 0; for entry in &diff_entries { @@ -207,14 +230,14 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode DiffStatus::OnlyFirst => { // New object, copy it if let Some(info) = source_objects.get(&entry.key) { - to_copy.push((&entry.key, info)); + to_copy.push((&entry.key, info, true)); } } DiffStatus::Different => { if args.overwrite { // Different and overwrite enabled, copy it if let Some(info) = source_objects.get(&entry.key) { - to_copy.push((&entry.key, info)); + to_copy.push((&entry.key, info, false)); } } else { skipped += 1; @@ -223,7 +246,9 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode DiffStatus::OnlySecond => { if args.remove { // Extra object at destination, remove it - to_remove.push(&entry.key); + if let Some(info) = target_objects.get(&entry.key) { + to_remove.push((&entry.key, info)); + } } } DiffStatus::Same => { @@ -240,20 +265,20 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode if !to_copy.is_empty() { formatter.println(&format!("Would copy {} object(s):", to_copy.len())); - for (key, info) in &to_copy { + for (key, info, _) in &to_copy { let size = info .size .map(|s| humansize::format_size(s as u64, humansize::BINARY)) .unwrap_or_default(); - formatter.println(&format!(" + {key} ({size})")); + formatter.println(&format!(" + {} ({size})", formatter.sanitize_text(key))); } formatter.println(""); } if !to_remove.is_empty() { formatter.println(&format!("Would remove {} object(s):", to_remove.len())); - for key in &to_remove { - formatter.println(&format!(" - {key}")); + for (key, _) in &to_remove { + formatter.println(&format!(" - {}", formatter.sanitize_text(key))); } formatter.println(""); } @@ -303,10 +328,16 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode let mut errors = 0; let parallel_limit = args.parallel.max(1); - let copy_semaphore = Arc::new(Semaphore::new(parallel_limit)); let mut copy_tasks: JoinSet<(String, Result<(), String>)> = JoinSet::new(); + let copy_context = MirrorTaskContext { + success_marker: "+", + operation: "copy", + quiet: args.quiet, + formatter: &formatter, + progress: overall_pb.as_ref(), + }; - for (key, _) in to_copy { + for (key, _, if_absent) in to_copy { let source_sep = if source_path.key.is_empty() || source_path.key.ends_with('/') { "" } else { @@ -332,60 +363,44 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode let key = key.to_string(); let source_client = Arc::clone(&source_client); let target_client = Arc::clone(&target_client); - let permit = copy_semaphore - .clone() - .acquire_owned() - .await - .expect("semaphore should not be closed"); copy_tasks.spawn(async move { - let _permit = permit; let result = copy_object_with_metadata( source_client.as_ref(), target_client.as_ref(), &source_full, &target_full, + if_absent, ) .await .map_err(|e| format!("Failed to copy {key}: {e}")); (key, result) }); - } - while let Some(task_result) = copy_tasks.join_next().await { - match task_result { - Ok((key, Ok(()))) => { - copied += 1; - if !args.quiet && !formatter.is_json() { - formatter.println(&format!("+ {key}")); - } - } - Ok((_, Err(message))) => { - errors += 1; - if !formatter.is_json() { - formatter.error(&message); - } - } - Err(join_error) => { - errors += 1; - if !formatter.is_json() { - formatter.error(&format!("Mirror copy worker failed: {join_error}")); - } - } + if copy_tasks.len() >= parallel_limit + && let Some(task_result) = copy_tasks.join_next().await + { + record_mirror_task(task_result, ©_context, &mut copied, &mut errors); } + } - if let Some(ref pb) = overall_pb { - pb.inc(1); - } + while let Some(task_result) = copy_tasks.join_next().await { + record_mirror_task(task_result, ©_context, &mut copied, &mut errors); } // Perform remove operations let mut removed = 0; - if args.remove { - let remove_semaphore = Arc::new(Semaphore::new(parallel_limit)); + if args.remove && errors == 0 { let mut remove_tasks: JoinSet<(String, Result<(), String>)> = JoinSet::new(); + let remove_context = MirrorTaskContext { + success_marker: "-", + operation: "remove", + quiet: args.quiet, + formatter: &formatter, + progress: overall_pb.as_ref(), + }; - for key in to_remove { + for (key, info) in to_remove { let sep = if target_path.key.is_empty() || target_path.key.ends_with('/') { "" } else { @@ -398,49 +413,35 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode ); let key = key.to_string(); + let etag = info.etag.clone(); let target_client = Arc::clone(&target_client); - let permit = remove_semaphore - .clone() - .acquire_owned() - .await - .expect("semaphore should not be closed"); remove_tasks.spawn(async move { - let _permit = permit; - let result = target_client - .delete_object(&target_full) - .await - .map(|_| ()) - .map_err(|e| format!("Failed to remove {key}: {e}")); + let result = match etag { + Some(etag) => { + target_client + .delete_object_if_match(&target_full, &etag) + .await + } + None => Err(rc_core::Error::Conflict(format!( + "Refusing to remove {key} because its ETag is unavailable" + ))), + } + .map_err(|e| format!("Failed to remove {key}: {e}")); (key, result) }); - } - while let Some(task_result) = remove_tasks.join_next().await { - match task_result { - Ok((key, Ok(()))) => { - removed += 1; - if !args.quiet && !formatter.is_json() { - formatter.println(&format!("- {key}")); - } - } - Ok((_, Err(message))) => { - errors += 1; - if !formatter.is_json() { - formatter.error(&message); - } - } - Err(join_error) => { - errors += 1; - if !formatter.is_json() { - formatter.error(&format!("Mirror remove worker failed: {join_error}")); - } - } + if remove_tasks.len() >= parallel_limit + && let Some(task_result) = remove_tasks.join_next().await + { + record_mirror_task(task_result, &remove_context, &mut removed, &mut errors); } + } - if let Some(ref pb) = overall_pb { - pb.inc(1); - } + while let Some(task_result) = remove_tasks.join_next().await { + record_mirror_task(task_result, &remove_context, &mut removed, &mut errors); } + } else if args.remove && errors > 0 && !formatter.is_json() { + formatter.error("Skipping removals because one or more copy operations failed"); } if let Some(pb) = overall_pb { @@ -473,6 +474,45 @@ pub async fn execute(args: MirrorArgs, output_config: OutputConfig) -> ExitCode } } +fn record_mirror_task( + task_result: Result<(String, Result<(), String>), JoinError>, + context: &MirrorTaskContext<'_>, + succeeded: &mut usize, + errors: &mut usize, +) { + match task_result { + Ok((key, Ok(()))) => { + *succeeded += 1; + if !context.quiet && !context.formatter.is_json() { + context.formatter.println(&format!( + "{} {}", + context.success_marker, + context.formatter.sanitize_text(&key) + )); + } + } + Ok((_, Err(message))) => { + *errors += 1; + if !context.formatter.is_json() { + context.formatter.error(&message); + } + } + Err(join_error) => { + *errors += 1; + if !context.formatter.is_json() { + context.formatter.error(&format!( + "Mirror {} worker failed: {join_error}", + context.operation + )); + } + } + } + + if let Some(progress) = context.progress { + progress.inc(1); + } +} + // Mirror should preserve source content type when metadata is available, but // still fall back to a plain byte copy if metadata lookup fails for any reason. async fn copy_object_with_metadata( @@ -480,6 +520,7 @@ async fn copy_object_with_metadata( target_client: &T, source_path: &RemotePath, target_path: &RemotePath, + if_absent: bool, ) -> rc_core::Result<()> where S: MirrorCopySource, @@ -501,7 +542,7 @@ where let data = source_client.get_object_for_mirror(source_path).await?; target_client - .put_object_for_mirror(target_path, data, content_type.as_deref()) + .put_object_for_mirror(target_path, data, content_type.as_deref(), if_absent) .await?; Ok(()) } @@ -568,7 +609,10 @@ fn compare_objects_internal( if let Some(target_info) = target.get(key) { // Object exists in both let is_same = source_info.size == target_info.size - && (source_info.etag == target_info.etag || source_info.etag.is_none()); + && matches!( + (&source_info.etag, &target_info.etag), + (Some(source_etag), Some(target_etag)) if source_etag == target_etag + ); let status = if is_same { DiffStatus::Same @@ -615,6 +659,45 @@ fn compare_objects_internal( entries } +fn mirror_locations_overlap( + source: &RemotePath, + target: &RemotePath, + source_endpoint: &str, + target_endpoint: &str, +) -> bool { + if source.bucket != target.bucket || !same_endpoint(source_endpoint, target_endpoint) { + return false; + } + + let source = source.key.trim_matches('/'); + let target = target.key.trim_matches('/'); + if source.is_empty() || target.is_empty() || source == target { + return true; + } + + target + .strip_prefix(source) + .is_some_and(|rest| rest.starts_with('/')) + || source + .strip_prefix(target) + .is_some_and(|rest| rest.starts_with('/')) +} + +fn same_endpoint(first: &str, second: &str) -> bool { + match (url::Url::parse(first), url::Url::parse(second)) { + (Ok(first), Ok(second)) => { + first.scheme().eq_ignore_ascii_case(second.scheme()) + && first.host_str().zip(second.host_str()).is_some_and( + |(first_host, second_host)| first_host.eq_ignore_ascii_case(second_host), + ) + && first.port_or_known_default() == second.port_or_known_default() + } + _ => first + .trim_end_matches('/') + .eq_ignore_ascii_case(second.trim_end_matches('/')), + } +} + #[cfg(test)] mod tests { use super::*; @@ -688,6 +771,7 @@ mod tests { key: String, content_type: Option, data: Vec, + if_absent: bool, } #[derive(Debug, Default)] @@ -701,6 +785,7 @@ mod tests { path: &RemotePath, data: Vec, content_type: Option<&str>, + if_absent: bool, ) -> rc_core::Result { self.put_calls .lock() @@ -709,6 +794,7 @@ mod tests { key: path.key.clone(), content_type: content_type.map(ToOwned::to_owned), data: data.clone(), + if_absent, }); Ok(ObjectInfo::file(path.key.clone(), data.len() as i64)) @@ -722,7 +808,7 @@ mod tests { let source_path = RemotePath::new("stage", "images", "photo.jpg"); let target_path = RemotePath::new("prod", "images", "photo.jpg"); - copy_object_with_metadata(&source, &target, &source_path, &target_path) + copy_object_with_metadata(&source, &target, &source_path, &target_path, true) .await .expect("copy should succeed"); @@ -752,6 +838,7 @@ mod tests { key: "photo.jpg".to_string(), content_type: Some("image/jpeg".to_string()), data: b"jpeg-bytes".to_vec(), + if_absent: true, }] ); } @@ -763,7 +850,7 @@ mod tests { let source_path = RemotePath::new("stage", "docs", "readme.txt"); let target_path = RemotePath::new("prod", "docs", "readme.txt"); - copy_object_with_metadata(&source, &target, &source_path, &target_path) + copy_object_with_metadata(&source, &target, &source_path, &target_path, false) .await .expect("copy should succeed"); @@ -777,6 +864,7 @@ mod tests { key: "readme.txt".to_string(), content_type: None, data: b"plain-bytes".to_vec(), + if_absent: false, }] ); } @@ -791,7 +879,7 @@ mod tests { let source_path = RemotePath::new("stage", "docs", "readme.txt"); let target_path = RemotePath::new("prod", "docs", "readme.txt"); - copy_object_with_metadata(&source, &target, &source_path, &target_path) + copy_object_with_metadata(&source, &target, &source_path, &target_path, false) .await .expect("copy should succeed"); @@ -821,6 +909,7 @@ mod tests { key: "readme.txt".to_string(), content_type: None, data: b"plain-bytes".to_vec(), + if_absent: false, }] ); } @@ -951,6 +1040,50 @@ mod tests { assert_eq!(entries[0].status, DiffStatus::Different); } + #[test] + fn test_compare_missing_etag_is_different() { + let source = HashMap::from([( + "file.txt".to_string(), + FileInfo { + size: Some(100), + modified: None, + etag: None, + }, + )]); + let target = HashMap::from([( + "file.txt".to_string(), + FileInfo { + size: Some(100), + modified: None, + etag: Some("target-etag".to_string()), + }, + )]); + + let entries = compare_objects_internal(&source, &target); + + assert_eq!(entries[0].status, DiffStatus::Different); + } + + #[test] + fn mirror_rejects_overlapping_prefixes_across_aliases_for_same_endpoint() { + let source = RemotePath::new("source", "bucket", "data/source"); + let nested = RemotePath::new("target", "bucket", "data/source/archive"); + let adjacent = RemotePath::new("target", "bucket", "data/source-archive"); + + assert!(mirror_locations_overlap( + &source, + &nested, + "https://s3.example.com/", + "https://S3.EXAMPLE.COM:443" + )); + assert!(!mirror_locations_overlap( + &source, + &adjacent, + "https://s3.example.com", + "https://s3.example.com" + )); + } + #[test] fn test_mirror_args_defaults() { let args = MirrorArgs { diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 7ad2676..34606ca 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -7,10 +7,11 @@ use std::io::{IsTerminal, stderr, stdout}; use clap::{Parser, Subcommand, ValueEnum}; -use rc_core::{RequestHeader, set_global_request_headers}; +use rc_core::config::Defaults; +use rc_core::{ConfigManager, RequestHeader, set_global_request_headers}; use crate::exit_code::ExitCode; -use crate::output::OutputConfig; +use crate::output::{Formatter, OutputConfig}; mod admin; mod alias; @@ -106,6 +107,7 @@ struct GlobalOutputOptions { format: Option, json: bool, no_color: bool, + force_color: bool, no_progress: bool, quiet: bool, } @@ -116,11 +118,36 @@ impl GlobalOutputOptions { format: cli.format, json: cli.json, no_color: cli.no_color, + force_color: false, no_progress: cli.no_progress, quiet: cli.quiet, } } + fn apply_defaults(mut self, defaults: &Defaults) -> Result { + if self.format.is_none() && !self.json { + self.format = Some(match defaults.output.as_str() { + "human" => OutputFormat::Human, + "json" => OutputFormat::Json, + value => return Err(format!("Invalid default output format '{value}'")), + }); + } + + if !self.no_color { + match defaults.color.as_str() { + "auto" => {} + "always" => self.force_color = true, + "never" => self.no_color = true, + value => return Err(format!("Invalid default color mode '{value}'")), + } + } + if !defaults.progress { + self.no_progress = true; + } + + Ok(self) + } + fn resolve(self, behavior: OutputBehavior) -> OutputConfig { let stdout_is_tty = stdout().is_terminal(); let stderr_is_tty = stderr().is_terminal(); @@ -142,7 +169,7 @@ impl GlobalOutputOptions { OutputConfig { json, - no_color: self.no_color || !stdout_is_tty || json, + no_color: self.no_color || (!self.force_color && !stdout_is_tty) || json, no_progress: self.no_progress || !stderr_is_tty || json, quiet: self.quiet, } @@ -258,7 +285,41 @@ pub enum Commands { /// Execute the CLI command and return an exit code pub async fn execute(cli: Cli) -> ExitCode { set_global_request_headers(cli.request_headers.clone()); - let output_options = GlobalOutputOptions::from_cli(&cli); + let cli_output_options = GlobalOutputOptions::from_cli(&cli); + let defaults = match ConfigManager::new() { + Ok(manager) if manager.config_path().exists() => match manager.load() { + Ok(config) => Some(config.defaults), + Err(error) => { + return Formatter::new(cli_output_options.resolve(OutputBehavior::HumanDefault)) + .fail( + ExitCode::GeneralError, + &format!("Failed to load configuration: {error}"), + ); + } + }, + Ok(_) => None, + Err(error) => { + return Formatter::new(cli_output_options.resolve(OutputBehavior::HumanDefault)).fail( + ExitCode::GeneralError, + &format!("Failed to load configuration: {error}"), + ); + } + }; + let output_options = cli_output_options; + let output_options = if let Some(defaults) = &defaults { + match output_options.apply_defaults(defaults) { + Ok(options) => options, + Err(error) => { + return Formatter::new(cli_output_options.resolve(OutputBehavior::HumanDefault)) + .fail( + ExitCode::GeneralError, + &format!("Failed to load configuration: {error}"), + ); + } + } + } else { + output_options + }; match cli.command { Commands::Alias(cmd) => { @@ -390,6 +451,7 @@ mod tests { format: None, json: false, no_color: false, + force_color: false, no_progress: false, quiet: false, }; @@ -404,6 +466,7 @@ mod tests { format: None, json: false, no_color: false, + force_color: false, no_progress: false, quiet: false, }; @@ -418,6 +481,7 @@ mod tests { format: Some(OutputFormat::Human), json: true, no_color: false, + force_color: false, no_progress: false, quiet: false, }; @@ -432,6 +496,7 @@ mod tests { format: Some(OutputFormat::Human), json: false, no_color: false, + force_color: false, no_progress: false, quiet: false, }; @@ -446,6 +511,7 @@ mod tests { format: Some(OutputFormat::Auto), json: false, no_color: false, + force_color: false, no_progress: false, quiet: false, }; @@ -454,6 +520,50 @@ mod tests { assert_eq!(resolved.json, !std::io::stdout().is_terminal()); } + #[test] + fn configured_defaults_control_output_color_and_progress() { + let options = GlobalOutputOptions { + format: None, + json: false, + no_color: false, + force_color: false, + no_progress: false, + quiet: false, + }; + let defaults = Defaults { + output: "json".to_string(), + color: "never".to_string(), + progress: false, + }; + + let resolved = options + .apply_defaults(&defaults) + .expect("valid defaults") + .resolve(OutputBehavior::HumanDefault); + + assert!(resolved.json); + assert!(resolved.no_color); + assert!(resolved.no_progress); + } + + #[test] + fn invalid_configured_defaults_are_rejected() { + let options = GlobalOutputOptions { + format: None, + json: false, + no_color: false, + force_color: false, + no_progress: false, + quiet: false, + }; + let defaults = Defaults { + output: "yaml".to_string(), + ..Defaults::default() + }; + + assert!(options.apply_defaults(&defaults).is_err()); + } + #[test] fn cli_accepts_global_custom_amz_header() { let cli = Cli::try_parse_from([ diff --git a/crates/cli/src/commands/mv.rs b/crates/cli/src/commands/mv.rs index 388eb53..16ae50a 100644 --- a/crates/cli/src/commands/mv.rs +++ b/crates/cli/src/commands/mv.rs @@ -177,6 +177,10 @@ async fn move_s3_to_local( ) -> ExitCode { use crate::commands::cp; + if args.recursive { + return move_s3_prefix_to_local(src, dst, args, formatter).await; + } + // First, copy S3 to local let cp_args = cp::CpArgs { source: format!("{}/{}/{}", src.alias, src.bucket, src.key), @@ -241,6 +245,140 @@ async fn move_s3_to_local( ExitCode::Success } +async fn move_s3_prefix_to_local( + src: &RemotePath, + dst: &Path, + args: &MvArgs, + formatter: &Formatter, +) -> ExitCode { + use crate::commands::cp; + + let alias_manager = match AliasManager::new() { + Ok(manager) => manager, + Err(error) => { + formatter.error(&format!("Failed to load aliases: {error}")); + return ExitCode::GeneralError; + } + }; + let alias = match alias_manager.get(&src.alias) { + Ok(alias) => alias, + Err(_) => { + formatter.error(&format!("Alias '{}' not found", src.alias)); + return ExitCode::NotFound; + } + }; + let client = match S3Client::new(alias).await { + Ok(client) => client, + Err(error) => { + formatter.error(&format!("Failed to create S3 client: {error}")); + return ExitCode::NetworkError; + } + }; + + let mut continuation_token = None; + let mut objects = Vec::new(); + loop { + let result = match client + .list_objects( + src, + ListOptions { + recursive: true, + max_keys: Some(1000), + continuation_token: continuation_token.clone(), + ..Default::default() + }, + ) + .await + { + Ok(result) => result, + Err(error) => { + formatter.error(&format!("Failed to list source objects: {error}")); + return ExitCode::NetworkError; + } + }; + + objects.extend(result.items.into_iter().filter(|item| !item.is_dir)); + if !result.truncated { + break; + } + continuation_token = result.continuation_token; + } + + let cp_args = cp::CpArgs { + source: src.to_string(), + target: dst.to_string_lossy().to_string(), + recursive: true, + preserve: false, + continue_on_error: args.continue_on_error, + overwrite: true, + dry_run: args.dry_run, + storage_class: None, + content_type: None, + enc_s3: Vec::new(), + enc_kms: Vec::new(), + }; + let mut errors = 0usize; + + for item in objects { + let relative = match cp::safe_download_relative_path(&item.key, &src.key) { + Ok(relative) => relative, + Err(error) => { + errors += 1; + formatter.error(&format!( + "Refusing unsafe object key '{}': {error}", + item.key + )); + if !args.continue_on_error { + return ExitCode::UsageError; + } + continue; + } + }; + let object = RemotePath::new(&src.alias, &src.bucket, &item.key); + let target = match cp::safe_download_destination(dst, &relative).await { + Ok(target) => target, + Err(error) => { + errors += 1; + formatter.error(&format!( + "Refusing unsafe destination for '{}': {error}", + item.key + )); + if !args.continue_on_error { + return ExitCode::UsageError; + } + continue; + } + }; + let result = cp::download_file(&client, &object, &target, &cp_args, formatter).await; + if result != ExitCode::Success { + errors += 1; + if !args.continue_on_error { + return result; + } + continue; + } + + if !args.dry_run + && let Err(error) = client.delete_object(&object).await + { + errors += 1; + formatter.error(&format!( + "Downloaded but failed to delete source '{}': {error}", + object + )); + if !args.continue_on_error { + return ExitCode::GeneralError; + } + } + } + + if errors == 0 { + ExitCode::Success + } else { + ExitCode::GeneralError + } +} + async fn move_s3_to_s3( src: &RemotePath, dst: &RemotePath, @@ -263,6 +401,11 @@ async fn move_s3_to_s3( return ExitCode::UnsupportedFeature; } + if args.recursive && remote_prefixes_overlap(src, dst) { + formatter.error("Recursive move source and destination prefixes must not overlap."); + return ExitCode::UsageError; + } + let alias_manager = match AliasManager::new() { Ok(am) => am, Err(e) => { @@ -301,6 +444,7 @@ async fn move_s3_to_s3( let mut moved_count = 0usize; let mut error_count = 0usize; let src_prefix = src.key.clone(); + let mut objects = Vec::new(); loop { let list_opts = ListOptions { @@ -317,65 +461,7 @@ async fn move_s3_to_s3( } }; - for item in &list_result.items { - if item.is_dir { - continue; - } - - let relative = if src_prefix.is_empty() { - item.key.clone() - } else if let Some(rest) = item.key.strip_prefix(&src_prefix) { - rest.trim_start_matches('/').to_string() - } else { - item.key.clone() - }; - - let target_key = if dst.key.is_empty() { - relative.clone() - } else if dst.key.ends_with('/') { - format!("{}{}", dst.key, relative) - } else { - format!("{}/{}", dst.key, relative) - }; - - let src_obj = RemotePath::new(&src.alias, &src.bucket, &item.key); - let dst_obj = RemotePath::new(&dst.alias, &dst.bucket, &target_key); - let src_obj_display = src_obj.to_string(); - let dst_obj_display = dst_obj.to_string(); - - match client - .copy_object(&src_obj, &dst_obj, encryption.as_ref()) - .await - { - Ok(_) => match client.delete_object(&src_obj).await { - Ok(()) => { - moved_count += 1; - if !formatter.is_json() { - formatter - .println(&format!("{src_obj_display} -> {dst_obj_display}")); - } - } - Err(e) => { - error_count += 1; - formatter.error(&format!( - "Copied but failed to delete source '{src_obj_display}': {e}" - )); - if !args.continue_on_error { - return ExitCode::GeneralError; - } - } - }, - Err(e) => { - error_count += 1; - formatter.error(&format!( - "Failed to move '{src_obj_display}' -> '{dst_obj_display}': {e}" - )); - if !args.continue_on_error { - return ExitCode::NetworkError; - } - } - } - } + objects.extend(list_result.items.into_iter().filter(|item| !item.is_dir)); if !list_result.truncated { break; @@ -391,6 +477,81 @@ async fn move_s3_to_s3( }; } + for item in &objects { + let relative = if src_prefix.is_empty() { + item.key.clone() + } else if let Some(rest) = item.key.strip_prefix(&src_prefix) { + rest.trim_start_matches('/').to_string() + } else { + error_count += 1; + formatter.error(&format!( + "Source listing returned key '{}' outside prefix '{}'", + item.key, src_prefix + )); + if !args.continue_on_error { + return ExitCode::GeneralError; + } + continue; + }; + + if relative.is_empty() { + error_count += 1; + formatter.error(&format!( + "Cannot derive a destination key for source '{}'", + item.key + )); + if !args.continue_on_error { + return ExitCode::UsageError; + } + continue; + } + + let target_key = if dst.key.is_empty() { + relative.clone() + } else if dst.key.ends_with('/') { + format!("{}{}", dst.key, relative) + } else { + format!("{}/{}", dst.key, relative) + }; + + let src_obj = RemotePath::new(&src.alias, &src.bucket, &item.key); + let dst_obj = RemotePath::new(&dst.alias, &dst.bucket, &target_key); + let src_obj_display = src_obj.to_string(); + let dst_obj_display = dst_obj.to_string(); + + match client + .copy_object(&src_obj, &dst_obj, encryption.as_ref()) + .await + { + Ok(_) => match client.delete_object(&src_obj).await { + Ok(()) => { + moved_count += 1; + if !formatter.is_json() { + formatter.println(&format!("{src_obj_display} -> {dst_obj_display}")); + } + } + Err(e) => { + error_count += 1; + formatter.error(&format!( + "Copied but failed to delete source '{src_obj_display}': {e}" + )); + if !args.continue_on_error { + return ExitCode::GeneralError; + } + } + }, + Err(e) => { + error_count += 1; + formatter.error(&format!( + "Failed to move '{src_obj_display}' -> '{dst_obj_display}': {e}" + )); + if !args.continue_on_error { + return ExitCode::NetworkError; + } + } + } + } + if formatter.is_json() { #[derive(Serialize)] struct MvRecursiveOutput { @@ -465,6 +626,25 @@ async fn move_s3_to_s3( } } +fn remote_prefixes_overlap(source: &RemotePath, target: &RemotePath) -> bool { + if source.alias != target.alias || source.bucket != target.bucket { + return false; + } + + let source = source.key.trim_matches('/'); + let target = target.key.trim_matches('/'); + if source.is_empty() || target.is_empty() || source == target { + return true; + } + + target + .strip_prefix(source) + .is_some_and(|rest| rest.starts_with('/')) + || source + .strip_prefix(target) + .is_some_and(|rest| rest.starts_with('/')) +} + #[cfg(test)] mod tests { use super::*; @@ -584,6 +764,18 @@ mod tests { assert_eq!(args.enc_kms.len(), 1); } + #[test] + fn recursive_move_rejects_overlapping_remote_prefixes() { + let source = RemotePath::new("local", "bucket", "source/"); + let nested_target = RemotePath::new("local", "bucket", "source/archive/"); + let parent_target = RemotePath::new("local", "bucket", ""); + let separate_target = RemotePath::new("local", "bucket", "archive/"); + + assert!(remote_prefixes_overlap(&source, &nested_target)); + assert!(remote_prefixes_overlap(&source, &parent_target)); + assert!(!remote_prefixes_overlap(&source, &separate_target)); + } + #[test] fn test_mv_output_serialization() { let output = MvOutput { diff --git a/crates/cli/src/commands/pipe.rs b/crates/cli/src/commands/pipe.rs index 3de9e5b..0e7a309 100644 --- a/crates/cli/src/commands/pipe.rs +++ b/crates/cli/src/commands/pipe.rs @@ -47,6 +47,12 @@ struct PipeOutput { /// Execute the pipe command pub async fn execute(args: PipeArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.storage_class.is_some() { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--storage-class is not implemented for pipe", + ); + } let encryption = match (args.enc_s3, args.enc_kms.as_deref()) { (true, None) => Some(ObjectEncryptionRequest::SseS3), (false, Some(key_id)) => Some(ObjectEncryptionRequest::SseKms { diff --git a/crates/cli/src/commands/rb.rs b/crates/cli/src/commands/rb.rs index 7483848..21b4b5f 100644 --- a/crates/cli/src/commands/rb.rs +++ b/crates/cli/src/commands/rb.rs @@ -37,6 +37,13 @@ struct RbOutput { pub async fn execute(args: RbArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.force || args.dangerous { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--force and --dangerous are not implemented for bucket removal", + ); + } + // Parse the target path let (alias_name, bucket) = match parse_rb_path(&args.target) { Ok(parsed) => parsed, @@ -85,9 +92,6 @@ pub async fn execute(args: RbArgs, output_config: OutputConfig) -> ExitCode { } } - // TODO: If --force is specified, delete all objects first - // This will be implemented in Phase 3 when we have delete_object - // Delete the bucket match client.delete_bucket(&bucket).await { Ok(()) => { @@ -108,15 +112,9 @@ pub async fn execute(args: RbArgs, output_config: OutputConfig) -> ExitCode { Err(e) => { let err_str = e.to_string(); if err_str.contains("BucketNotEmpty") { - if args.force { - formatter.error(&format!( - "Bucket '{alias_name}/{bucket}' is not empty. --force with object deletion not yet implemented." - )); - } else { - formatter.error(&format!( - "Bucket '{alias_name}/{bucket}' is not empty. Use --force to delete all objects first." - )); - } + formatter.error(&format!( + "Bucket '{alias_name}/{bucket}' is not empty. Remove its contents first." + )); ExitCode::Conflict } else if err_str.contains("NoSuchBucket") || err_str.contains("NotFound") { formatter.error(&format!("Bucket '{alias_name}/{bucket}' does not exist")); diff --git a/crates/cli/src/commands/rm.rs b/crates/cli/src/commands/rm.rs index 5b719f1..310b94b 100644 --- a/crates/cli/src/commands/rm.rs +++ b/crates/cli/src/commands/rm.rs @@ -6,6 +6,7 @@ use clap::Args; use rc_core::{AliasManager, ListOptions, ObjectStore as _, RemotePath}; use rc_s3::{DeleteRequestOptions, S3Client}; use serde::Serialize; +use std::collections::HashSet; use crate::exit_code::ExitCode; use crate::output::{Formatter, OutputConfig}; @@ -66,6 +67,13 @@ struct RmOutput { pub async fn execute(args: RmArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.incomplete || args.versions || args.bypass { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--incomplete, --versions, and --bypass are not implemented; refusing to continue with a silently ignored destructive option", + ); + } + // Process each path let mut all_deleted = Vec::new(); let mut all_failed = Vec::new(); @@ -129,6 +137,15 @@ async fn process_rm_path( } }; + if let Err(error) = validate_removal_scope(&key, args.recursive) { + let code = formatter.fail_with_suggestion( + ExitCode::UsageError, + &error, + "Add --recursive only after verifying the bucket or prefix to remove.", + ); + return Err((code, vec![path_str.to_string()])); + } + // Load alias let alias_manager = match AliasManager::new() { Ok(am) => am, @@ -162,10 +179,7 @@ async fn process_rm_path( } }; - let is_prefix = key.ends_with('/') || key.is_empty(); - - // If recursive or prefix, list and delete all matching objects - if args.recursive || is_prefix { + if args.recursive { delete_recursive(&client, &alias_name, &bucket, &key, args, formatter).await } else { // Delete single object @@ -341,7 +355,9 @@ async fn delete_recursive( .await { Ok(deleted_keys) => { - for key in &deleted_keys { + let (confirmed_keys, failed_keys) = + partition_delete_results(&chunk_keys, deleted_keys); + for key in &confirmed_keys { let full_path = format!("{alias_name}/{bucket}/{key}"); if !formatter.is_json() { let styled_path = formatter.style_file(&full_path); @@ -349,6 +365,11 @@ async fn delete_recursive( } deleted.push(full_path); } + failed.extend( + failed_keys + .into_iter() + .map(|key| format!("{alias_name}/{bucket}/{key}")), + ); } Err(e) => { formatter.error_with_code( @@ -408,6 +429,24 @@ fn delete_request_options(args: &RmArgs) -> DeleteRequestOptions { } } +fn validate_removal_scope(key: &str, recursive: bool) -> Result<(), String> { + if !recursive && (key.is_empty() || key.ends_with('/')) { + return Err("Bucket and prefix removal requires the explicit --recursive flag".to_string()); + } + Ok(()) +} + +fn partition_delete_results( + requested: &[String], + deleted: Vec, +) -> (Vec, Vec) { + let deleted: HashSet = deleted.into_iter().collect(); + requested + .iter() + .cloned() + .partition(|key| deleted.contains(key)) +} + #[cfg(test)] mod tests { use super::*; @@ -451,6 +490,25 @@ mod tests { assert!(parse_rm_path("/mybucket/file.txt").is_err()); } + #[test] + fn recursive_targets_require_explicit_recursive_flag() { + assert!(validate_removal_scope("", false).is_err()); + assert!(validate_removal_scope("prefix/", false).is_err()); + assert!(validate_removal_scope("prefix/", true).is_ok()); + assert!(validate_removal_scope("object.txt", false).is_ok()); + } + + #[test] + fn partial_batch_delete_marks_missing_results_as_failed() { + let requested = vec!["deleted.txt".to_string(), "denied.txt".to_string()]; + let deleted = vec!["deleted.txt".to_string()]; + + let (confirmed, failed) = partition_delete_results(&requested, deleted); + + assert_eq!(confirmed, vec!["deleted.txt"]); + assert_eq!(failed, vec!["denied.txt"]); + } + #[test] fn test_delete_request_options_enable_force_delete_for_purge() { let args = RmArgs { diff --git a/crates/cli/src/commands/share.rs b/crates/cli/src/commands/share.rs index 5b04b3f..3776c57 100644 --- a/crates/cli/src/commands/share.rs +++ b/crates/cli/src/commands/share.rs @@ -170,15 +170,19 @@ fn parse_expiration(s: &str) -> Result { .parse() .map_err(|_| format!("Invalid expiration number: {num_str}"))?; - let seconds = match suffix.to_lowercase().as_str() { - "s" => num, - "m" => num * 60, - "h" => num * 3600, - "d" => num * 86400, - "w" => num * 604800, + let multiplier = match suffix.to_lowercase().as_str() { + "s" => 1, + "m" => 60, + "h" => 3600, + "d" => 86400, + "w" => 604800, _ => return Err(format!("Unknown expiration suffix: {suffix}")), }; + let seconds = num + .checked_mul(multiplier) + .ok_or_else(|| "Expiration is too large".to_string())?; + Ok(seconds) } @@ -246,6 +250,7 @@ mod tests { assert!(parse_expiration("").is_err()); assert!(parse_expiration("abc").is_err()); assert!(parse_expiration("1x").is_err()); + assert!(parse_expiration("30500568904944w").is_err()); } #[test] diff --git a/crates/cli/src/commands/stat.rs b/crates/cli/src/commands/stat.rs index 9c0621d..ec4a007 100644 --- a/crates/cli/src/commands/stat.rs +++ b/crates/cli/src/commands/stat.rs @@ -81,6 +81,13 @@ fn build_display_metadata(info: &ObjectInfo) -> BTreeMap { pub async fn execute(args: StatArgs, output_config: OutputConfig) -> ExitCode { let formatter = Formatter::new(output_config); + if args.version_id.is_some() || args.rewind.is_some() { + return formatter.fail( + ExitCode::UnsupportedFeature, + "--version-id and --rewind are not implemented for stat", + ); + } + // Parse the path let (alias_name, bucket, key) = match parse_stat_path(&args.path) { Ok(parsed) => parsed, @@ -130,7 +137,7 @@ pub async fn execute(args: StatArgs, output_config: OutputConfig) -> ExitCode { etag: info.etag.clone(), content_type: info.content_type.clone(), storage_class: info.storage_class.clone(), - version_id: args.version_id, + version_id: None, metadata: info .metadata .as_ref() diff --git a/crates/cli/src/output/formatter.rs b/crates/cli/src/output/formatter.rs index 3b4a740..a3d5820 100644 --- a/crates/cli/src/output/formatter.rs +++ b/crates/cli/src/output/formatter.rs @@ -289,12 +289,18 @@ impl Formatter { /// Style a directory name (blue + bold) pub fn style_dir(&self, text: &str) -> String { - self.theme.dir.apply_to(text).to_string() + self.theme + .dir + .apply_to(self.sanitize_text(text)) + .to_string() } /// Style a file name (default) pub fn style_file(&self, text: &str) -> String { - self.theme.file.apply_to(text).to_string() + self.theme + .file + .apply_to(self.sanitize_text(text)) + .to_string() } /// Style a file size (green) @@ -309,17 +315,51 @@ impl Formatter { /// Style a property key (cyan) pub fn style_key(&self, text: &str) -> String { - self.theme.key.apply_to(text).to_string() + self.theme + .key + .apply_to(self.sanitize_text(text)) + .to_string() } /// Style a URL/endpoint (cyan + underline) pub fn style_url(&self, text: &str) -> String { - self.theme.url.apply_to(text).to_string() + self.theme + .url + .apply_to(self.sanitize_text(text)) + .to_string() } /// Style an alias/bucket name (bold) pub fn style_name(&self, text: &str) -> String { - self.theme.name.apply_to(text).to_string() + self.theme + .name + .apply_to(self.sanitize_text(text)) + .to_string() + } + + pub fn sanitize_text(&self, text: &str) -> String { + let mut sanitized = String::with_capacity(text.len()); + for character in text.chars() { + match character { + '\n' => sanitized.push_str("\\n"), + '\r' => sanitized.push_str("\\r"), + '\t' => sanitized.push_str("\\t"), + character + if character.is_control() + || matches!( + character, + '\u{200e}' + | '\u{200f}' + | '\u{202a}'..='\u{202e}' + | '\u{2066}'..='\u{2069}' + ) => + { + sanitized.push_str(&format!("\\u{{{:x}}}", character as u32)); + } + character => sanitized.push(character), + } + } + sanitized } /// Style tree branch characters (dim) @@ -407,7 +447,7 @@ impl Formatter { ); } else { let cross = self.theme.error.apply_to("✗"); - eprintln!("{cross} {}", descriptor.message); + eprintln!("{cross} {}", self.sanitize_text(&descriptor.message)); } } @@ -418,7 +458,7 @@ impl Formatter { } let warn_icon = self.theme.warning.apply_to("⚠"); - eprintln!("{warn_icon} {message}"); + eprintln!("{warn_icon} {}", self.sanitize_text(message)); } /// Output JSON directly @@ -479,6 +519,16 @@ mod tests { assert!(!formatter.colors_enabled()); } + #[test] + fn test_sanitize_text_escapes_terminal_control_characters() { + let formatter = Formatter::default(); + + assert_eq!( + formatter.sanitize_text("safe\n\u{1b}[31m\u{202e}"), + "safe\\n\\u{1b}[31m\\u{202e}" + ); + } + #[test] fn test_error_descriptor_from_code_sets_defaults() { let descriptor = diff --git a/crates/cli/tests/golden.rs b/crates/cli/tests/golden.rs index bea5ac7..0edcd9e 100644 --- a/crates/cli/tests/golden.rs +++ b/crates/cli/tests/golden.rs @@ -9,23 +9,8 @@ use std::process::Command; -/// Get the path to the rc binary -fn rc_binary() -> String { - // Use cargo to build and get the binary path - let output = Command::new("cargo") - .args(["build", "--release", "-p", "rustfs-cli"]) - .output() - .expect("Failed to build rc binary"); - - if !output.status.success() { - panic!( - "Failed to build rc binary: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - // Return path to binary - env!("CARGO_MANIFEST_DIR").to_string() + "/../../target/release/rc" +fn rc_binary() -> &'static str { + env!("CARGO_BIN_EXE_rc") } mod alias_tests { @@ -259,10 +244,8 @@ mod s3_tests { #[test] fn test_mb_json() { - let Some((temp_dir, config_dir)) = setup_test_env_with_alias() else { - eprintln!("Skipping test: S3 environment not configured"); - return; - }; + let (temp_dir, config_dir) = + setup_test_env_with_alias().expect("S3 golden test setup failed"); let bucket = unique_bucket_name(); let output = Command::new(rc_binary()) @@ -292,10 +275,8 @@ mod s3_tests { #[test] fn test_rb_json() { - let Some((temp_dir, config_dir)) = setup_test_env_with_alias() else { - eprintln!("Skipping test: S3 environment not configured"); - return; - }; + let (temp_dir, config_dir) = + setup_test_env_with_alias().expect("S3 golden test setup failed"); let bucket = unique_bucket_name(); @@ -326,10 +307,8 @@ mod s3_tests { #[test] fn test_ls_empty_bucket_json() { - let Some((temp_dir, config_dir)) = setup_test_env_with_alias() else { - eprintln!("Skipping test: S3 environment not configured"); - return; - }; + let (temp_dir, config_dir) = + setup_test_env_with_alias().expect("S3 golden test setup failed"); let bucket = unique_bucket_name(); @@ -368,10 +347,8 @@ mod s3_tests { #[test] fn test_ls_with_objects_json() { - let Some((temp_dir, config_dir)) = setup_test_env_with_alias() else { - eprintln!("Skipping test: S3 environment not configured"); - return; - }; + let (temp_dir, config_dir) = + setup_test_env_with_alias().expect("S3 golden test setup failed"); let bucket = unique_bucket_name(); @@ -383,7 +360,7 @@ mod s3_tests { .expect("Failed to create bucket"); // Upload a test file using pipe - let output = Command::new(rc_binary()) + let upload = Command::new(rc_binary()) .args(["pipe", &format!("test/{}/test-file.txt", bucket), "--json"]) .env("RC_CONFIG_DIR", &config_dir) .stdin(std::process::Stdio::piped()) @@ -393,21 +370,12 @@ mod s3_tests { .and_then(|mut child| { use std::io::Write; if let Some(ref mut stdin) = child.stdin { - stdin.write_all(b"Hello, World!").ok(); + stdin.write_all(b"Hello, World!")?; } child.wait_with_output() - }); - - if output.is_err() { - // Cleanup and skip - Command::new(rc_binary()) - .args(["rb", &format!("test/{}", bucket), "--force", "--json"]) - .env("RC_CONFIG_DIR", &config_dir) - .output() - .ok(); - eprintln!("Skipping test: pipe command failed"); - return; - } + }) + .expect("pipe upload should execute"); + assert!(upload.status.success(), "pipe upload should succeed"); // List bucket with object let output = Command::new(rc_binary()) @@ -444,10 +412,8 @@ mod s3_tests { #[test] fn test_stat_json() { - let Some((temp_dir, config_dir)) = setup_test_env_with_alias() else { - eprintln!("Skipping test: S3 environment not configured"); - return; - }; + let (temp_dir, config_dir) = + setup_test_env_with_alias().expect("S3 golden test setup failed"); let bucket = unique_bucket_name(); @@ -469,20 +435,12 @@ mod s3_tests { .and_then(|mut child| { use std::io::Write; if let Some(ref mut stdin) = child.stdin { - stdin.write_all(b"Test content for stat").ok(); + stdin.write_all(b"Test content for stat")?; } child.wait_with_output() - }); - - if upload.is_err() { - Command::new(rc_binary()) - .args(["rb", &format!("test/{}", bucket), "--force", "--json"]) - .env("RC_CONFIG_DIR", &config_dir) - .output() - .ok(); - eprintln!("Skipping test: pipe command failed"); - return; - } + }) + .expect("pipe upload should execute"); + assert!(upload.status.success(), "pipe upload should succeed"); // Get stat let output = Command::new(rc_binary()) @@ -491,14 +449,13 @@ mod s3_tests { .output() .expect("Failed to execute rc"); - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let json: serde_json::Value = - serde_json::from_str(&stdout).expect("Output should be valid JSON"); + assert!(output.status.success(), "stat should succeed"); + let stdout = String::from_utf8_lossy(&output.stdout); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("Output should be valid JSON"); - assert!(json["key"].is_string()); - assert!(json["size_bytes"].is_number()); - } + assert!(json["key"].is_string()); + assert!(json["size_bytes"].is_number()); // Cleanup Command::new(rc_binary()) diff --git a/crates/cli/tests/integration.rs b/crates/cli/tests/integration.rs index ef12164..01dee03 100644 --- a/crates/cli/tests/integration.rs +++ b/crates/cli/tests/integration.rs @@ -225,10 +225,7 @@ mod bucket_operations { fn test_create_and_delete_bucket() { let (endpoint, access_key, secret_key) = match get_test_config() { Some(c) => c, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let config_dir = tempfile::tempdir().expect("Failed to create temp dir"); @@ -295,10 +292,7 @@ mod bucket_operations { fn test_mb_ignore_existing_first_and_second_run() { let (endpoint, access_key, secret_key) = match get_test_config() { Some(c) => c, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let config_dir = tempfile::tempdir().expect("Failed to create temp dir"); @@ -375,10 +369,7 @@ mod bucket_operations { fn test_bucket_cors_set_accepts_stdin_source() { let (config_dir, bucket_name) = match setup_with_alias("corsstdin") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let cors_config = r#"{ @@ -462,10 +453,7 @@ mod object_operations { fn test_upload_and_download_small_file() { let (config_dir, bucket_name) = match setup_with_alias("small") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create a small test file @@ -541,10 +529,7 @@ mod object_operations { fn test_upload_download_large_file_multipart() { let (config_dir, bucket_name) = match setup_with_alias("large") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create a large file (15 MiB to trigger multipart with 5 MiB parts) @@ -654,10 +639,7 @@ mod object_operations { fn test_copy_object_between_paths() { let (config_dir, bucket_name) = match setup_with_alias("copy") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create test file @@ -711,10 +693,7 @@ mod object_operations { fn test_move_object() { let (config_dir, bucket_name) = match setup_with_alias("move") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create and upload test file @@ -767,10 +746,7 @@ mod object_operations { fn test_move_recursive_prefix_s3_to_s3() { let (config_dir, bucket_name) = match setup_with_alias("mvrec") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let source_files = ["src/dir/a.txt", "src/dir/sub/b.txt"]; @@ -849,10 +825,7 @@ mod object_operations { fn test_delete_object() { let (config_dir, bucket_name) = match setup_with_alias("delete") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create and upload test file @@ -913,10 +886,7 @@ mod listing_operations { fn test_list_objects_with_prefix() { let (config_dir, bucket_name) = match setup_with_alias("list") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create multiple files with different prefixes @@ -966,10 +936,7 @@ mod listing_operations { fn test_recursive_listing() { let (config_dir, bucket_name) = match setup_with_alias("recursive") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create nested structure @@ -1024,10 +991,7 @@ mod admin_operations { fn test_admin_info_cluster() { let config_dir = match setup_alias_only() { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let output = run_rc( @@ -1053,10 +1017,7 @@ mod admin_operations { fn test_admin_info_server() { let config_dir = match setup_alias_only() { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let output = run_rc( @@ -1078,10 +1039,7 @@ mod admin_operations { fn test_admin_info_disk() { let config_dir = match setup_alias_only() { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let output = run_rc( @@ -1103,10 +1061,7 @@ mod admin_operations { fn test_admin_heal_status() { let config_dir = match setup_alias_only() { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let output = run_rc( @@ -1139,10 +1094,7 @@ mod error_handling { fn test_not_found_error() { let (config_dir, bucket_name) = match setup_with_alias("notfound") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Try to stat non-existent object @@ -1175,10 +1127,7 @@ mod error_handling { fn test_bucket_not_found() { let (endpoint, access_key, secret_key) = match get_test_config() { Some(c) => c, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let config_dir = tempfile::tempdir().expect("Failed to create temp dir"); @@ -1218,10 +1167,7 @@ mod presigned_urls { fn test_generate_presigned_url() { let (config_dir, bucket_name) = match setup_with_alias("presign") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Upload a test file first @@ -1277,10 +1223,7 @@ mod multipart_operations { // Test file that is exactly on part size boundary (10 MiB) let (config_dir, bucket_name) = match setup_with_alias("mpboundary") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create file exactly 10 MiB (2 parts at 5 MiB minimum) @@ -1339,10 +1282,7 @@ mod multipart_operations { // Test file with small last part (10 MiB + 1 byte) let (config_dir, bucket_name) = match setup_with_alias("mplastpart") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create file 10 MiB + 1 byte @@ -1406,10 +1346,7 @@ mod recursive_operations { fn test_recursive_delete() { let (config_dir, bucket_name) = match setup_with_alias("recdel") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create nested structure @@ -1477,10 +1414,7 @@ mod recursive_operations { fn test_recursive_copy() { let (config_dir, bucket_name) = match setup_with_alias("reccopy") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create source structure @@ -1554,10 +1488,7 @@ mod recursive_operations { fn test_recursive_download_prefix_keeps_paths_under_destination() { let (config_dir, bucket_name) = match setup_with_alias("cpdownloadprefix") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let source_key = "src/dir/a.txt"; @@ -1611,10 +1542,7 @@ mod concurrent_operations { fn test_concurrent_uploads() { let (config_dir, bucket_name) = match setup_with_alias("concurrent") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create multiple test files @@ -1678,10 +1606,7 @@ mod edge_cases { fn test_special_characters_in_key() { let (config_dir, bucket_name) = match setup_with_alias("special") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create test file with special characters in name @@ -1729,10 +1654,7 @@ mod edge_cases { fn test_empty_file_upload() { let (config_dir, bucket_name) = match setup_with_alias("empty") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create empty file @@ -1778,10 +1700,7 @@ mod edge_cases { fn test_deep_nested_path() { let (config_dir, bucket_name) = match setup_with_alias("deep") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create test file @@ -1835,10 +1754,7 @@ mod content_operations { fn test_cat_object() { let (config_dir, bucket_name) = match setup_with_alias("cat") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create test file with known content @@ -1882,10 +1798,7 @@ mod content_operations { fn test_head_object() { let (config_dir, bucket_name) = match setup_with_alias("head") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create test file with multiple lines @@ -1948,10 +1861,7 @@ mod content_operations { fn test_pipe_to_object() { let (config_dir, bucket_name) = match setup_with_alias("pipe") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let test_content = "Piped content from stdin"; @@ -2006,10 +1916,7 @@ mod find_operations { fn test_find_by_name() { let (config_dir, bucket_name) = match setup_with_alias("find") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create test files @@ -2082,10 +1989,7 @@ mod find_operations { fn test_find_by_size() { let (config_dir, bucket_name) = match setup_with_alias("findsize") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create files of different sizes @@ -2146,10 +2050,7 @@ mod diff_operations { fn test_diff_buckets() { let (config_dir, bucket_name) = match setup_with_alias("diff") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create second bucket for comparison @@ -2239,10 +2140,7 @@ mod mirror_operations { fn test_mirror_between_buckets() { let (config_dir, bucket_name) = match setup_with_alias("mirror") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create a second bucket for mirroring destination @@ -2320,10 +2218,7 @@ mod mirror_operations { fn test_mirror_preserves_content_type() { let (config_dir, bucket_name) = match setup_with_alias("mirrorcontenttype") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let target_bucket = format!("{}-dest", bucket_name); @@ -2401,10 +2296,7 @@ mod tree_operations { fn test_tree_display() { let (config_dir, bucket_name) = match setup_with_alias("tree") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create nested structure @@ -2468,10 +2360,7 @@ mod version_operations { fn test_bucket_versioning() { let (config_dir, bucket_name) = match setup_with_alias("version") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Get versioning status @@ -2540,10 +2429,7 @@ mod version_operations { fn test_rm_purge_permanently_deletes_versioned_object() { let (config_dir, bucket_name) = match setup_with_alias("rmpurge") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let enable_output = run_rc( @@ -2798,10 +2684,7 @@ mod version_operations { fn test_ls_versions_lists_multiple_object_versions() { let (config_dir, bucket_name) = match setup_with_alias("lsversions") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let enable_output = run_rc( @@ -2914,10 +2797,7 @@ mod version_operations { fn test_ls_versions_json_summary_reports_totals() { let (config_dir, bucket_name) = match setup_with_alias("lsversionssummary") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let enable_output = run_rc( @@ -3023,10 +2903,7 @@ mod version_operations { fn test_rm_recursive_purge_permanently_deletes_versioned_prefix() { let (config_dir, bucket_name) = match setup_with_alias("rmpurgeprefix") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let enable_output = run_rc( @@ -3126,10 +3003,7 @@ mod tag_operations { fn test_object_tags() { let (config_dir, bucket_name) = match setup_with_alias("tag") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; // Create and upload test file @@ -3223,10 +3097,7 @@ mod quota_operations { fn test_bucket_quota_set_info_clear() { let (config_dir, bucket_name) = match setup_with_alias("quota") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let bucket_path = format!("test/{}", bucket_name); @@ -3282,10 +3153,7 @@ mod alias_operations { fn test_alias_lifecycle() { let (endpoint, access_key, secret_key) = match get_test_config() { Some(c) => c, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let config_dir = tempfile::tempdir().expect("Failed to create temp dir"); @@ -3367,10 +3235,7 @@ mod encryption_operations { fn test_bucket_encryption_set_info_clear() { let (config_dir, bucket_name) = match setup_with_alias("encryption") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let output = run_rc( @@ -3468,10 +3333,7 @@ mod encryption_operations { fn test_cp_enc_s3_upload() { let (config_dir, bucket_name) = match setup_with_alias("cpencs3") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file"); @@ -3510,10 +3372,7 @@ mod encryption_operations { fn test_mv_enc_s3_remote_copy() { let (config_dir, bucket_name) = match setup_with_alias("mvencs3") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object( @@ -3562,10 +3421,7 @@ mod encryption_operations { fn test_pipe_enc_s3_upload() { let (config_dir, bucket_name) = match setup_with_alias("pipeencs3") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let target = format!("test/{}/piped-sse-s3.txt", bucket_name); @@ -3619,10 +3475,7 @@ mod option_behavior_operations { fn test_cp_dry_run_does_not_create_target_object() { let (config_dir, bucket_name) = match setup_with_alias("cpdryrun") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object( @@ -3678,10 +3531,7 @@ mod option_behavior_operations { fn test_mv_dry_run_keeps_source_and_skips_target() { let (config_dir, bucket_name) = match setup_with_alias("mvdryrun") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object( @@ -3737,10 +3587,7 @@ mod option_behavior_operations { fn test_rm_dry_run_does_not_delete_object() { let (config_dir, bucket_name) = match setup_with_alias("rmdryrun") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object( @@ -3791,10 +3638,7 @@ mod option_behavior_operations { fn test_rm_purge_dry_run_does_not_delete_versioned_object() { let (config_dir, bucket_name) = match setup_with_alias("rmpurgedryrun") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let enable_output = run_rc( @@ -3882,10 +3726,7 @@ mod option_behavior_operations { fn test_object_remove_purge_dry_run_does_not_delete_versioned_object() { let (config_dir, bucket_name) = match setup_with_alias("objectremovepurgedryrun") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let enable_output = run_rc( @@ -3974,10 +3815,7 @@ mod option_behavior_operations { fn test_head_bytes_returns_prefix_bytes() { let (config_dir, bucket_name) = match setup_with_alias("headbytes") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object( @@ -4013,10 +3851,7 @@ mod option_behavior_operations { fn test_find_count_json_reports_match_count() { let (config_dir, bucket_name) = match setup_with_alias("findcount") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object(config_dir.path(), &bucket_name, "a.txt", "aaaaa"); @@ -4052,10 +3887,7 @@ mod option_behavior_operations { fn test_share_upload_generates_upload_url_with_expiration() { let (config_dir, bucket_name) = match setup_with_alias("shareupload") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let object_path = format!("test/{}/upload-target.bin", bucket_name); @@ -4120,10 +3952,7 @@ mod option_behavior_operations { fn test_find_print_outputs_full_remote_path() { let (config_dir, bucket_name) = match setup_with_alias("findprint") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object( @@ -4165,10 +3994,7 @@ mod option_behavior_operations { fn test_find_exec_runs_command_with_full_remote_path() { let (config_dir, bucket_name) = match setup_with_alias("findexec") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object(config_dir.path(), &bucket_name, "exec-a.txt", "a"); @@ -4213,10 +4039,7 @@ mod option_behavior_operations { fn test_find_exec_rejects_json_output() { let (config_dir, bucket_name) = match setup_with_alias("findexecjson") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object(config_dir.path(), &bucket_name, "x.txt", "x"); @@ -4251,10 +4074,7 @@ mod option_behavior_operations { fn test_find_maxdepth_excludes_deeper_matches() { let (config_dir, bucket_name) = match setup_with_alias("finddepth") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object(config_dir.path(), &bucket_name, "top.txt", "top"); @@ -4297,10 +4117,7 @@ mod option_behavior_operations { fn test_mirror_remove_with_parallel_synchronizes_destination() { let (config_dir, bucket_name) = match setup_with_alias("mirroropt") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let target_bucket = format!("{}-dest", bucket_name); @@ -4388,10 +4205,7 @@ mod option_behavior_operations { fn test_mirror_parallel_zero_returns_usage_error() { let (config_dir, bucket_name) = match setup_with_alias("mirrorparallelzero") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let target_bucket = format!("{}-dest", bucket_name); @@ -4434,10 +4248,7 @@ mod option_behavior_operations { fn test_tree_option_combination_filters_expected_nodes() { let (config_dir, bucket_name) = match setup_with_alias("treeopts") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; upload_text_object(config_dir.path(), &bucket_name, "dir/a.txt", "A"); @@ -4502,10 +4313,7 @@ mod option_behavior_operations { fn test_diff_diff_only_excludes_same_entries() { let (config_dir, bucket_name) = match setup_with_alias("diffonly") { Some(v) => v, - None => { - eprintln!("Skipping: S3 test config not available"); - return; - } + None => panic!("S3 integration test setup failed"), }; let second_bucket = format!("{}-second", bucket_name); From 54f0ed9c4605e0e3c81a1b094fda68c6d7a8d6bd Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 11 Jul 2026 12:03:09 +0800 Subject: [PATCH 3/4] ci: harden test and release pipelines --- .github/workflows/ci.yml | 52 ++++--- .github/workflows/docker.yml | 49 +++++-- .github/workflows/integration.yml | 30 ++-- .github/workflows/release.yml | 179 +++++++++++++----------- Dockerfile | 3 + crates/cli/tests/dockerfile_contract.rs | 14 ++ 6 files changed, 200 insertions(+), 127 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4150883..cd90135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,13 +10,25 @@ env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 +permissions: + contents: read + jobs: + advisories: + name: Dependency Advisories + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2 + with: + command: check advisories + fmt: name: Format Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable with: components: rustfmt - name: Check formatting @@ -26,11 +38,11 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable with: components: clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Run Clippy run: cargo clippy --workspace --all-targets -- -D warnings @@ -42,9 +54,9 @@ jobs: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Run tests # Don't use --all-features as integration/golden require external services # See integration.yml for those tests @@ -58,9 +70,9 @@ jobs: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build run: cargo build --workspace --release @@ -68,9 +80,9 @@ jobs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build documentation run: cargo doc --workspace --no-deps env: @@ -82,10 +94,12 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 - name: Check protected files + env: + PR_BODY: ${{ github.event.pull_request.body }} run: | PROTECTED_FILES=( "docs/reference/rc/" @@ -103,7 +117,7 @@ jobs: echo "This change requires the Breaking Change process. See AGENTS.md." # Check if PR body contains BREAKING marker - if ! echo "${{ github.event.pull_request.body }}" | grep -q "BREAKING"; then + if ! grep -q "BREAKING" <<< "$PR_BODY"; then echo "::error::Protected file $file modified without BREAKING marker in PR description" exit 1 fi @@ -115,10 +129,10 @@ jobs: name: Minimum Supported Rust Version runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: toolchain: "1.92" - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build with MSRV run: cargo build --workspace diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index fee0fab..89a9134 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -14,35 +14,62 @@ on: permissions: contents: read -env: - image_tag: ${{ github.event.inputs.tag || github.event.workflow_run.head_branch || 'latest' }} - jobs: build-and-push: + if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push') runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} + + - name: Resolve image tags + id: image + env: + EVENT_NAME: ${{ github.event_name }} + MANUAL_TAG: ${{ github.event.inputs.tag }} + shell: bash + run: | + set -euo pipefail + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + image_tag="$MANUAL_TAG" + else + workspace_version=$(awk -F'"' '/^version = "/ { print $2; exit }' Cargo.toml) + image_tag="v${workspace_version}" + fi + + if [[ ! "$image_tag" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then + echo "Invalid Docker image tag: $image_tag" + exit 1 + fi + + { + echo 'tags<> "$GITHUB_OUTPUT" - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . platforms: linux/amd64,linux/arm64 push: true - tags: | - rustfs/rc:${{ env.image_tag }} - rustfs/rc:latest + tags: ${{ steps.image.outputs.tags }} diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 05a5988..ebbc7f5 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -16,15 +16,18 @@ env: TEST_S3_ACCESS_KEY: accesskey TEST_S3_SECRET_KEY: secretkey +permissions: + contents: read + jobs: cli-contract: name: CLI Contract (Commands and Options) runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Run CLI help contract tests run: cargo test --package rustfs-cli --test help_contract @@ -34,9 +37,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Start RustFS latest run: | @@ -103,9 +106,9 @@ jobs: timeout-minutes: 90 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Start RustFS latest run: | @@ -139,6 +142,9 @@ jobs: - name: Run full integration suite run: cargo test --package rustfs-cli --test integration --features integration -- --test-threads=1 + - name: Run S3 golden tests + run: cargo test --package rustfs-cli --test golden --features golden,integration -- --test-threads=1 + - name: Show RustFS logs on failure if: failure() run: docker logs rustfs 2>&1 | tail -200 @@ -148,9 +154,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Run golden tests run: cargo test --package rustfs-cli --test golden --features golden diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d8d942..93c5254 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,9 +3,7 @@ name: Build and Release on: push: tags: - - "*.*.*" - release: - types: [ published ] + - "v*.*.*" workflow_dispatch: inputs: tag: @@ -16,6 +14,9 @@ on: env: CARGO_TERM_COLOR: always +permissions: + contents: read + jobs: # Build strategy check - determine build type and version build-check: @@ -27,31 +28,37 @@ jobs: version: ${{ steps.check.outputs.version }} is_release: ${{ steps.check.outputs.is_release }} has_token: ${{ steps.check.outputs.has_token }} + source_ref: ${{ steps.check.outputs.source_ref }} steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 - name: Determine build strategy id: check + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: | version="" is_release=false + source_ref="${GITHUB_SHA}" - if [[ "${{ github.event_name }}" == "release" ]]; then - # Release event - get version from release tag - version="${{ github.event.release.tag_name }}" - is_release=true - echo "đŸ“Ļ Release build detected: $version" - elif [[ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then + if [[ "$GITHUB_REF" == refs/tags/* ]]; then # Tag push - get version from tag version="${GITHUB_REF#refs/tags/}" + source_ref="${GITHUB_REF}" is_release=true echo "đŸˇī¸ Tag build detected: $version" - elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && [[ -n "${{ github.event.inputs.tag }}" ]]; then + elif [[ "$EVENT_NAME" == "workflow_dispatch" ]] && [[ -n "$INPUT_TAG" ]]; then # Manual trigger with tag input - treat as release - version="${{ github.event.inputs.tag }}" + version="$INPUT_TAG" + if [[ ! "$version" =~ ^v ]]; then + version="v${version}" + fi + source_ref="refs/tags/${version}" is_release=true echo "đŸˇī¸ Manual release build detected: $version" else @@ -60,6 +67,15 @@ jobs: echo "đŸ› ī¸ Development build detected: $version" fi + if [[ "$is_release" == "true" ]]; then + git rev-parse --verify "${source_ref}^{commit}" >/dev/null + workspace_version=$(git show "${source_ref}:Cargo.toml" | awk -F'"' '/^version = "/ { print $2; exit }') + if [[ -z "$workspace_version" || "${version#v}" != "$workspace_version" ]]; then + echo "Release tag '$version' does not match workspace version '$workspace_version'" + exit 1 + fi + fi + # Ensure version starts with 'v' for release builds if [[ "$is_release" == "true" ]] && [[ ! "$version" =~ ^v ]]; then version="v${version}" @@ -67,22 +83,49 @@ jobs: fi has_token="false" - if [ -n "${{ secrets.CARGO_REGISTRY_TOKEN }}" ]; then + if [[ -n "$CARGO_REGISTRY_TOKEN" ]]; then has_token="true" fi echo "version=$version" >> $GITHUB_OUTPUT echo "is_release=$is_release" >> $GITHUB_OUTPUT echo "has_token=$has_token" >> $GITHUB_OUTPUT + echo "source_ref=$source_ref" >> $GITHUB_OUTPUT echo "📊 Build Summary:" echo " - Version: $version" echo " - Is release: $is_release" echo " - Publish crate: $has_token" + verify: + name: Release Verification + needs: build-check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ needs.build-check.outputs.source_ref }} + - name: Setup Rust + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + components: rustfmt, clippy + - name: Setup Rust cache + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + - name: Check formatting + run: cargo fmt --all --check + - name: Run Clippy + run: cargo clippy --workspace --all-targets --locked -- -D warnings + - name: Run tests + run: cargo test --workspace --locked + - name: Check dependency advisories + uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2 + with: + command: check advisories + build: name: Build (${{ matrix.os_name }}-${{ matrix.arch }}) - needs: build-check + needs: [build-check, verify] runs-on: ${{ matrix.os }} permissions: contents: read @@ -141,15 +184,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ needs.build-check.outputs.source_ref }} - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable with: targets: ${{ matrix.target }} - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: key: ${{ matrix.target }} @@ -163,16 +208,16 @@ jobs: # Install cross for cross-compilation - name: Install cross if: matrix.cross - run: cargo install cross --git https://github.com/cross-rs/cross + run: cargo install cross --version 0.2.5 --locked # Build - name: Build (native) if: "!matrix.cross" - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release --locked --target ${{ matrix.target }} - name: Build (cross) if: matrix.cross - run: cross build --release --target ${{ matrix.target }} + run: cross build --release --locked --target ${{ matrix.target }} # Package and prepare artifacts - name: Prepare and package artifacts (Unix) @@ -186,8 +231,9 @@ jobs: ARTIFACT_PREFIX="rustfs-cli-${{ matrix.os_name }}-${{ matrix.arch }}" # Create versioned archive + cp LICENSE-MIT LICENSE-APACHE target/${{ matrix.target }}/release/ cd target/${{ matrix.target }}/release - tar -czvf "../../../artifacts/${ARTIFACT_PREFIX}-${VERSION}.tar.gz" ${{ matrix.artifact_name }} + tar -czvf "../../../artifacts/${ARTIFACT_PREFIX}-${VERSION}.tar.gz" ${{ matrix.artifact_name }} LICENSE-MIT LICENSE-APACHE cd ../../.. # Create latest archive (copy of versioned) @@ -216,7 +262,7 @@ jobs: # Create versioned archive $sourcePath = "target/${{ matrix.target }}/release/${{ matrix.artifact_name }}" $versionedZip = "artifacts/${ARTIFACT_PREFIX}-${env:VERSION}.zip" - Compress-Archive -Path $sourcePath -DestinationPath $versionedZip + Compress-Archive -Path $sourcePath, "LICENSE-MIT", "LICENSE-APACHE" -DestinationPath $versionedZip # Create latest archive (copy of versioned) $latestZip = "artifacts/${ARTIFACT_PREFIX}-latest.zip" @@ -234,7 +280,7 @@ jobs: # Upload artifacts for later jobs - name: Upload artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: rustfs-cli-${{ matrix.os_name }}-${{ matrix.arch }} path: artifacts/* @@ -255,21 +301,7 @@ jobs: exit 0 fi - # Ensure aws CLI is available; k8s/docker build images may not ship it. - if ! command -v aws >/dev/null 2>&1; then - echo "â„šī¸ aws CLI not found, installing AWS CLI v2 for Linux..." - ARCH="$(uname -m)" - case "$ARCH" in - x86_64) AWS_CLI_ARCH="x86_64" ;; - aarch64|arm64) AWS_CLI_ARCH="aarch64" ;; - *) echo "❌ Unsupported architecture for aws CLI install: $ARCH"; exit 1 ;; - esac - curl -sL "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_CLI_ARCH}.zip" -o awscliv2.zip - unzip -q awscliv2.zip - ./aws/install --update -i "${RUNNER_TEMP:-/tmp}/aws-cli" -b "${RUNNER_TEMP:-/tmp}/aws-bin" - export PATH="${RUNNER_TEMP:-/tmp}/aws-bin:$PATH" - rm -rf awscliv2.zip aws - fi + command -v aws >/dev/null 2>&1 || { echo "AWS CLI is required on the release runner"; exit 1; } export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY" @@ -296,20 +328,22 @@ jobs: # Generate shell completions completions: name: Generate Shell Completions - needs: build-check + needs: [build-check, verify] runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ needs.build-check.outputs.source_ref }} - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build - run: cargo build --release + run: cargo build --release --locked - name: Generate completions run: | @@ -319,14 +353,13 @@ jobs: ./target/release/rc completions fish > completions/rc.fish ./target/release/rc completions powershell > completions/rc.ps1 tar -czvf completions.tar.gz completions/ - continue-on-error: true - name: Upload completions artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: completions path: completions.tar.gz - if-no-files-found: warn + if-no-files-found: error # Upload release assets to GitHub Release upload-release-assets: @@ -338,18 +371,17 @@ jobs: contents: write steps: - name: Download all artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: path: artifacts pattern: rustfs-cli-* merge-multiple: true - name: Download completions - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: completions path: artifacts - continue-on-error: true - name: Display structure of downloaded files run: | @@ -400,21 +432,7 @@ jobs: exit 0 fi - # Ensure aws CLI is available; k8s/docker build images may not ship it. - if ! command -v aws >/dev/null 2>&1; then - echo "â„šī¸ aws CLI not found, installing AWS CLI v2 for Linux..." - ARCH="$(uname -m)" - case "$ARCH" in - x86_64) AWS_CLI_ARCH="x86_64" ;; - aarch64|arm64) AWS_CLI_ARCH="aarch64" ;; - *) echo "❌ Unsupported architecture for aws CLI install: $ARCH"; exit 1 ;; - esac - curl -sL "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_CLI_ARCH}.zip" -o awscliv2.zip - unzip -q awscliv2.zip - ./aws/install --update -i "${RUNNER_TEMP:-/tmp}/aws-cli" -b "${RUNNER_TEMP:-/tmp}/aws-bin" - export PATH="${RUNNER_TEMP:-/tmp}/aws-bin:$PATH" - rm -rf awscliv2.zip aws - fi + command -v aws >/dev/null 2>&1 || { echo "AWS CLI is required on the release runner"; exit 1; } export AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY" @@ -437,18 +455,8 @@ jobs: echo "✅ Release asset R2 upload completed successfully" - - name: Upload to GitHub Release (release event) - if: github.event_name == 'release' - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.event.release.tag_name }} - files: release-assets/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload to GitHub Release (tag push or manual) - if: github.event_name != 'release' - uses: softprops/action-gh-release@v2 + - name: Upload to GitHub Release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: ${{ needs.build-check.outputs.version }} name: Release ${{ needs.build-check.outputs.version }} @@ -462,30 +470,31 @@ jobs: # Publish to crates.io publish-crates: name: Publish to crates.io - needs: [ build-check, build ] + needs: [build-check, verify, build] runs-on: ubuntu-latest if: needs.build-check.outputs.is_release == 'true' && needs.build-check.outputs.has_token == 'true' + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ needs.build-check.outputs.source_ref }} - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - name: Publish rc-core - run: cargo publish --package rc-core --token ${{ secrets.CARGO_REGISTRY_TOKEN }} - continue-on-error: true + run: cargo publish --locked --package rc-core - name: Wait for crates.io run: sleep 30 - name: Publish rc-s3 - run: cargo publish --package rc-s3 --token ${{ secrets.CARGO_REGISTRY_TOKEN }} - continue-on-error: true + run: cargo publish --locked --package rc-s3 - name: Wait for crates.io run: sleep 30 - name: Publish rustfs-cli - run: cargo publish --package rustfs-cli --token ${{ secrets.CARGO_REGISTRY_TOKEN }} - continue-on-error: true + run: cargo publish --locked --package rustfs-cli diff --git a/Dockerfile b/Dockerfile index 06e4b91..1b8d0e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,11 @@ FROM alpine:3.23 # - ca-certificates for HTTPS # - jq/yq-go for Kubernetes-oriented config processing workflows RUN apk add --no-cache ca-certificates jq yq-go +RUN addgroup -S rc && adduser -S -G rc -h /home/rc rc COPY --from=builder /app/target/release/rc /usr/bin/rc COPY --from=builder /app/LICENSE-* /licenses/ +USER rc + ENTRYPOINT ["rc"] diff --git a/crates/cli/tests/dockerfile_contract.rs b/crates/cli/tests/dockerfile_contract.rs index 220184b..2fa98d2 100644 --- a/crates/cli/tests/dockerfile_contract.rs +++ b/crates/cli/tests/dockerfile_contract.rs @@ -28,3 +28,17 @@ fn runtime_image_contains_jq_and_yq() { "runtime image must install jq and yq-go" ); } + +#[test] +fn runtime_image_uses_non_root_user() { + let dockerfile = include_str!("../../../Dockerfile"); + let runtime_section = dockerfile + .split("FROM alpine:3.23") + .nth(1) + .expect("Dockerfile must contain runtime stage based on alpine:3.23"); + + assert!( + runtime_section.lines().any(|line| line.trim() == "USER rc"), + "runtime image must use the dedicated rc user" + ); +} From 2eddea5be20fdeacaaa0e97e64310b7360077f47 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 11 Jul 2026 12:26:27 +0800 Subject: [PATCH 4/4] fix(cli): address cross-platform CI failures --- crates/cli/src/commands/bucket.rs | 47 +++++++++++++++++++++++-------- crates/cli/src/commands/ls.rs | 4 +-- crates/cli/tests/integration.rs | 2 +- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/crates/cli/src/commands/bucket.rs b/crates/cli/src/commands/bucket.rs index fdaeb59..b468a5b 100644 --- a/crates/cli/src/commands/bucket.rs +++ b/crates/cli/src/commands/bucket.rs @@ -74,27 +74,52 @@ pub enum BucketCommands { /// Execute the bucket command group pub async fn execute(args: BucketArgs, output_config: OutputConfig) -> ExitCode { + // Boxing each branch keeps this dispatcher future below Windows' 1 MiB main-thread stack. match args.command { - BucketCommands::List(args) => ls::execute(args, output_config).await, - BucketCommands::Create(args) => mb::execute(args, output_config).await, - BucketCommands::Remove(args) => rb::execute(args, output_config).await, + BucketCommands::List(args) => Box::pin(ls::execute(args, output_config)).await, + BucketCommands::Create(args) => Box::pin(mb::execute(args, output_config)).await, + BucketCommands::Remove(args) => Box::pin(rb::execute(args, output_config)).await, BucketCommands::Event(cmd) => { - event::execute(event::EventArgs { command: cmd }, output_config).await + Box::pin(event::execute( + event::EventArgs { command: cmd }, + output_config, + )) + .await } BucketCommands::Cors(cmd) => { - cors::execute(cors::CorsArgs { command: cmd }, output_config).await + Box::pin(cors::execute( + cors::CorsArgs { command: cmd }, + output_config, + )) + .await + } + BucketCommands::Encryption(args) => { + Box::pin(encryption::execute(args, output_config)).await } - BucketCommands::Encryption(args) => encryption::execute(args, output_config).await, BucketCommands::Version(cmd) => { - version::execute(version::VersionArgs { command: cmd }, output_config).await + Box::pin(version::execute( + version::VersionArgs { command: cmd }, + output_config, + )) + .await } BucketCommands::Quota(cmd) => { - quota::execute(quota::QuotaArgs { command: cmd }, output_config).await + Box::pin(quota::execute( + quota::QuotaArgs { command: cmd }, + output_config, + )) + .await } BucketCommands::Anonymous(cmd) => { - anonymous::execute(anonymous::AnonymousArgs { command: cmd }, output_config).await + Box::pin(anonymous::execute( + anonymous::AnonymousArgs { command: cmd }, + output_config, + )) + .await + } + BucketCommands::Lifecycle(args) => Box::pin(ilm::execute(args, output_config)).await, + BucketCommands::Replication(args) => { + Box::pin(replicate::execute(args, output_config)).await } - BucketCommands::Lifecycle(args) => ilm::execute(args, output_config).await, - BucketCommands::Replication(args) => replicate::execute(args, output_config).await, } } diff --git a/crates/cli/src/commands/ls.rs b/crates/cli/src/commands/ls.rs index 392a822..aaff947 100644 --- a/crates/cli/src/commands/ls.rs +++ b/crates/cli/src/commands/ls.rs @@ -521,13 +521,13 @@ async fn list_all_objects( if item.is_dir { let styled_size = formatter.style_size(&format!("{:>10}", "0B")); let styled_name = - formatter.style_dir(&format!("{}/{}", bucket_name, &item.key)); + formatter.style_dir(&format!("{}/{}", bucket_name, item.key)); formatter.println(&format!("{styled_date} {styled_size} {styled_name}")); } else { let size = item.size_human.clone().unwrap_or_else(|| "0 B".to_string()); let styled_size = formatter.style_size(&format!("{:>10}", size)); let styled_name = - formatter.style_file(&format!("{}/{}", bucket_name, &item.key)); + formatter.style_file(&format!("{}/{}", bucket_name, item.key)); formatter.println(&format!("{styled_date} {styled_size} {styled_name}")); } } diff --git a/crates/cli/tests/integration.rs b/crates/cli/tests/integration.rs index 01dee03..0b542e3 100644 --- a/crates/cli/tests/integration.rs +++ b/crates/cli/tests/integration.rs @@ -4235,7 +4235,7 @@ mod option_behavior_operations { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("--parallel must be greater than 0"), + stderr.contains("--parallel must be between 1 and 256"), "Unexpected error output: {}", stderr );