diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 0000000..01d324d --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,4 @@ +# SQLx locks optional MySQL dependencies even though this project builds only +# the PostgreSQL driver. The vulnerable RSA code is absent from `cargo tree`. +[advisories] +ignore = ["RUSTSEC-2023-0071"] diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 02fc553..17f4512 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -30,6 +30,12 @@ jobs: - name: Run Rust tests run: cargo test --all-targets + - name: Install cargo-audit + run: cargo install cargo-audit --locked --version 0.22.2 + + - name: Audit Rust dependencies + run: cargo audit + - name: Install Node.js uses: actions/setup-node@v4 with: diff --git a/Cargo.lock b/Cargo.lock index eac0c62..d391d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -26,7 +26,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -50,7 +50,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -156,18 +155,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object", -] +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -192,7 +182,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -203,7 +193,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -234,7 +224,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -280,7 +270,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom 8.0.0", + "nom", "num-rational", "v_frame", ] @@ -296,9 +286,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.12" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26bbf46abc608f2dc61fd6cb3b7b0665497cc259a21520151ed98f8b37d2c79" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -331,15 +321,15 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.0" +version = "1.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f92058d22a46adf53ec57a6a96f34447daf02bff52e8fb956c66bcd5c6ac12" +checksum = "6c9b9de216a988dd54b754a82a7660cfe14cee4f6782ae4524470972fa0ccb39" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", - "aws-smithy-http 0.63.4", + "aws-smithy-http", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -359,18 +349,20 @@ 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", "aws-smithy-async", "aws-smithy-checksums", "aws-smithy-eventstream", - "aws-smithy-http 0.62.6", + "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -379,46 +371,46 @@ dependencies = [ "bytes", "fastrand", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", "http 1.4.0", - "http-body 0.4.6", + "http-body 1.0.1", "lru", "percent-encoding", "regex-lite", - "sha2", + "sha2 0.11.0", "tracing", "url", ] [[package]] name = "aws-sigv4" -version = "1.4.0" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f6ae9b71597dc5fd115d52849d7a5556ad9265885ad3492ea8d73b93bbc46e" +checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", - "aws-smithy-http 0.63.4", + "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", "form_urlencoded", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", "http 1.4.0", "percent-encoding", - "sha2", + "sha2 0.11.0", "time", "tracing", ] [[package]] name = "aws-smithy-async" -version = "1.2.12" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -427,29 +419,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 0.62.6", + "aws-smithy-http", "aws-smithy-types", "bytes", "crc-fast", "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5 0.11.0", "pin-project-lite", - "sha1", - "sha2", + "sha1 0.11.0", + "sha2 0.11.0", "tracing", ] [[package]] name = "aws-smithy-eventstream" -version = "0.60.19" +version = "0.60.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79" +checksum = "78d8391e65fcea47c586a22e1a41f173b38615b112b2c6b7a44e80cec3e6b706" dependencies = [ "aws-smithy-types", "bytes", @@ -458,32 +451,11 @@ 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", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http" -version = "0.63.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e" -dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "bytes", @@ -501,63 +473,60 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0709f0083aa19b704132684bc26d3c868e06bd428ccc4373b0b55c3e8748a58b" +checksum = "5c3ef8931ad1c98aa6a55b4256f847f3116090819844e0dd41ea682cac5dd2d3" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.15", - "http 0.2.12", + "h2", "http 1.4.0", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.8.1", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.35", + "rustls", "rustls-native-certs", "rustls-pki-types", "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.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-runtime" -version = "1.10.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd3dfc18c1ce097cf81fced7192731e63809829c6cbf933c1ec47452d08e1aa" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", - "aws-smithy-http 0.63.4", + "aws-smithy-http", "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", @@ -574,11 +543,12 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.4" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec" +checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", @@ -590,10 +560,32 @@ dependencies = [ ] [[package]] -name = "aws-smithy-types" -version = "1.4.4" +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd" +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.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85" dependencies = [ "base64-simd", "bytes", @@ -626,13 +618,14 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.12" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c50f3cdf47caa8d01f2be4a6663ea02418e892f9bbfd82c7b9a3a37eaccdd3a" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -651,7 +644,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "itoa", "matchit", @@ -713,12 +706,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -783,7 +770,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -795,6 +782,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 = "brotli" version = "8.0.2" @@ -854,9 +850,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[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" @@ -931,6 +927,17 @@ 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.42" @@ -945,23 +952,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", - "stacker", -] - [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] @@ -1002,6 +999,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "color_quant" version = "1.1.0" @@ -1048,12 +1051,27 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" 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 = "constant_time_eq" version = "0.3.1" @@ -1096,6 +1114,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -1113,15 +1140,12 @@ 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 0.9.2", - "regex", - "rustversion", + "digest 0.10.7", + "spin 0.10.1", ] [[package]] @@ -1154,9 +1178,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -1193,6 +1217,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1202,6 +1235,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "dav1d" version = "0.10.4" @@ -1236,19 +1278,16 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" 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 = "derive_arbitrary" @@ -1258,7 +1297,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1267,12 +1306,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "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 = "displaydoc" version = "0.2.5" @@ -1281,7 +1332,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1311,7 +1362,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" dependencies = [ - "base64 0.22.1", + "base64", "memchr", ] @@ -1368,7 +1419,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1400,9 +1451,14 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.3" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "fallible_collections" @@ -1436,7 +1492,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1484,7 +1540,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin", + "spin 0.9.9", ] [[package]] @@ -1499,6 +1555,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1587,7 +1649,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1652,11 +1714,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1683,25 +1759,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[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.15" @@ -1741,16 +1798,6 @@ dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -1759,7 +1806,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1767,23 +1814,19 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.5", + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] -name = "heck" -version = "0.4.1" +name = "hashlink" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "unicode-segmentation", + "hashbrown 0.15.5", ] [[package]] @@ -1804,7 +1847,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1813,7 +1856,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]] @@ -1899,27 +1951,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[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.4.10", - "tokio", - "tower-service", - "tracing", - "want", + "typenum", ] [[package]] @@ -1932,7 +1969,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.15", + "h2", "http 1.4.0", "http-body 1.0.1", "httparse", @@ -1945,21 +1982,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -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" @@ -1967,13 +1989,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper", "hyper-util", - "rustls 0.23.35", + "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower-service", "webpki-roots 1.0.4", ] @@ -1984,14 +2006,14 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.8.1", + "hyper", "ipnet", "libc", "percent-encoding", @@ -2168,12 +2190,12 @@ dependencies = [ "aws-sdk-s3", "axum", "axum-extra", - "base64 0.22.1", + "base64", "bytes", "chrono", "dotenvy", "hex", - "hmac", + "hmac 0.12.1", "image", "img-parts", "jpeg-encoder", @@ -2181,14 +2203,14 @@ dependencies = [ "lettre", "oxipng", "percent-encoding", - "rand 0.8.5", + "rand 0.8.7", "ravif", "redis", "reqwest", "rgb", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx", "time", "tokio", @@ -2247,7 +2269,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2283,9 +2305,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -2319,7 +2341,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.22.1", + "base64", "js-sys", "pem", "ring", @@ -2334,18 +2356,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.9", ] [[package]] name = "lettre" -version = "0.11.19" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ "async-trait", - "base64 0.22.1", - "chumsky", + "base64", "email-encoding", "email_address", "fastrand", @@ -2354,13 +2375,13 @@ dependencies = [ "httpdate", "idna", "mime", - "nom 8.0.0", + "nom", "percent-encoding", "quoted_printable", - "rustls 0.23.35", + "rustls", "socket2 0.6.1", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "url", "webpki-roots 1.0.4", ] @@ -2418,11 +2439,10 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -2475,11 +2495,11 @@ dependencies = [ [[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]] @@ -2541,7 +2561,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", ] [[package]] @@ -2566,12 +2596,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2630,7 +2654,7 @@ dependencies = [ "httparse", "memchr", "mime", - "spin", + "spin 0.9.9", "version_check", ] @@ -2649,16 +2673,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nom" version = "8.0.0" @@ -2704,16 +2718,16 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.7", "smallvec", "zeroize", ] [[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-derive" @@ -2723,7 +2737,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2767,15 +2781,6 @@ dependencies = [ "libm", ] -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -2827,6 +2832,12 @@ dependencies = [ "zopfli", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2873,8 +2884,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] @@ -2883,7 +2894,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] @@ -2919,7 +2930,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2981,7 +2992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -3012,9 +3023,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3035,17 +3046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.111", -] - -[[package]] -name = "psm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" -dependencies = [ - "ar_archive_writer", - "cc", + "syn", ] [[package]] @@ -3075,7 +3076,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.35", + "rustls", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -3085,17 +3086,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 0.9.2", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", - "rustls 0.23.35", + "rustls", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -3120,9 +3122,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3139,6 +3141,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -3147,23 +3155,24 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -3176,16 +3185,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -3197,11 +3196,17 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "getrandom 0.3.4", + "rand_core 0.10.1", ] [[package]] @@ -3232,8 +3237,8 @@ dependencies = [ "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.8.7", + "rand_chacha", "simd_helpers", "system-deps 6.2.2", "thiserror 1.0.69", @@ -3318,18 +3323,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.13" @@ -3359,28 +3352,28 @@ version = "0.12.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-core", "http 1.4.0", "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.35", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -3420,8 +3413,8 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -3462,18 +3455,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[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.35" @@ -3485,7 +3466,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki", "subtle", "zeroize", ] @@ -3502,15 +3483,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pki-types" version = "1.13.2" @@ -3523,19 +3495,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", @@ -3551,9 +3513,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" @@ -3570,16 +3532,6 @@ 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 = "security-framework" version = "3.6.0" @@ -3636,7 +3588,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -3700,8 +3652,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]] @@ -3717,8 +3680,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]] @@ -3751,7 +3725,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -3793,6 +3767,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -3816,13 +3793,19 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + [[package]] name = "spki" version = "0.7.3" @@ -3833,21 +3816,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom 7.1.3", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3858,101 +3831,94 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "ahash", - "atoi", - "byteorder", + "base64", "bytes", "chrono", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown 0.15.5", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", - "rustls 0.21.12", - "rustls-pemfile", + "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", "url", "uuid", - "webpki-roots 0.25.4", + "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck", "hex", "once_cell", "proc-macro2", "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", - "tempfile", + "syn", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.21.7", + "base64", "bitflags", "byteorder", "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -3962,22 +3928,22 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.7", "rsa", "serde", - "sha1", - "sha2", + "sha1 0.10.6", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -3985,12 +3951,12 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.21.7", + "base64", "bitflags", "byteorder", "chrono", @@ -3999,25 +3965,24 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.7", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -4025,9 +3990,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -4041,10 +4006,11 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", + "thiserror 2.0.17", "tracing", "url", - "urlencoding", "uuid", ] @@ -4054,19 +4020,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -4098,20 +4051,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -4135,7 +4077,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -4145,7 +4087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr 0.15.8", - "heck 0.5.0", + "heck", "pkg-config", "toml 0.8.23", "version-compare", @@ -4158,7 +4100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" dependencies = [ "cfg-expr 0.20.5", - "heck 0.5.0", + "heck", "pkg-config", "toml 0.9.10+spec-1.1.0", "version-compare", @@ -4182,19 +4124,6 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "terminal_size" version = "0.4.3" @@ -4231,7 +4160,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -4242,7 +4171,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -4270,30 +4199,29 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -4326,9 +4254,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -4349,7 +4277,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -4359,17 +4287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" dependencies = [ "pin-project", - "rand 0.8.5", - "tokio", -] - -[[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", + "rand 0.8.7", "tokio", ] @@ -4379,7 +4297,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls", "tokio", ] @@ -4396,13 +4314,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -4557,7 +4476,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -4607,9 +4526,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" @@ -4644,25 +4563,13 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -4684,12 +4591,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4830,7 +4731,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn", "wasm-bindgen-shared", ] @@ -4874,9 +4775,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] [[package]] name = "webpki-roots" @@ -4946,7 +4850,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -4957,7 +4861,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -5002,15 +4906,6 @@ 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" @@ -5279,7 +5174,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", "synstructure", ] @@ -5300,7 +5195,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -5320,7 +5215,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", "synstructure", ] @@ -5341,7 +5236,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -5374,7 +5269,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -5393,12 +5288,12 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac", + "hmac 0.12.1", "indexmap", "lzma-rs", "memchr", "pbkdf2", - "sha1", + "sha1 0.10.6", "thiserror 2.0.17", "time", "xz2", diff --git a/Cargo.toml b/Cargo.toml index e751680..fa76e20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,11 @@ axum = { version = "0.8", features = ["multipart"] } tokio = { version = "1", features = ["full"] } tower-http = { version = "0.6", features = ["cors", "trace", "compression-full", "fs"] } axum-extra = { version = "0.12", features = ["cookie"] } -time = "0.3" +time = "0.3.47" serde = { version = "1", features = ["derive"] } serde_json = "1" -bytes = "1" +bytes = "1.11.1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } @@ -25,7 +25,7 @@ chrono = { version = "0.4", features = ["serde"] } dotenvy = "0.15" -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } +sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json", "macros", "migrate"] } redis = { version = "0.24.1", features = ["tokio-comp", "connection-manager", "streams"] } # Auth / security @@ -35,12 +35,12 @@ hex = "0.4" hmac = "0.12" jsonwebtoken = "9" percent-encoding = "2" -rand = "0.8" +rand = "0.8.7" sha2 = "0.10" aes-gcm = "0.10" # S3-compatible object storage (Garage, MinIO, AWS S3, etc.) -aws-sdk-s3 = { version = "1", default-features = false, features = ["rustls", "default-https-client", "rt-tokio"] } +aws-sdk-s3 = { version = "1.137.0", default-features = false, features = ["default-https-client", "rt-tokio"] } # Images # Keep image-rs limited to formats exposed by the API. AVIF decoding uses @@ -58,7 +58,7 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls" url = "2" # Mail -lettre = { version = "0.11", default-features = false, features = ["tokio1", "tokio1-rustls-tls", "builder", "smtp-transport"] } +lettre = { version = "0.11.22", default-features = false, features = ["tokio1", "tokio1-rustls-tls", "builder", "smtp-transport"] } # ZIP download (batch) tokio-util = { version = "0.7", features = ["io"] } diff --git a/docs/architecture.md b/docs/architecture.md index 393e062..91711a2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -248,7 +248,7 @@ webp = "0.3" # WebP 编解码 ravif = "0.11" # AVIF 编码 # 数据库 -sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid", "chrono"] } +sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json", "macros", "migrate"] } # Redis redis = { version = "0.24", features = ["tokio-comp"] } diff --git a/migrations/010_anonymous_quota_settlement.sql b/migrations/010_anonymous_quota_settlement.sql new file mode 100644 index 0000000..51771a1 --- /dev/null +++ b/migrations/010_anonymous_quota_settlement.sql @@ -0,0 +1,11 @@ +ALTER TABLE tasks + ADD COLUMN IF NOT EXISTS anonymous_quota_date DATE; + +UPDATE tasks +SET anonymous_quota_date = (created_at AT TIME ZONE 'Asia/Shanghai')::date +WHERE anonymous_units_reserved > 0 + AND anonymous_quota_date IS NULL; + +CREATE INDEX IF NOT EXISTS idx_tasks_anonymous_reservation_pending + ON tasks(id) + WHERE anonymous_units_reserved > 0; diff --git a/src/api/admin.rs b/src/api/admin.rs index 1ed5714..4b6a2c5 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -3,6 +3,7 @@ use crate::api::envelope::Envelope; use crate::error::{AppError, ErrorCode}; use crate::services::billing; use crate::services::mail; +use crate::services::quota; use crate::services::settings; use crate::services::settings::{ AuthConfigStored, MailConfigStored, MailCustomSmtp, StripeConfigStored, @@ -543,6 +544,7 @@ async fn cancel_task( }; if matches!(status.as_str(), "completed" | "failed" | "cancelled") { + quota::settle_anonymous_task_reservation(&state, task_id).await?; return Ok(Json(Envelope { success: true, data: MessageResponse { @@ -595,6 +597,8 @@ async fn cancel_task( .await .map_err(|err| AppError::new(ErrorCode::Internal, "提交事务失败").with_source(err))?; + quota::settle_anonymous_task_reservation(&state, task_id).await?; + Ok(Json(Envelope { success: true, data: MessageResponse { diff --git a/src/api/mod.rs b/src/api/mod.rs index 756ae02..9e128fb 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -22,6 +22,7 @@ use crate::state::AppState; use axum::extract::DefaultBodyLimit; use axum::Router; use std::net::SocketAddr; +use tower_http::compression::CompressionLayer; use tower_http::services::{ServeDir, ServeFile}; pub async fn run(state: AppState) -> Result<(), AppError> { @@ -33,7 +34,9 @@ pub async fn run(state: AppState) -> Result<(), AppError> { let static_service = ServeDir::new("static").not_found_service(ServeFile::new("static/index.html")); - let v1 = v1_router().layer(DefaultBodyLimit::max(100 * 1024 * 1024)); + let v1 = v1_router() + .layer(CompressionLayer::new()) + .layer(DefaultBodyLimit::max(100 * 1024 * 1024)); let app = Router::new() .route("/health", axum::routing::get(health::health)) diff --git a/src/api/tasks.rs b/src/api/tasks.rs index 9c56479..62e67df 100644 --- a/src/api/tasks.rs +++ b/src/api/tasks.rs @@ -190,13 +190,15 @@ async fn create_batch_task( } let mut anonymous_reserved_units = 0u32; + let mut anonymous_quota_date = None; let create_result: Result = (async { match &admission.task_owner { TaskOwner::Anonymous { session_id } => { let units = u32::try_from(files.len()).map_err(|_| { AppError::new(ErrorCode::InvalidRequest, "批量文件数量超出限制") })?; - quota::consume_anonymous_units(&state, session_id, ip, units).await?; + anonymous_quota_date = + Some(quota::reserve_anonymous_units(&state, session_id, ip, units).await?); anonymous_reserved_units = units; } TaskOwner::User { .. } | TaskOwner::ApiKey { .. } => { @@ -234,13 +236,13 @@ async fn create_batch_task( compression_rate, total_files, completed_files, failed_files, total_original_size, total_compressed_size, - expires_at, retention_hours, anonymous_units_reserved + expires_at, retention_hours, anonymous_units_reserved, anonymous_quota_date ) VALUES ( $1, $2, $3, $4, $5::inet, $6::task_source, 'pending', $7::compression_level, $8, $9, $10, $11, $12, $13, 0, 0, $14, 0, - $15, $16, $17 + $15, $16, $17, $18 ) "#, ) @@ -261,6 +263,7 @@ async fn create_batch_task( .bind(expires_at) .bind(retention_hours as i32) .bind(anonymous_reserved_units as i32) + .bind(anonymous_quota_date) .execute(&mut *tx) .await .map_err(|err| AppError::new(ErrorCode::Internal, "创建任务失败").with_source(err))?; @@ -344,13 +347,30 @@ async fn create_batch_task( Err(err) => { if anonymous_reserved_units > 0 { if let context::Principal::Anonymous { session_id } = &principal { - let _ = quota::refund_anonymous_units( - &state, - session_id, - ip, - anonymous_reserved_units, - ) - .await; + let settlement = + quota::settle_anonymous_task_reservation(&state, task_id).await; + match settlement { + Ok(Some(_)) => {} + Ok(None) => { + if let Some(date) = anonymous_quota_date { + if let Err(refund_err) = quota::refund_anonymous_reservation_once( + &state, + task_id, + session_id, + ip, + date, + anonymous_reserved_units, + ) + .await + { + tracing::warn!(task_id = %task_id, error = %refund_err, "failed to refund anonymous batch admission"); + } + } + } + Err(settle_err) => { + tracing::warn!(task_id = %task_id, error = %settle_err, "failed to settle anonymous batch admission"); + } + } } } if let (Some(scope), Some(idem_key)) = (idempotency_scope, idempotency_key.as_deref()) { @@ -950,6 +970,7 @@ async fn cancel_task( )?; if matches!(task.status.as_str(), "completed" | "failed" | "cancelled") { + quota::settle_anonymous_task_reservation(&state, task_id).await?; return Ok(( jar, Json(Envelope { @@ -971,6 +992,8 @@ async fn cancel_task( return Err(AppError::new(ErrorCode::InvalidRequest, "任务状态不可取消")); } + quota::settle_anonymous_task_reservation(&state, task_id).await?; + Ok(( jar, Json(Envelope { @@ -1019,6 +1042,24 @@ async fn delete_task( )); } + if task.status == "pending" { + let updated = sqlx::query( + "UPDATE tasks SET status = 'cancelled', completed_at = NOW() WHERE id = $1 AND status = 'pending'", + ) + .bind(task_id) + .execute(&state.db) + .await + .map_err(|err| AppError::new(ErrorCode::Internal, "锁定待删除任务失败").with_source(err))?; + if updated.rows_affected() == 0 { + return Err(AppError::new( + ErrorCode::InvalidRequest, + "任务状态已变化,请刷新后重试", + )); + } + } + + quota::settle_anonymous_task_reservation(&state, task_id).await?; + let files = sqlx::query_as::<_, TaskStorageRow>( r#" SELECT storage_backend, storage_endpoint_id, diff --git a/src/services/compress.rs b/src/services/compress.rs index 21cc50f..03a8975 100644 --- a/src/services/compress.rs +++ b/src/services/compress.rs @@ -16,18 +16,79 @@ use img_parts::{Bytes as ImgBytes, DynImage, ImageEXIF, ImageICC}; use oxipng::StripChunks; use rgb::FromSlice; use std::io::Cursor; -use std::time::Instant; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; const TARGET_MIN_LONG_EDGE: u32 = 640; const TARGET_MIN_SCALE: f64 = 0.55; const TARGET_SCALE_SEARCH_ATTEMPTS: usize = 6; const TARGET_QUALITY_INTERPOLATION_ATTEMPTS: usize = 2; +const COMPRESSION_TIME_BUDGET: Duration = Duration::from_secs(30); +const COMPRESSION_TIMEOUT_GRACE: Duration = Duration::from_secs(1); const JPEG_TARGET_MIN_QUALITY: u8 = 25; const WEBP_TARGET_MIN_QUALITY: u8 = 30; const AVIF_TARGET_MIN_QUALITY: u8 = 38; const METADATA_TARGET_OVERHEAD: u64 = 1024; +#[derive(Clone)] +struct CompressionDeadline { + deadline: Option, + cancelled: Arc, +} + +impl CompressionDeadline { + fn new(budget: Duration) -> Self { + Self { + deadline: Instant::now().checked_add(budget), + cancelled: Arc::new(AtomicBool::new(false)), + } + } + + #[cfg(test)] + fn unlimited() -> Self { + Self { + deadline: None, + cancelled: Arc::new(AtomicBool::new(false)), + } + } + + fn cancel(&self) { + self.cancelled.store(true, Ordering::Relaxed); + } + + fn expired(&self) -> bool { + self.cancelled.load(Ordering::Relaxed) + || self + .deadline + .is_some_and(|deadline| Instant::now() >= deadline) + } + + fn remaining(&self) -> Duration { + if self.cancelled.load(Ordering::Relaxed) { + return Duration::ZERO; + } + self.deadline + .map(|deadline| deadline.saturating_duration_since(Instant::now())) + .unwrap_or(Duration::MAX) + } + + fn check(&self) -> Result<(), AppError> { + if self.expired() { + return Err(compression_timeout_error()); + } + Ok(()) + } +} + +fn compression_timeout_error() -> AppError { + AppError::new( + ErrorCode::CompressionFailed, + "图片处理超过 30 秒,请降低分辨率或改用 JPEG/WebP 格式", + ) +} + #[derive(Debug, Clone, Copy)] pub enum CompressionLevel { High, @@ -294,16 +355,27 @@ pub async fn compress_image_bytes( .await? .file_limits .max_image_pixels; - let permit = state - .image_processing_semaphore - .clone() - .acquire_owned() - .await - .map_err(|err| { - AppError::new(ErrorCode::Internal, "图片处理并发控制器已关闭").with_source(err) - })?; + let deadline = CompressionDeadline::new(COMPRESSION_TIME_BUDGET); + let permit = match tokio::time::timeout( + deadline.remaining(), + state.image_processing_semaphore.clone().acquire_owned(), + ) + .await + { + Ok(Ok(permit)) => permit, + Ok(Err(err)) => { + return Err( + AppError::new(ErrorCode::Internal, "图片处理并发控制器已关闭").with_source(err), + ) + } + Err(_) => { + crate::services::metrics::record_compression(state, started.elapsed(), bytes_in, None); + return Err(compression_timeout_error()); + } + }; - let result = match tokio::task::spawn_blocking(move || { + let blocking_deadline = deadline.clone(); + let handle = tokio::task::spawn_blocking(move || { let _permit = permit; compress_image_bytes_sync( input, @@ -316,14 +388,25 @@ pub async fn compress_image_bytes( max_height, preserve_metadata, max_image_pixels, + &blocking_deadline, ) - }) + }); + let result = match tokio::time::timeout( + deadline + .remaining() + .saturating_add(COMPRESSION_TIMEOUT_GRACE), + handle, + ) .await { - Ok(result) => result, - Err(err) => Err( + Ok(Ok(result)) => result, + Ok(Err(err)) => Err( AppError::new(ErrorCode::CompressionFailed, "图片处理任务异常退出").with_source(err), ), + Err(_) => { + deadline.cancel(); + Err(compression_timeout_error()) + } }; crate::services::metrics::record_compression( state, @@ -346,7 +429,9 @@ fn compress_image_bytes_sync( max_height: Option, preserve_metadata: bool, max_image_pixels: u64, + deadline: &CompressionDeadline, ) -> Result, AppError> { + deadline.check()?; let original_size = input.len() as u64; #[cfg(not(target_os = "linux"))] @@ -358,6 +443,7 @@ fn compress_image_bytes_sync( } let orientation = inspect_image(&input, max_image_pixels)?; + deadline.check()?; if is_animated_image(&input, format_in)? { return Err(AppError::new( @@ -365,6 +451,7 @@ fn compress_image_bytes_sync( format!("暂不支持动画 {}", format_in.as_str().to_ascii_uppercase()), )); } + deadline.check()?; let retention_rate = effective_rate(compression_rate, level); // 优先使用直接指定的目标大小,其次根据百分比计算 @@ -446,24 +533,26 @@ fn compress_image_bytes_sync( })? } else { let mut image = decode_image(&input, format_in)?; + deadline.check()?; image.apply_orientation(orientation); let (image, did_resize) = resize_if_needed(image, max_width, max_height); transformed |= did_resize; + deadline.check()?; match format_out { ImageFmt::Png => encode_png(image, strength_rate, preserve_metadata)?, ImageFmt::Jpeg => match encoding_target_size { - Some(target) => encode_jpeg_target(image, target)?, + Some(target) => encode_jpeg_target(image, target, deadline)?, None => encode_jpeg(image, strength_rate)?, }, ImageFmt::Webp => match encoding_target_size { - Some(target) => encode_webp_target(image, target)?, + Some(target) => encode_webp_target(image, target, deadline)?, None => encode_webp(image, strength_rate)?, }, ImageFmt::Avif => match encoding_target_size { - Some(target) => encode_avif_target(image, target)?, + Some(target) => encode_avif_target(image, target, deadline)?, None => encode_avif(image, strength_rate)?, }, ImageFmt::Gif => encode_gif(image, strength_rate)?, @@ -472,7 +561,6 @@ fn compress_image_bytes_sync( ImageFmt::Ico => encode_ico(image)?, } }; - if preserve_metadata { output = apply_metadata(output, icc_profile, exif)?; } @@ -775,28 +863,61 @@ fn encode_avif_raw(raw: &[u8], w: u32, h: u32, quality: u8) -> Result, A Ok(encoded.avif_file) } -fn encode_jpeg_target(image: DynamicImage, target_size: u64) -> Result, AppError> { - encode_with_auto_resize(image, target_size, JPEG_TARGET_MIN_QUALITY, 95, |img, q| { - let rgb = jpeg_rgb(img); - let (w, h) = rgb.dimensions(); - encode_jpeg_raw(rgb.as_raw(), w, h, q) - }) +fn encode_jpeg_target( + image: DynamicImage, + target_size: u64, + deadline: &CompressionDeadline, +) -> Result, AppError> { + encode_with_auto_resize( + image, + target_size, + JPEG_TARGET_MIN_QUALITY, + 95, + deadline, + |img, q| { + let rgb = jpeg_rgb(img); + let (w, h) = rgb.dimensions(); + encode_jpeg_raw(rgb.as_raw(), w, h, q) + }, + ) } -fn encode_webp_target(image: DynamicImage, target_size: u64) -> Result, AppError> { - encode_with_auto_resize(image, target_size, WEBP_TARGET_MIN_QUALITY, 95, |img, q| { - let rgba = img.to_rgba8(); - let (w, h) = rgba.dimensions(); - encode_webp_raw(rgba.as_raw(), w, h, q) - }) +fn encode_webp_target( + image: DynamicImage, + target_size: u64, + deadline: &CompressionDeadline, +) -> Result, AppError> { + encode_with_auto_resize( + image, + target_size, + WEBP_TARGET_MIN_QUALITY, + 95, + deadline, + |img, q| { + let rgba = img.to_rgba8(); + let (w, h) = rgba.dimensions(); + encode_webp_raw(rgba.as_raw(), w, h, q) + }, + ) } -fn encode_avif_target(image: DynamicImage, target_size: u64) -> Result, AppError> { - encode_with_auto_resize(image, target_size, AVIF_TARGET_MIN_QUALITY, 95, |img, q| { - let rgba = img.to_rgba8(); - let (w, h) = rgba.dimensions(); - encode_avif_raw(rgba.as_raw(), w, h, q) - }) +fn encode_avif_target( + image: DynamicImage, + target_size: u64, + deadline: &CompressionDeadline, +) -> Result, AppError> { + encode_with_auto_resize( + image, + target_size, + AVIF_TARGET_MIN_QUALITY, + 95, + deadline, + |img, q| { + let rgba = img.to_rgba8(); + let (w, h) = rgba.dimensions(); + encode_avif_raw(rgba.as_raw(), w, h, q) + }, + ) } /// 目标体积压缩(质量优先 + 有边界的降尺寸) @@ -811,18 +932,27 @@ fn encode_with_auto_resize( target_size: u64, min_q: u8, max_q: u8, + deadline: &CompressionDeadline, mut encode_fn: F, ) -> Result, AppError> where F: FnMut(&DynamicImage, u8) -> Result, AppError>, { + deadline.check()?; let (orig_w, orig_h) = image.dimensions(); let long_edge = orig_w.max(orig_h); let long_edge_floor = TARGET_MIN_LONG_EDGE.min(long_edge) as f64 / long_edge as f64; let min_scale = TARGET_MIN_SCALE.max(long_edge_floor).min(1.0); - let (full_result, full_dimensions) = - encode_target_at_scale(&image, 1.0, min_q, max_q, target_size, &mut encode_fn)?; + let (full_result, full_dimensions) = encode_target_at_scale( + &image, + 1.0, + min_q, + max_q, + target_size, + deadline, + &mut encode_fn, + )?; if full_result.len() as u64 <= target_size { return Ok(full_result); } @@ -834,8 +964,16 @@ where )); } - let (min_result, min_dimensions) = - encode_target_at_scale(&image, min_scale, min_q, max_q, target_size, &mut encode_fn)?; + deadline.check()?; + let (min_result, min_dimensions) = encode_target_at_scale( + &image, + min_scale, + min_q, + max_q, + target_size, + deadline, + &mut encode_fn, + )?; if min_result.len() as u64 > target_size { return Err(target_unreachable_error( target_size, @@ -849,7 +987,10 @@ where let mut over_scale = 1.0; let mut over_dimensions = full_dimensions; - for _ in 0..TARGET_SCALE_SEARCH_ATTEMPTS { + for _ in 0..scale_search_attempts(orig_w, orig_h) { + if deadline.expired() { + return Ok(best_under); + } let candidate_scale = (under_scale + over_scale) / 2.0; let candidate_dimensions = dimensions_at_scale(orig_w, orig_h, candidate_scale); @@ -868,6 +1009,7 @@ where min_q, max_q, target_size, + deadline, &mut encode_fn, )?; if candidate_result.len() as u64 <= target_size { @@ -883,6 +1025,14 @@ where Ok(best_under) } +fn scale_search_attempts(width: u32, height: u32) -> usize { + match u64::from(width).saturating_mul(u64::from(height)) { + 12_000_000.. => 3, + 4_000_000.. => 4, + _ => TARGET_SCALE_SEARCH_ATTEMPTS, + } +} + fn dimensions_at_scale(orig_w: u32, orig_h: u32, scale: f64) -> (u32, u32) { ( ((orig_w as f64 * scale).round() as u32).clamp(1, orig_w), @@ -896,11 +1046,13 @@ fn encode_target_at_scale( min_q: u8, max_q: u8, target_size: u64, + deadline: &CompressionDeadline, encode_fn: &mut F, ) -> Result<(Vec, (u32, u32)), AppError> where F: FnMut(&DynamicImage, u8) -> Result, AppError>, { + deadline.check()?; let (orig_w, orig_h) = image.dimensions(); let dimensions = dimensions_at_scale(orig_w, orig_h, scale); let resized = (dimensions != (orig_w, orig_h)).then(|| { @@ -910,8 +1062,16 @@ where image::imageops::FilterType::Lanczos3, ) }); + deadline.check()?; let candidate = resized.as_ref().unwrap_or(image); - let result = encode_target_quality_with_image(candidate, min_q, max_q, target_size, encode_fn)?; + let result = encode_target_quality_with_image( + candidate, + min_q, + max_q, + target_size, + deadline, + encode_fn, + )?; Ok((result, dimensions)) } @@ -921,11 +1081,13 @@ fn encode_target_quality_with_image( min_q: u8, max_q: u8, target_size: u64, + deadline: &CompressionDeadline, encode_fn: &mut F, ) -> Result, AppError> where F: FnMut(&DynamicImage, u8) -> Result, AppError>, { + deadline.check()?; // Start with the highest quality. If it already fits, no lower-quality // encodes can improve the result. let max_quality = encode_fn(image, max_q)?; @@ -934,6 +1096,7 @@ where return Ok(max_quality); } + deadline.check()?; let min_quality = encode_fn(image, min_q)?; let min_size = min_quality.len() as u64; if min_size > target_size { @@ -950,6 +1113,9 @@ where // jumps near the target first; the binary phase still proves the exact // highest fitting integer quality, so output quality is not approximated. for _ in 0..TARGET_QUALITY_INTERPOLATION_ATTEMPTS { + if deadline.expired() { + return Ok(best_under); + } if under_q.saturating_add(1) >= over_q { break; } @@ -979,6 +1145,9 @@ where } while under_q.saturating_add(1) < over_q { + if deadline.expired() { + return Ok(best_under); + } let mid = under_q + (over_q - under_q) / 2; let bytes = encode_fn(image, mid)?; let size = bytes.len() as u64; @@ -1372,6 +1541,7 @@ mod tests { None, false, 1_000_000, + &CompressionDeadline::unlimited(), ) .unwrap(); assert_eq!(detect_format(&output).unwrap(), ImageFmt::Webp); @@ -1406,10 +1576,17 @@ mod tests { let calls = Cell::new(0); let image = DynamicImage::new_rgb8(800, 600); - let result = encode_with_auto_resize(image, 100, 40, 95, |_image, quality| { - calls.set(calls.get() + 1); - Ok(vec![0; quality as usize]) - }) + let result = encode_with_auto_resize( + image, + 100, + 40, + 95, + &CompressionDeadline::unlimited(), + |_image, quality| { + calls.set(calls.get() + 1); + Ok(vec![0; quality as usize]) + }, + ) .unwrap(); assert_eq!(result.len(), 95); @@ -1422,12 +1599,18 @@ mod tests { let calls = RefCell::new(Vec::new()); let image = DynamicImage::new_rgb8(32, 32); - let result = - encode_target_quality_with_image(&image, 25, 95, 50_000, &mut |_image, quality| { + let result = encode_target_quality_with_image( + &image, + 25, + 95, + 50_000, + &CompressionDeadline::unlimited(), + &mut |_image, quality| { calls.borrow_mut().push(quality); Ok(vec![0; usize::from(quality) * 1_000]) - }) - .unwrap(); + }, + ) + .unwrap(); assert_eq!(result.len(), 50_000); assert_eq!(*calls.borrow(), vec![95, 25, 50, 51]); @@ -1436,10 +1619,17 @@ mod tests { #[test] fn target_encoder_can_reduce_a_landscape_at_the_long_edge_floor() { let image = DynamicImage::new_rgb8(960, 640); - let result = encode_with_auto_resize(image, 40_000, 40, 40, |image, _quality| { - let (width, height) = image.dimensions(); - Ok(vec![0; (width as usize * height as usize) / 10]) - }) + let result = encode_with_auto_resize( + image, + 40_000, + 40, + 40, + &CompressionDeadline::unlimited(), + |image, _quality| { + let (width, height) = image.dimensions(); + Ok(vec![0; (width as usize * height as usize) / 10]) + }, + ) .unwrap(); assert!(result.len() <= 40_000); @@ -1455,6 +1645,7 @@ mod tests { 1_000, 40, 40, + &CompressionDeadline::unlimited(), |_image, _quality| { calls.set(calls.get() + 1); Ok(vec![0; 50_000]) @@ -1467,6 +1658,74 @@ mod tests { assert_eq!(calls.get(), 2); } + #[test] + fn target_encoder_rejects_work_after_the_deadline() { + use std::cell::Cell; + + let calls = Cell::new(0); + let deadline = CompressionDeadline::unlimited(); + deadline.cancel(); + let error = encode_with_auto_resize( + DynamicImage::new_rgb8(960, 640), + 40_000, + 40, + 40, + &deadline, + |_image, _quality| { + calls.set(calls.get() + 1); + Ok(vec![0; 1]) + }, + ) + .unwrap_err(); + + assert_eq!(error.code, ErrorCode::CompressionFailed); + assert_eq!(calls.get(), 0); + } + + #[test] + fn compression_deadline_reports_no_remaining_time_after_cancellation() { + let deadline = CompressionDeadline::unlimited(); + assert_eq!(deadline.remaining(), Duration::MAX); + + deadline.cancel(); + assert_eq!(deadline.remaining(), Duration::ZERO); + } + + #[test] + fn target_encoder_returns_the_best_candidate_when_search_is_cancelled() { + use std::cell::Cell; + + let calls = Cell::new(0); + let deadline = CompressionDeadline::unlimited(); + let controller = deadline.clone(); + let result = encode_with_auto_resize( + DynamicImage::new_rgb8(960, 640), + 40_000, + 40, + 40, + &deadline, + |image, _quality| { + calls.set(calls.get() + 1); + if calls.get() == 2 { + controller.cancel(); + } + let (width, height) = image.dimensions(); + Ok(vec![0; (width as usize * height as usize) / 10]) + }, + ) + .unwrap(); + + assert!(result.len() <= 40_000); + assert_eq!(calls.get(), 2); + } + + #[test] + fn large_images_use_a_smaller_scale_search_budget() { + assert_eq!(scale_search_attempts(4_000, 3_000), 3); + assert_eq!(scale_search_attempts(2_000, 2_000), 4); + assert_eq!(scale_search_attempts(1_999, 2_000), 6); + } + #[test] fn png_fast_path_enforces_the_pixel_limit() { let input = encode_png(DynamicImage::new_rgb8(20, 20), 100, false).unwrap(); @@ -1481,6 +1740,7 @@ mod tests { None, true, 399, + &CompressionDeadline::unlimited(), ) .unwrap_err(); @@ -1506,6 +1766,7 @@ mod tests { None, false, 1_000_000, + &CompressionDeadline::unlimited(), ) .unwrap(); let decoded = image::load_from_memory(&output).unwrap(); @@ -1564,6 +1825,7 @@ mod tests { None, false, 1_000_000, + &CompressionDeadline::unlimited(), ) .unwrap(); diff --git a/src/services/quota.rs b/src/services/quota.rs index db6b18a..f1a708d 100644 --- a/src/services/quota.rs +++ b/src/services/quota.rs @@ -2,7 +2,7 @@ use crate::error::{AppError, ErrorCode}; use crate::services::billing::BillingContext; use crate::state::AppState; -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Duration, NaiveDate, Utc}; use sqlx::{FromRow, Postgres, Transaction}; use std::net::IpAddr; use uuid::Uuid; @@ -249,13 +249,24 @@ pub async fn consume_anonymous_units( ip: IpAddr, units: u32, ) -> Result<(), AppError> { + reserve_anonymous_units(state, session_id, ip, units) + .await + .map(|_| ()) +} + +pub async fn reserve_anonymous_units( + state: &AppState, + session_id: &str, + ip: IpAddr, + units: u32, +) -> Result { + let date = utc8_date(); if units == 0 { - return Ok(()); + return Ok(date); } - let date = utc8_date(); - let session_key = format!("anon_quota:{session_id}:{date}"); - let ip_key = format!("anon_quota_ip:{ip}:{date}"); + let session_key = anonymous_session_key(session_id, date); + let ip_key = anonymous_ip_key(ip, date); let mut conn = state.redis.clone(); @@ -306,7 +317,7 @@ pub async fn consume_anonymous_units( )); } - Ok(()) + Ok(date) } pub async fn refund_anonymous_units( @@ -314,14 +325,23 @@ pub async fn refund_anonymous_units( session_id: &str, ip: IpAddr, units: u32, +) -> Result<(), AppError> { + refund_anonymous_units_for_date(state, session_id, ip, utc8_date(), units).await +} + +async fn refund_anonymous_units_for_date( + state: &AppState, + session_id: &str, + ip: IpAddr, + date: NaiveDate, + units: u32, ) -> Result<(), AppError> { if units == 0 { return Ok(()); } - let date = utc8_date(); - let session_key = format!("anon_quota:{session_id}:{date}"); - let ip_key = format!("anon_quota_ip:{ip}:{date}"); + let session_key = anonymous_session_key(session_id, date); + let ip_key = anonymous_ip_key(ip, date); let mut conn = state.redis.clone(); let script = redis::Script::new( r#" @@ -349,9 +369,171 @@ pub async fn refund_anonymous_units( Ok(()) } -fn utc8_date() -> String { +pub async fn refund_anonymous_reservation_once( + state: &AppState, + task_id: Uuid, + session_id: &str, + ip: IpAddr, + date: NaiveDate, + units: u32, +) -> Result<(), AppError> { + if units == 0 { + return Ok(()); + } + + let session_key = anonymous_session_key(session_id, date); + let ip_key = anonymous_ip_key(ip, date); + let marker_key = format!("anon_quota_refund:{task_id}"); + let mut conn = state.redis.clone(); + let script = redis::Script::new( + r#" + local dec = tonumber(ARGV[1]) + local ttl = tonumber(ARGV[2]) + + if redis.call('EXISTS', KEYS[3]) == 1 then + return 0 + end + + local function refund(key) + local current = tonumber(redis.call('GET', key) or '0') + if current <= 0 then return 0 end + return redis.call('DECRBY', key, math.min(current, dec)) + end + + refund(KEYS[1]) + refund(KEYS[2]) + redis.call('SET', KEYS[3], '1', 'EX', ttl) + return 1 + "#, + ); + + let _: i64 = script + .key(session_key) + .key(ip_key) + .key(marker_key) + .arg(units as i64) + .arg(48 * 60 * 60) + .invoke_async(&mut conn) + .await + .map_err(|err| { + AppError::new(ErrorCode::Internal, "退还匿名任务配额失败").with_source(err) + })?; + Ok(()) +} + +#[derive(Debug, FromRow)] +struct AnonymousTaskReservationRow { + anonymous_units_reserved: i32, + anonymous_quota_date: Option, + total_files: i32, + consumed_units: i32, + session_id: Option, + client_ip: Option, + created_at: DateTime, +} + +pub async fn settle_anonymous_task_reservation( + state: &AppState, + task_id: Uuid, +) -> Result, AppError> { + let mut tx = state.db.begin().await.map_err(|err| { + AppError::new(ErrorCode::Internal, "开启匿名配额结算事务失败").with_source(err) + })?; + let row = sqlx::query_as::<_, AnonymousTaskReservationRow>( + r#" + SELECT anonymous_units_reserved, + anonymous_quota_date, + total_files, + COALESCE(( + SELECT COUNT(*)::integer + FROM task_files f + WHERE f.task_id = tasks.id + AND f.status = 'completed' + AND f.compressed_size < f.original_size + AND NOT ( + tasks.compression_rate = 100 + AND f.original_format = f.output_format + AND tasks.max_width IS NULL + AND tasks.max_height IS NULL + ) + ), 0) AS consumed_units, + session_id, + host(client_ip) AS client_ip, + created_at + FROM tasks + WHERE id = $1 + FOR UPDATE + "#, + ) + .bind(task_id) + .fetch_optional(&mut *tx) + .await + .map_err(|err| AppError::new(ErrorCode::Internal, "查询匿名任务配额失败").with_source(err))?; + + let Some(row) = row else { + tx.rollback().await.ok(); + return Ok(None); + }; + if row.anonymous_units_reserved <= 0 { + tx.rollback().await.ok(); + return Ok(Some(0)); + } + + let refundable = refundable_reserved_units( + row.anonymous_units_reserved, + row.total_files, + row.consumed_units, + ); + if refundable > 0 { + let session_id = row + .session_id + .as_deref() + .filter(|value| !value.is_empty()) + .ok_or_else(|| AppError::new(ErrorCode::Internal, "匿名任务缺少 session_id"))?; + let ip = row + .client_ip + .as_deref() + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| AppError::new(ErrorCode::Internal, "匿名任务缺少 client_ip"))?; + let date = row + .anonymous_quota_date + .unwrap_or_else(|| (row.created_at + Duration::hours(8)).date_naive()); + refund_anonymous_reservation_once(state, task_id, session_id, ip, date, refundable).await?; + } + + sqlx::query("UPDATE tasks SET anonymous_units_reserved = 0 WHERE id = $1") + .bind(task_id) + .execute(&mut *tx) + .await + .map_err(|err| { + AppError::new(ErrorCode::Internal, "完成匿名配额结算失败").with_source(err) + })?; + tx.commit().await.map_err(|err| { + AppError::new(ErrorCode::Internal, "提交匿名配额结算失败").with_source(err) + })?; + Ok(Some(refundable)) +} + +fn refundable_reserved_units(reserved: i32, total_files: i32, consumed_units: i32) -> u32 { + let reserved = reserved.max(0); + let total_files = total_files.max(0); + let consumed_units = consumed_units.clamp(0, total_files); + let unused_reservation = reserved.saturating_sub(consumed_units); + let unfinished_files = total_files.saturating_sub(consumed_units); + u32::try_from(unused_reservation.min(unfinished_files)).unwrap_or(0) +} + +fn anonymous_session_key(session_id: &str, date: NaiveDate) -> String { + format!("anon_quota:{session_id}:{}", date.format("%Y-%m-%d")) +} + +fn anonymous_ip_key(ip: IpAddr, date: NaiveDate) -> String { + format!("anon_quota_ip:{ip}:{}", date.format("%Y-%m-%d")) +} + +fn utc8_date() -> NaiveDate { let now = Utc::now() + Duration::hours(8); - now.format("%Y-%m-%d").to_string() + now.date_naive() } #[cfg(test)] @@ -391,4 +573,32 @@ mod tests { now + Duration::days(20), )); } + + #[test] + fn anonymous_reservation_refunds_only_unconsumed_units() { + assert_eq!(refundable_reserved_units(10, 10, 6), 4); + assert_eq!(refundable_reserved_units(10, 10, 10), 0); + assert_eq!(refundable_reserved_units(10, 10, 0), 10); + } + + #[test] + fn anonymous_reservation_refund_is_clamped_to_valid_bounds() { + assert_eq!(refundable_reserved_units(3, 10, 2), 1); + assert_eq!(refundable_reserved_units(10, 3, 1), 2); + assert_eq!(refundable_reserved_units(-1, 10, 0), 0); + assert_eq!(refundable_reserved_units(10, -1, -2), 0); + } + + #[test] + fn anonymous_quota_keys_use_the_reserved_date() { + let date = NaiveDate::from_ymd_opt(2026, 7, 25).unwrap(); + assert_eq!( + anonymous_session_key("session", date), + "anon_quota:session:2026-07-25" + ); + assert_eq!( + anonymous_ip_key("127.0.0.1".parse().unwrap(), date), + "anon_quota_ip:127.0.0.1:2026-07-25" + ); + } } diff --git a/src/worker/mod.rs b/src/worker/mod.rs index ba25436..0fc8ab9 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -28,7 +28,7 @@ pub async fn run(state: AppState) -> Result<(), AppError> { crate::services::bootstrap::ensure_schema(&state).await?; let consumer = format!("worker_{}", Uuid::new_v4()); - ensure_group(&state, &consumer).await?; + ensure_group(&state).await?; let mut last_maintenance = Instant::now(); @@ -47,7 +47,7 @@ pub async fn run(state: AppState) -> Result<(), AppError> { } } -async fn ensure_group(state: &AppState, _consumer: &str) -> Result<(), AppError> { +async fn ensure_group(state: &AppState) -> Result<(), AppError> { let mut conn = state.redis.clone(); let res: Result = redis::cmd("XGROUP") @@ -284,6 +284,7 @@ async fn mark_task_dead_letter( tx.commit() .await .map_err(|err| AppError::new(ErrorCode::Internal, "提交死信事务失败").with_source(err))?; + quota::settle_anonymous_task_reservation(state, task_id).await?; Ok(()) } @@ -384,7 +385,12 @@ async fn process_task(state: &AppState, task_id: Uuid) -> Result<(), AppError> { return Ok(()); }; - if matches!(task.status.as_str(), "completed" | "failed" | "cancelled") { + if task.status == "cancelled" { + finalize_task_status(state, task_id).await?; + return Ok(()); + } + if matches!(task.status.as_str(), "completed" | "failed") { + quota::settle_anonymous_task_reservation(state, task_id).await?; return Ok(()); } @@ -752,6 +758,18 @@ async fn finalize_file( .await .map_err(|err| AppError::new(ErrorCode::Internal, "开启事务失败").with_source(err))?; + let task_status: Option = + sqlx::query_scalar("SELECT status::text FROM tasks WHERE id = $1 FOR UPDATE") + .bind(task_id) + .fetch_optional(&mut *tx) + .await + .map_err(|err| { + AppError::new(ErrorCode::Internal, "锁定任务状态失败").with_source(err) + })?; + if !matches!(task_status.as_deref(), Some("pending" | "processing")) { + return Err(AppError::new(ErrorCode::InvalidRequest, "任务已结束")); + } + // Paid users: charge before marking file completed (atomic w/ status update). if charge_units { if let Some(billing) = billing_ctx { @@ -923,11 +941,18 @@ async fn finalize_task_status(state: &AppState, task_id: Uuid) -> Result<(), App .await; let _ = sqlx::query( - "UPDATE tasks SET failed_files = GREATEST(total_files - completed_files, 0), completed_at = NOW() WHERE id = $1 AND completed_at IS NULL", + r#" + UPDATE tasks + SET completed_files = (SELECT COUNT(*) FROM task_files WHERE task_id = $1 AND status = 'completed'), + failed_files = (SELECT COUNT(*) FROM task_files WHERE task_id = $1 AND status = 'failed'), + completed_at = COALESCE(completed_at, NOW()) + WHERE id = $1 + "#, ) .bind(task_id) .execute(&state.db) .await; + quota::settle_anonymous_task_reservation(state, task_id).await?; return Ok(()); } @@ -945,6 +970,7 @@ async fn finalize_task_status(state: &AppState, task_id: Uuid) -> Result<(), App .execute(&state.db) .await .map_err(|err| AppError::new(ErrorCode::Internal, "更新任务状态失败").with_source(err))?; + quota::settle_anonymous_task_reservation(state, task_id).await?; } Ok(()) @@ -995,12 +1021,34 @@ async fn charge_one_unit( } async fn maintenance(state: &AppState) -> Result<(), AppError> { + settle_finished_anonymous_reservations(state).await?; cleanup_expired_tasks(state).await?; cleanup_stale_zip_temp(state).await?; cleanup_expired_records(state).await?; Ok(()) } +async fn settle_finished_anonymous_reservations(state: &AppState) -> Result<(), AppError> { + let task_ids: Vec = sqlx::query_scalar( + r#" + SELECT id + FROM tasks + WHERE anonymous_units_reserved > 0 + AND status IN ('completed', 'failed', 'cancelled') + ORDER BY completed_at ASC NULLS FIRST + LIMIT 200 + "#, + ) + .fetch_all(&state.db) + .await + .map_err(|err| AppError::new(ErrorCode::Internal, "查询待结算匿名任务失败").with_source(err))?; + + for task_id in task_ids { + quota::settle_anonymous_task_reservation(state, task_id).await?; + } + Ok(()) +} + async fn cleanup_stale_zip_temp(state: &AppState) -> Result<(), AppError> { let root = std::path::Path::new(&state.config.storage_path).join("tmp/zips"); let mut entries = match tokio::fs::read_dir(&root).await { @@ -1092,6 +1140,15 @@ async fn cleanup_expired_tasks(state: &AppState) -> Result<(), AppError> { } async fn cleanup_expired_task(state: &AppState, task_id: Uuid) -> Result<(), AppError> { + sqlx::query( + "UPDATE tasks SET status = 'cancelled', completed_at = COALESCE(completed_at, NOW()) WHERE id = $1 AND expires_at < NOW() AND status IN ('pending', 'processing')", + ) + .bind(task_id) + .execute(&state.db) + .await + .map_err(|err| AppError::new(ErrorCode::Internal, "终止过期任务失败").with_source(err))?; + quota::settle_anonymous_task_reservation(state, task_id).await?; + let files: Vec = sqlx::query_as( r#" SELECT storage_backend, storage_endpoint_id,