diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ac431..e59c524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Trailing slashes in URLs are now trimmed for all endpoints before processing (`/studies/` and `/studies` are equivalent). - Return HTTP status code 200 (OK) instead of 204 (No Content) for QIDO-RS/MWL responses where there were no matches ([GH-51](https://github.com/UMEssen/DICOM-RST/pull/51), [CP-2473](https://www.dicomstandard.org/news-dir/current/docs/cpack134/cp2473.pdf)). +### Fixed + +- Correctly return 413 (Payload Too Large) if the request body exceeds the configured `max-upload-size`. +- The association pool no longer leaks semaphore permits when the association is rejected ([GH-56](https://github.com/UMEssen/DICOM-RST/issues/56)). + ## [0.2.1] ### Added diff --git a/Cargo.lock b/Cargo.lock index f02f7ed..36d0ed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,6 +237,22 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "astral-tokio-tar" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693" +dependencies = [ + "filetime", + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -953,6 +969,80 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bitflags", + "bollard-buildkit-proto", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http 1.3.1", + "http-body-util", + "hyper 1.7.0", + "hyper-named-pipe", + "hyper-rustls 0.27.7", + "hyper-util", + "hyperlocal", + "log", + "num", + "pin-project-lite", + "rand 0.9.2", + "rustls 0.23.34", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-buildkit-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-prost", + "ureq", +] + +[[package]] +name = "bollard-stubs" +version = "1.52.1-rc.29.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" +dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "prost", + "serde", + "serde_json", + "serde_repr", + "time", +] + [[package]] name = "built" version = "0.7.7" @@ -1054,6 +1144,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.42" @@ -1062,6 +1169,7 @@ checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "num-traits", + "serde", "windows-link 0.2.1", ] @@ -1091,6 +1199,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "config" version = "0.15.18" @@ -1181,6 +1299,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.3.0" @@ -1280,6 +1407,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "debugid" version = "0.8.0" @@ -1317,6 +1479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -1369,7 +1532,7 @@ dependencies = [ "num-traits", "safe-transmute", "smallvec", - "snafu", + "snafu 0.8.9", ] [[package]] @@ -1396,7 +1559,7 @@ dependencies = [ "dicom-transfer-syntax-registry", "owo-colors", "serde_json", - "snafu", + "snafu 0.8.9", "terminal_size", ] @@ -1411,7 +1574,7 @@ dependencies = [ "dicom-dictionary-std", "encoding", "inventory", - "snafu", + "snafu 0.8.9", ] [[package]] @@ -1444,7 +1607,7 @@ dependencies = [ "dicom-transfer-syntax-registry", "itertools 0.14.0", "smallvec", - "snafu", + "snafu 0.8.9", "tracing", ] @@ -1458,7 +1621,7 @@ dependencies = [ "dicom-dictionary-std", "dicom-encoding", "smallvec", - "snafu", + "snafu 0.8.9", "tracing", ] @@ -1477,7 +1640,7 @@ dependencies = [ "image", "num-traits", "rayon", - "snafu", + "snafu 0.8.9", "tracing", ] @@ -1499,7 +1662,10 @@ dependencies = [ "dicom", "dicom-json", "dicom-pixeldata", + "dicom-test-files", + "dicom-web", "futures", + "http-body-util", "image", "mime", "multer", @@ -1507,6 +1673,7 @@ dependencies = [ "sentry", "serde", "serde_json", + "testcontainers", "thiserror 2.0.17", "tokio", "tower", @@ -1517,6 +1684,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "dicom-test-files" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d580c9f46de0f4e89e1b7f98e79b980f03b5bb4f15e0045bc4687ae107dd185b" +dependencies = [ + "sha2", + "tempfile", + "ureq", + "zstd", +] + [[package]] name = "dicom-transfer-syntax-registry" version = "0.9.0" @@ -1544,10 +1723,29 @@ dependencies = [ "bytes", "dicom-encoding", "dicom-transfer-syntax-registry", - "snafu", + "snafu 0.8.9", "tracing", ] +[[package]] +name = "dicom-web" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0a9ff3f321dfdf99068826261a5cbefb07d62d943e93ae893d24ed25caf17be" +dependencies = [ + "dicom-core", + "dicom-json", + "dicom-object", + "futures-util", + "mediatype 0.21.0", + "multipart-rs", + "rand 0.10.1", + "reqwest 0.13.3", + "serde", + "serde_json", + "snafu 0.9.0", +] + [[package]] name = "digest" version = "0.10.7" @@ -1579,12 +1777,29 @@ dependencies = [ "const-random", ] +[[package]] +name = "docker_credential" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4564c274ebf369f501de192b02a0b81a5c4bda375abfe526aa70fc702fa6fa0" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.14.8" @@ -1743,6 +1958,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "exr" version = "1.73.0" @@ -1793,6 +2018,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ferroid" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" +dependencies = [ + "portable-atomic", + "rand 0.10.1", + "web-time", +] + [[package]] name = "ff" version = "0.12.1" @@ -1803,6 +2039,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -1896,9 +2143,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1906,9 +2153,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" @@ -1923,15 +2170,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1940,21 +2187,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1964,7 +2211,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1985,8 +2231,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1994,11 +2242,27 @@ name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", + "wasip3", ] [[package]] @@ -2046,7 +2310,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2065,7 +2329,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2083,6 +2347,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -2145,6 +2415,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "hostname" version = "0.4.1" @@ -2271,6 +2550,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.7.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -2303,6 +2597,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.7.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -2343,6 +2650,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.7.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -2453,6 +2775,18 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -2522,12 +2856,25 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.12.0" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -2616,6 +2963,55 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.17", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2643,10 +3039,12 @@ checksum = "b454d911ac55068f53495488d8ccd0646eaa540c033a28ee15b07838afafb01f" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2831,6 +3229,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -2863,6 +3267,18 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2925,6 +3341,12 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -2960,6 +3382,18 @@ dependencies = [ "digest", ] +[[package]] +name = "mediatype" +version = "0.19.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" + +[[package]] +name = "mediatype" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120fa187be19d9962f0926633453784691731018a2bf936ddb4e29101b79c4a7" + [[package]] name = "memchr" version = "2.7.6" @@ -3027,6 +3461,20 @@ dependencies = [ "version_check", ] +[[package]] +name = "multipart-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1653328875ea3613bd4725fc09707d7f3ae7f07a4f0f48443a0d058a17638b" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "mediatype 0.19.20", + "memchr", + "uuid", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -3075,6 +3523,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3085,6 +3547,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3111,6 +3582,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -3257,11 +3739,36 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn", +] + [[package]] name = "paste" version = "1.0.15" @@ -3380,6 +3887,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -3387,7 +3900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap", + "indexmap 2.12.0", "quick-xml", "serde", "time", @@ -3406,6 +3919,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.3" @@ -3468,6 +3987,38 @@ dependencies = [ "syn", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + [[package]] name = "pxfm" version = "0.1.25" @@ -3501,6 +4052,62 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.34", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.34", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.41" @@ -3516,6 +4123,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 = "rand" version = "0.8.5" @@ -3537,6 +4150,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3575,6 +4199,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rav1e" version = "0.7.1" @@ -3654,6 +4284,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.2" @@ -3691,9 +4350,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -3728,37 +4387,79 @@ dependencies = [ ] [[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" - -[[package]] -name = "ring" -version = "0.17.14" +name = "reqwest" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.34", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] name = "ron" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3833,7 +4534,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.103.7", "subtle", @@ -3867,9 +4570,37 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.34", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.7", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -3910,6 +4641,15 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -3919,6 +4659,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3999,7 +4763,7 @@ checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20" dependencies = [ "httpdate", "native-tls", - "reqwest", + "reqwest 0.12.28", "sentry-actix", "sentry-backtrace", "sentry-contexts", @@ -4161,7 +4925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap", + "indexmap 2.12.0", "itoa", "ryu", "serde_core", @@ -4169,16 +4933,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap", + "indexmap 2.12.0", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -4192,6 +4956,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -4222,6 +4997,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4229,7 +5035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4240,7 +5046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4284,6 +5090,16 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -4293,6 +5109,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.11" @@ -4311,7 +5133,16 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" dependencies = [ - "snafu-derive", + "snafu-derive 0.8.9", +] + +[[package]] +name = "snafu" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d4bced6a69f90b2056c03dcff2c4737f98d6fb9e0853493996e1d253ca29c6" +dependencies = [ + "snafu-derive 0.9.0", ] [[package]] @@ -4326,6 +5157,18 @@ dependencies = [ "syn", ] +[[package]] +name = "snafu-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54254b8531cafa275c5e096f62d48c81435d1015405a91198ddb11e967301d40" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.5.10" @@ -4374,6 +5217,35 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4472,6 +5344,37 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "testcontainers" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera", + "ferroid", + "futures", + "http 1.3.1", + "itertools 0.14.0", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4585,6 +5488,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -4716,7 +5634,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.12.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -4732,6 +5650,46 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2 0.6.1", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" @@ -4740,7 +5698,9 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.12.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", "tokio-util", @@ -4751,9 +5711,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -4908,14 +5868,17 @@ checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" dependencies = [ "base64 0.22.1", "der 0.7.10", + "flate2", "log", "native-tls", "percent-encoding", + "rustls 0.23.34", "rustls-pemfile", "rustls-pki-types", "ureq-proto", "utf-8", "webpki-root-certs", + "webpki-roots", ] [[package]] @@ -5013,6 +5976,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -5034,54 +6007,46 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", ] [[package]] -name = "wasm-bindgen" -version = "0.2.104" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "wit-bindgen 0.51.0", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" +name = "wasm-bindgen" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5089,31 +6054,88 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.12.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -5128,6 +6150,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.10" @@ -5150,6 +6181,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -5401,12 +6441,110 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.12.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.12.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xmlparser" version = "0.13.6" @@ -5528,6 +6666,40 @@ dependencies = [ "syn", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index b655491..c26edde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,11 +52,18 @@ bytes = "1.10.1" multer = "3.1.0" pin-project = "1.1.10" image = { version = "0.25.8", features = ["png", "jpeg", "gif"] } +http-body-util = "0.1.3" + # S3 backend aws-config = { version = "1.8.14", features = ["behavior-version-latest"], optional = true } aws-sdk-s3 = { version = "1.124.0", optional = true } aws-credential-types = { version = "1.2.13", optional = true } +[dev-dependencies] +testcontainers = "0.27.3" +dicom-test-files = "0.4.0" +dicom-web = "0.5.0" + [lints.rust] unsafe_code = "forbid" renamed_and_removed_lints = "allow" diff --git a/src/api/stow/routes.rs b/src/api/stow/routes.rs index 75cb466..18a13d3 100644 --- a/src/api/stow/routes.rs +++ b/src/api/stow/routes.rs @@ -3,7 +3,6 @@ use crate::backend::ServiceProvider; use crate::utils::multipart::DicomMultipart; use crate::AppState; use axum::body::Body; -use axum::extract::rejection::LengthLimitError; use axum::http::header::CONTENT_TYPE; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; @@ -12,8 +11,7 @@ use axum::Router; use bytes::Buf; use dicom::object::{FileDicomObject, InMemDicomObject}; use dicom_json::DicomJson; -use multer::Error; -use tracing::{error, instrument, warn}; +use tracing::instrument; /// HTTP Router for the Store Transaction /// @@ -27,64 +25,32 @@ pub fn routes() -> Router { async fn studies( provider: ServiceProvider, mut multipart: DicomMultipart<'static>, -) -> impl IntoResponse { +) -> Result { + let Some(stow) = provider.stow else { + return Ok(( + StatusCode::SERVICE_UNAVAILABLE, + "STOW-RS endpoint is disabled", + ) + .into_response()); + }; + let mut instances = Vec::new(); - while let Some(field) = multipart.next_field().await.unwrap_or_default() { - match field.bytes().await { - Ok(data) => { - // TODO: better error handling - let file = FileDicomObject::from_reader(data.reader()).unwrap(); - instances.push(file); - } - Err(err) => { - let err = if let Error::StreamReadFailed(stream_error) = &err { - let is_limit_exceeded = stream_error - .downcast_ref::() - .and_then(std::error::Error::source) - .and_then(|err| err.downcast_ref::()) - .is_some(); - if is_limit_exceeded { - warn!("Upload limit exceeded."); - StoreError::UploadLimitExceeded - } else { - error!("Failed to read multipart stream: {err:?}"); - StoreError::Stream(err) - } - } else { - error!("Failed to read multipart stream: {:?}", err); - StoreError::Stream(err) - }; - return (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(); - } - }; + while let Some(field) = multipart.next_field().await? { + let data = field.bytes().await?; + let file = FileDicomObject::from_reader(data.reader())?; + instances.push(file); } let request = StoreRequest { instances }; + let response = stow.store(request).await?; + let json = DicomJson::from(InMemDicomObject::from(response)); - if let Some(stow) = provider.stow { - #[allow(clippy::option_if_let_else)] - if let Ok(response) = stow.store(request).await { - let json = DicomJson::from(InMemDicomObject::from(response)); - - Response::builder() - .status(StatusCode::OK) - .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) - .body(Body::from(serde_json::to_string(&json).unwrap())) - .unwrap() - } else { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap() - } - } else { - ( - StatusCode::SERVICE_UNAVAILABLE, - "STOW-RS endpoint is disabled", - ) - .into_response() - } + Ok(Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) + .body(Body::from(serde_json::to_string(&json).unwrap())) + .unwrap()) } #[instrument(skip_all)] diff --git a/src/api/stow/service.rs b/src/api/stow/service.rs index 150dea9..d1638f4 100644 --- a/src/api/stow/service.rs +++ b/src/api/stow/service.rs @@ -1,5 +1,8 @@ use crate::types::UI; use async_trait::async_trait; +use axum::body::Body; +use axum::http::{Response, StatusCode}; +use axum::response::IntoResponse; use dicom::core::value::{DataSetSequence, Value}; use dicom::core::{DataElement, VR}; use dicom::dicom_value; @@ -91,5 +94,46 @@ pub enum StoreError { #[error("The file exceeds the configured upload size limit")] UploadLimitExceeded, #[error(transparent)] - Stream(#[from] multer::Error), + Stream(multer::Error), + #[error(transparent)] + ReadDicomFile(#[from] dicom::object::ReadError), +} + +impl IntoResponse for StoreError { + fn into_response(self) -> axum::response::Response { + match self { + Self::UploadLimitExceeded => Response::builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .body(Body::from("Upload limit exceeded")) + .unwrap(), + Self::Stream(err) => Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(format!( + "Failed to read multipart stream: {err:#}" + ))) + .unwrap(), + Self::ReadDicomFile(err) => Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(format!("Failed to read DICOM file: {err:#}"))) + .unwrap(), + } + } +} + +impl From for StoreError { + fn from(error: multer::Error) -> Self { + if let multer::Error::StreamReadFailed(stream_error) = &error { + let is_limit_exceeded = stream_error + .downcast_ref::() + .and_then(std::error::Error::source) + .and_then(|err| err.downcast_ref::()) + .is_some(); + + if is_limit_exceeded { + return Self::UploadLimitExceeded; + } + } + + Self::Stream(error) + } } diff --git a/src/api/wado/routes.rs b/src/api/wado/routes.rs index 938a3cb..b2a3479 100644 --- a/src/api/wado/routes.rs +++ b/src/api/wado/routes.rs @@ -85,7 +85,7 @@ async fn instance_resource( Response::builder() .header( CONTENT_DISPOSITION, - format!(r#"attachment; filename="{study_instance_uid}""#,), + format!(r#"attachment; filename="{study_instance_uid}""#), ) .header( CONTENT_TYPE, diff --git a/src/backend/dimse/association/pool.rs b/src/backend/dimse/association/pool.rs index 045873c..e11d40b 100644 --- a/src/backend/dimse/association/pool.rs +++ b/src/backend/dimse/association/pool.rs @@ -11,7 +11,7 @@ use futures::TryFutureExt; use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; use thiserror::Error; -use tokio::sync::Semaphore; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::time::Instant; use tracing::{info, warn}; @@ -45,7 +45,7 @@ impl Pool { inner: Arc::new(InnerPool { manager, slots: Mutex::new(VecDeque::new()), - semaphore: Semaphore::new(pool_size), + semaphore: Arc::new(Semaphore::new(pool_size)), timeout, }), } @@ -53,12 +53,10 @@ impl Pool { pub async fn get(&self, parameter: M::Parameter) -> Result, PoolError> { let timeout = tokio::time::timeout(self.inner.timeout, async { - self.inner - .semaphore - .acquire() + let permit = Arc::clone(&self.inner.semaphore) + .acquire_owned() .await - .expect("Semaphore should not be closed") - .forget(); + .expect("Semaphore should not be closed"); let slot: Option> = { let mut slots = self.inner.slots.lock().unwrap(); @@ -117,6 +115,7 @@ impl Pool { Ok(Object { pool: Arc::downgrade(&self.inner), inner: Some(object_inner), + permit, }) }); @@ -127,6 +126,8 @@ impl Pool { pub struct Object { pool: Weak>, inner: Option>, + #[allow(unused)] + permit: OwnedSemaphorePermit, } impl Deref for Object { @@ -140,7 +141,6 @@ impl Deref for Object { impl Drop for Object { fn drop(&mut self) { if let Some(pool) = self.pool.upgrade() { - pool.semaphore.add_permits(1); if let Some(object) = self.inner.take() { let mut slots = pool.slots.lock().unwrap(); slots.push_back(object); @@ -160,7 +160,7 @@ impl Clone for Pool { struct InnerPool { manager: M, slots: Mutex>>, - semaphore: Semaphore, + semaphore: Arc, timeout: Duration, } diff --git a/src/backend/dimse/cfind/findscu.rs b/src/backend/dimse/cfind/findscu.rs index 5f3ed51..70bb311 100644 --- a/src/backend/dimse/cfind/findscu.rs +++ b/src/backend/dimse/cfind/findscu.rs @@ -43,6 +43,7 @@ impl FindServiceClassUser { Self { pool, timeout } } + #[allow(clippy::significant_drop_tightening)] pub fn invoke( &self, options: FindServiceClassUserOptions, diff --git a/src/backend/dimse/cmove/movescu.rs b/src/backend/dimse/cmove/movescu.rs index 15b4ee4..c99f611 100644 --- a/src/backend/dimse/cmove/movescu.rs +++ b/src/backend/dimse/cmove/movescu.rs @@ -23,6 +23,7 @@ impl MoveServiceClassUser { } #[instrument(skip_all, name = "MOVE-SCU")] + #[allow(clippy::significant_drop_tightening)] pub async fn invoke(&self, request: CompositeMoveRequest) -> Result<(), MoveError> { let association = self .pool diff --git a/src/backend/dimse/cstore/storescp.rs b/src/backend/dimse/cstore/storescp.rs index 977d8cc..2a18395 100644 --- a/src/backend/dimse/cstore/storescp.rs +++ b/src/backend/dimse/cstore/storescp.rs @@ -44,7 +44,13 @@ impl StoreServiceClassProvider { pub async fn spawn(&self) -> anyhow::Result<()> { let address = SocketAddr::from((self.inner.config.interface, self.inner.config.port)); let listener = TcpListener::bind(&address).await?; - info!("Started Store Service Class Provider on {}", address); + let server_addr = listener.local_addr()?; + + info!( + server.address = server_addr.ip().to_string(), + server.port = server_addr.port(), + "Started Store Service Class Provider" + ); loop { match listener.accept().await { Ok((stream, peer)) => { diff --git a/src/backend/dimse/cstore/storescu.rs b/src/backend/dimse/cstore/storescu.rs index cdaf90a..88e09a6 100644 --- a/src/backend/dimse/cstore/storescu.rs +++ b/src/backend/dimse/cstore/storescu.rs @@ -21,6 +21,7 @@ impl StoreServiceClassUser { Self { pool, timeout } } + #[allow(clippy::significant_drop_tightening)] pub async fn store(&self, file: FileDicomObject) -> Result<(), StoreError> { let association = self .pool diff --git a/src/backend/dimse/wado.rs b/src/backend/dimse/wado.rs index 4cab123..dd96db2 100644 --- a/src/backend/dimse/wado.rs +++ b/src/backend/dimse/wado.rs @@ -271,7 +271,7 @@ impl<'a> DicomMultipartStream<'a> { ) -> Self { let transfer_syntax_uid = transfer_syntax_uid.and_then(|ts_uid| TransferSyntaxRegistry.get(ts_uid)); - + #[allow(clippy::result_large_err)] let multipart_stream = stream .map(move |item| { let transfer_syntax_uid = transfer_syntax_uid; diff --git a/src/main.rs b/src/main.rs index 5470cd8..6c23b68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use crate::config::{AppConfig, HttpServerConfig}; use crate::types::AE; use association::pool::AssociationPools; use axum::extract::{DefaultBodyLimit, Request}; +use axum::http::StatusCode; use axum::response::Response; use axum::ServiceExt; use std::net::SocketAddr; @@ -45,7 +46,6 @@ fn init_logger(level: tracing::Level) { .with( tracing_subscriber::fmt::layer() .compact() - .with_ansi(true) .with_file(false) .with_line_number(false) .with_target(false), @@ -144,9 +144,10 @@ async fn run(config: AppConfig) -> anyhow::Result<()> { .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), ) .layer(DefaultBodyLimit::max(config.server.http.max_upload_size)) - .layer(TimeoutLayer::new(Duration::from_secs( - config.server.http.request_timeout, - ))) + .layer(TimeoutLayer::with_status_code( + StatusCode::REQUEST_TIMEOUT, + Duration::from_secs(config.server.http.request_timeout), + )) .with_state(app_state); let app = NormalizePathLayer::trim_trailing_slash().layer(app); @@ -160,9 +161,11 @@ async fn run(config: AppConfig) -> anyhow::Result<()> { let addr = SocketAddr::from((host, port)); let listener = TcpListener::bind(addr).await?; + let server_addr = listener.local_addr()?; + info!( - server.address = addr.ip().to_string(), - server.port = addr.port(), + server.address = server_addr.ip().to_string(), + server.port = server_addr.port(), url.full = config.server.http.base_url()?.as_str(), "Started DICOMweb server" ); diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..f4fb392 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,119 @@ +use anyhow::{bail, Context}; +use dicom_web::DicomWebClient; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; +use testcontainers::core::{IntoContainerPort, WaitFor}; +use testcontainers::runners::AsyncRunner; +use testcontainers::{ContainerAsync, GenericImage}; +use tokio::io::{AsyncBufReadExt, BufReader, Lines}; +use tokio::process::{Child, ChildStdout, Command}; + +pub async fn spawn_orthanc() -> anyhow::Result> { + GenericImage::new("jodogne/orthanc", "latest") + .with_exposed_port(4242.tcp()) + .with_exposed_port(8042.tcp()) + .with_wait_for(WaitFor::message_on_stderr("Orthanc has started")) + .start() + .await + .context("failed to start Orthanc container") +} + +pub async fn spawn_dicomrst(config: &str) -> anyhow::Result { + let mut server = ServerProcess::spawn(config)?; + server.http_port = server.wait_until_started().await?; + Ok(server) +} + +pub struct ServerProcess { + child: Child, + stdout: Lines>, + workdir: PathBuf, + http_port: u16, +} + +impl ServerProcess { + fn spawn(config: &str) -> anyhow::Result { + let workdir = std::env::temp_dir().join(format!("dicom-rst-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&workdir)?; + std::fs::write(workdir.join("config.yaml"), config)?; + + let mut child = Command::new(env!("CARGO_BIN_EXE_dicom-rst")) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .env("NO_COLOR", "true") // disables colored ANSI output + .current_dir(&workdir) + .spawn() + .context("failed to spawn DICOM-RST server binary")?; + + let stdout = BufReader::new(child.stdout.take().unwrap()).lines(); + + Ok(Self { + child, + stdout, + workdir, + http_port: 0, + }) + } + + async fn wait_until_started(&mut self) -> anyhow::Result { + tokio::time::timeout(Duration::from_secs(15), async { + while let Some(line) = self + .stdout + .next_line() + .await + .context("Failed to read DICOM-RST stdout")? + { + if !line.contains("Started DICOMweb server") { + continue; + } + + let port = line + .split_whitespace() + .find_map(|part| part.strip_prefix("server.port=")) + .ok_or_else(|| { + anyhow::Error::msg( + "DICOM-RST started, but stdout did not contain server.port=", + ) + })? + .parse::() + .context("Failed to parse DICOM-RST server.port as u16")?; + + return Ok(port); + } + + bail!("DICOM-RST exited before becoming ready"); + }) + .await + .context("Timed out waiting for DICOM-RST to start")? + } +} + +impl Drop for ServerProcess { + fn drop(&mut self) { + self.child.start_kill().unwrap(); + std::fs::remove_dir_all(&self.workdir).unwrap(); + } +} + +pub async fn with_test_environment( + config: &str, + test: impl AsyncFnOnce(DicomWebClient) -> anyhow::Result<()>, +) -> anyhow::Result<()> { + let orthanc = spawn_orthanc().await?; + let orthanc_port = orthanc + .get_host_port_ipv4(4242.tcp()) + .await + .context("failed to get mapped Orthanc DIMSE port")?; + + let config = config.replace("${ORTHANC_PORT}", &orthanc_port.to_string()); + let server = spawn_dicomrst(&config).await?; + + let client = DicomWebClient::with_single_url(&format!( + "http://localhost:{}/aets/ORTHANC", + server.http_port + )); + test(client).await?; + + Ok(()) +} diff --git a/tests/stow.rs b/tests/stow.rs new file mode 100644 index 0000000..20a6f6b --- /dev/null +++ b/tests/stow.rs @@ -0,0 +1,173 @@ +mod common; + +use anyhow::Context; +use axum::http::StatusCode; +use common::*; +use dicom::dictionary_std::tags; +use dicom::object::open_file; +use dicom_web::DicomWebError; +use std::time::{Duration, Instant}; + +#[allow(clippy::redundant_closure_for_method_calls)] +#[tokio::test] +async fn can_upload_study_instances() -> anyhow::Result<()> { + let config = " + server: + http: + port: 0 + dimse: + - aet: DICOM-RST + interface: 0.0.0.0 + port: 0 + aets: + - aet: ORTHANC + host: 127.0.0.1 + port: ${ORTHANC_PORT} + backend: DIMSE + "; + + let instances = [ + "pydicom/liver.dcm", + "pydicom/CT_small.dcm", + "pydicom/MR_small.dcm", + ] + .map(|path| open_file(dicom_test_files::path(path).unwrap()).unwrap()); + let instances = futures::stream::iter(instances); + + with_test_environment(config, async |client| { + let response = client + .store_instances() + .with_instances(instances.clone()) + .run() + .await + .context("STOW-RS request failed")?; + + let failed_sequence = response + .element(tags::FAILED_SOP_SEQUENCE) + .context("STOW-RS response is missing FailedSOPSequence")?; + + let referenced_sop_sequence = response + .element(tags::REFERENCED_SOP_SEQUENCE) + .context("STOW-RS response is missing ReferencedSOPSequence")?; + + assert!( + failed_sequence + .items() + .is_some_and(|items| items.is_empty()), + "STOW-RS response contains FailedSOPSequence items" + ); + + assert!( + referenced_sop_sequence + .items() + .is_some_and(|items| items.len() == 3), + "STOW-RS response contains unexpected number of ReferencedSOPSequence items" + ); + + Ok(()) + }) + .await?; + + Ok(()) +} + +// https://github.com/UMEssen/DICOM-RST/issues/56 +#[tokio::test] +async fn does_not_leak_semaphore_permits_if_association_is_rejected() -> anyhow::Result<()> { + let config = " + server: + http: + port: 0 + dimse: + - aet: DICOM-RST + interface: 0.0.0.0 + port: 0 + aets: + - aet: ORTHANC + host: 127.0.0.1 + port: ${ORTHANC_PORT} + backend: DIMSE + pool: + size: 1 + timeout: 5000 + stow-rs: + timeout: 5000 + "; + + let create_instance = || { + let mut instance = + open_file(dicom_test_files::path("pydicom/CT_small.dcm").unwrap()).unwrap(); + // Set a fake SOPClassUID; Orthanc will reject this instance + instance.meta_mut().media_storage_sop_class_uid = String::from("1.2.3.4.5.6.7.8.9.10"); + instance + }; + let instances = [create_instance(), create_instance(), create_instance()]; + + with_test_environment(config, async |client| { + let start = Instant::now(); + let response = client + .store_instances() + .with_instances(futures::stream::iter(instances)) + .run() + .await + .context("STOW-RS request failed")?; + + let elapsed = start.elapsed(); + assert!( + elapsed < Duration::from_secs(4), + "STOW-RS took {elapsed:?}; expected fast failure" + ); + + let failed_sequence = response + .element(tags::FAILED_SOP_SEQUENCE) + .context("STOW-RS response is missing FailedSOPSequence")?; + assert!( + failed_sequence + .items() + .is_some_and(|items| items.len() == 3), + "All three rejected instances should appear in FailedSOPSequence" + ); + + Ok(()) + }) + .await +} + +// https://github.com/UMEssen/DICOM-RST/issues/55 +#[tokio::test] +async fn returns_413_if_max_upload_size_is_exceeded() -> anyhow::Result<()> { + let config = " + server: + http: + port: 0 + max-upload-size: 1 + dimse: + - aet: DICOM-RST + interface: 0.0.0.0 + port: 0 + aets: + - aet: ORTHANC + host: 127.0.0.1 + port: ${ORTHANC_PORT} + backend: DIMSE + "; + + with_test_environment(config, async |client| { + let instance = open_file(dicom_test_files::path("pydicom/CT_small.dcm").unwrap()).unwrap(); + let result = client + .store_instances() + .with_instances(futures::stream::iter([instance])) + .run() + .await; + + assert!(matches!( + result, + Err(DicomWebError::HttpStatusFailure { + status_code: StatusCode::PAYLOAD_TOO_LARGE + }) + )); + + Ok(()) + }) + .await +}