diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..32bda52 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + fmt: + name: rustfmt + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + name: clippy + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: docker/setup-buildx-action@v4 + - uses: docker/build-push-action@v7 + with: + context: . + target: tester + load: true + tags: docoxide-tester:ci + cache-from: type=gha + cache-to: type=gha,mode=max + - run: docker run --rm docoxide-tester:ci cargo clippy --all-targets -- -D warnings + + test: + name: integration tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: docker/setup-buildx-action@v4 + - uses: docker/build-push-action@v7 + with: + context: . + target: tester + load: true + tags: docoxide-tester:ci + cache-from: type=gha + cache-to: type=gha,mode=max + - run: docker run --rm --cap-add=SYS_NICE docoxide-tester:ci diff --git a/Cargo.lock b/Cargo.lock index 72ccc8b..4e2a6f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,12 +32,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -49,9 +43,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -64,15 +58,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -103,18 +97,52 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bindgen" version = "0.63.0" @@ -131,7 +159,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 1.0.109", "which", @@ -145,9 +173,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "brotli" @@ -192,12 +220,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" -version = "1.2.29" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -227,19 +264,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -261,9 +303,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -271,9 +313,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -283,21 +325,30 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] [[package]] name = "colorchoice" @@ -305,6 +356,36 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -347,7 +428,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -359,22 +440,46 @@ dependencies = [ "infer", "libreoffice-rs", "mime_guess", + "reqwest", "rouille", "sanitize-filename", "serde", "serde_json", "tempfile", "thiserror", + "tokio", + "tokio-test", "tracing", "tracing-subscriber", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.13" @@ -403,6 +508,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fnv" version = "1.0.7" @@ -418,6 +529,51 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -425,8 +581,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -436,9 +594,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -456,6 +616,31 @@ dependencies = [ "crc32fast", ] +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + [[package]] name = "heck" version = "0.5.0" @@ -477,6 +662,40 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -489,6 +708,68 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -620,6 +901,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "infer" version = "0.19.0" @@ -629,6 +920,12 @@ dependencies = [ "cfb", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -641,6 +938,65 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[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", + "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 2.0.117", +] + +[[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 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -665,9 +1021,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -685,7 +1041,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "libc", "redox_syscall", ] @@ -708,9 +1064,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -724,13 +1080,19 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[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.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -761,6 +1123,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + [[package]] name = "multipart" version = "0.18.0" @@ -773,7 +1146,7 @@ dependencies = [ "mime", "mime_guess", "quick-error", - "rand", + "rand 0.8.5", "safemem", "tempfile", "twoway", @@ -791,12 +1164,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.60.2", ] [[package]] @@ -846,10 +1218,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] -name = "overload" -version = "0.1.1" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "peeking_take_while" @@ -869,6 +1241,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "potential_utf" version = "0.1.2" @@ -895,9 +1273,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -908,11 +1286,67 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[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 2.1.2", + "rustls", + "socket2 0.5.10", + "thiserror", + "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.3", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "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.5.10", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -930,8 +1364,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -941,25 +1385,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] -name = "rand_core" -version = "0.6.4" +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.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "redox_syscall" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", ] [[package]] @@ -970,17 +1433,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -991,20 +1445,70 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "reqwest" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "rouille" @@ -1012,14 +1516,14 @@ version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921" dependencies = [ - "base64", + "base64 0.13.1", "brotli", "chrono", "deflate", "filetime", "multipart", "percent-encoding", - "rand", + "rand 0.8.5", "serde", "serde_derive", "serde_json", @@ -1036,13 +1540,28 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1051,28 +1570,97 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.60.2", ] [[package]] -name = "rustversion" -version = "1.0.21" +name = "rustls" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +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", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.60.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 = "ryu" -version = "1.0.20" +name = "rustls-webpki" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "safemem" @@ -1080,6 +1668,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +[[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 = "sanitize-filename" version = "0.6.0" @@ -1089,36 +1686,85 @@ dependencies = [ "regex", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1142,12 +1788,54 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[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 = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1160,6 +1848,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1173,15 +1867,24 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1190,40 +1893,61 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.1.4", + "windows-sys 0.60.2", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -1287,11 +2011,142 @@ 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.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1300,20 +2155,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1332,14 +2187,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -1348,6 +2203,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "twoway" version = "0.1.8" @@ -1369,6 +2230,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -1408,6 +2275,25 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1445,10 +2331,23 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -1467,7 +2366,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1481,6 +2380,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +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", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -1494,27 +2422,14 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.3.9" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows-sys 0.60.2", ] -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.61.2" @@ -1523,7 +2438,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -1536,7 +2451,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -1547,7 +2462,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -1556,13 +2471,30 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -1571,7 +2503,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1592,6 +2533,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1726,7 +2676,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.1", ] [[package]] @@ -1755,7 +2705,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "synstructure", ] @@ -1776,7 +2726,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] [[package]] @@ -1796,10 +2746,16 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" @@ -1830,5 +2786,11 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 7244c83..b0bd447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,24 @@ [package] name = "docoxide" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] -chrono = { version = "0.4.41", features = ["serde"] } -clap = { version = "4.5.40", features = ["derive"] } +chrono = { version = "0.4.44", features = ["serde"] } +clap = { version = "4.6.1", features = ["derive"] } infer = "0.19.0" libreoffice-rs = "0.3.3" mime_guess = "2.0.5" rouille = "3.6.2" sanitize-filename = "0.6.0" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" -tempfile = "3.20.0" -thiserror = "2.0.12" -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +tempfile = "3.27.0" +thiserror = "2.0.18" +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } + +[dev-dependencies] +reqwest = { version = "0.13.3", features = ["json", "multipart"] } +tokio = { version = "1.52.2", features = ["macros", "rt-multi-thread"] } +tokio-test = "0.4.5" diff --git a/Dockerfile b/Dockerfile index fa35b75..adb003f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ### Builder Stage ### -FROM rust:1.88.0-slim-bookworm AS builder +FROM rust:1.95.0-slim-trixie AS builder WORKDIR /usr/src/app @@ -16,8 +16,27 @@ COPY src ./src RUN cargo build --release +### Test Stage ### +FROM builder AS tester + +RUN apt-get update && apt-get install -y \ + libreoffice-writer \ + libreoffice-calc \ + fonts-dejavu \ + fonts-liberation \ + fontconfig \ + --no-install-recommends && \ + fc-cache -f && \ + rm -rf /var/lib/apt/lists/* + +RUN rustup component add clippy rustfmt + +COPY tests ./tests + +CMD ["bash", "-c", "cargo test --test integration_tests -- --nocapture 2>&1 | tee /tmp/test.log; grep -qE '^test result: ok\\..*0 failed' /tmp/test.log"] + ### Runtime Stage ### -FROM debian:bookworm-slim AS runtime +FROM debian:trixie-slim AS runtime RUN apt-get update && apt-get install -y \ libreoffice \ @@ -25,6 +44,8 @@ RUN apt-get update && apt-get install -y \ fonts-liberation \ fonts-noto \ fonts-noto-cjk \ + fonts-crosextra-carlito \ + fonts-crosextra-caladea \ fontconfig \ --no-install-recommends && \ fc-cache -f && \ diff --git a/README.md b/README.md index 9abf3f4..d5a3261 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ cargo run -- --office-path /path/to/libreoffice --port 8080 ## Requirements - LibreOffice installed -- Rust 1.70+ +- Rust 1.85+ diff --git a/src/errors.rs b/src/errors.rs index 680de94..4b66dcf 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -44,9 +44,7 @@ impl ConversionError { "status": status_code }); - Response::json(&error_response) - .with_status_code(status_code) - .with_additional_header("Content-Type", "application/json") + Response::json(&error_response).with_status_code(status_code) } } diff --git a/src/http_server.rs b/src/http_server.rs index 133161f..504c268 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,13 +1,13 @@ -use rouille::{post_input, try_or_400, Response}; +use rouille::{Response, post_input, try_or_400}; use std::io::Write; use std::net::IpAddr; use std::path::Path; -use std::sync::{mpsc, Arc}; +use std::sync::{Arc, mpsc}; use std::thread; use std::time::{Duration, Instant}; use tempfile::NamedTempFile; -use crate::errors::{log_error, ConversionError}; +use crate::errors::{ConversionError, log_error}; use crate::metrics::{HealthChecker, MetricsCollector}; use crate::office::OfficeManager; use crate::validation::FileValidator; @@ -243,17 +243,24 @@ fn process_conversion_request( .as_mut() .ok_or_else(|| ConversionError::Internal("LibreOffice not initialized".to_string()))?; - let mut tmp_input_file = NamedTempFile::new().map_err(|e| { + // LibreOffice autodetects the input format from the file extension; without one, document_load hangs. + let input_suffix = Path::new(&request.filename) + .extension() + .and_then(|e| e.to_str()) + .map(|e| format!(".{}", e)) + .unwrap_or_default(); + + let mut tmp_input_file = NamedTempFile::with_suffix(&input_suffix).map_err(|e| { ConversionError::TempFile(format!("Failed to create input temp file: {}", e)) })?; - let tmp_output_file = NamedTempFile::new().map_err(|e| { + let tmp_output_file = NamedTempFile::with_suffix(".pdf").map_err(|e| { ConversionError::TempFile(format!("Failed to create output temp file: {}", e)) })?; tmp_input_file .write_all(&request.file_content) - .map_err(|e| ConversionError::Io(e))?; + .map_err(ConversionError::Io)?; let input_path = tmp_input_file .path() @@ -276,8 +283,13 @@ fn process_conversion_request( } // Read and return the converted file - let output_content = - std::fs::read(tmp_output_file.path()).map_err(|e| ConversionError::Io(e))?; + let output_content = std::fs::read(tmp_output_file.path()).map_err(ConversionError::Io)?; + + if output_content.is_empty() { + return Err(ConversionError::Internal( + "Conversion produced empty output".to_string(), + )); + } Ok(output_content) } @@ -285,8 +297,13 @@ fn process_conversion_request( fn get_client_ip(request: &rouille::Request) -> IpAddr { request .header("X-Forwarded-For") - .and_then(|ip| ip.parse().ok()) - .or_else(|| request.header("X-Real-IP").and_then(|ip| ip.parse().ok())) + .and_then(|val| val.split(',').next()) + .and_then(|ip| ip.trim().parse().ok()) + .or_else(|| { + request + .header("X-Real-IP") + .and_then(|ip| ip.trim().parse().ok()) + }) .unwrap_or_else(|| request.remote_addr().ip()) } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bba0d9c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod errors; +pub mod http_server; +pub mod metrics; +pub mod office; +pub mod validation; diff --git a/src/main.rs b/src/main.rs index 615e80f..adcaacd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,5 @@ use clap::Parser; - -mod errors; -mod http_server; -mod metrics; -mod office; -mod validation; +use docoxide::http_server; #[derive(Parser, Debug)] #[command(author, version, about = "Document conversion service")] @@ -20,7 +15,7 @@ struct Args { fn main() { let args = Args::parse(); - let _stdout_subscriber = tracing_subscriber::fmt::init(); + tracing_subscriber::fmt::init(); http_server::serve(args.office_path, args.port); } diff --git a/src/metrics.rs b/src/metrics.rs index b0914ef..f38b094 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -102,7 +102,6 @@ impl MetricsCollector { 0.0 } } - } impl Default for MetricsCollector { diff --git a/src/office.rs b/src/office.rs index 70995b4..2f0cfd6 100644 --- a/src/office.rs +++ b/src/office.rs @@ -1,5 +1,5 @@ use crate::errors::{ConversionError, Result}; -use libreoffice_rs::{urls, Office as OfficeKit}; +use libreoffice_rs::{Office as OfficeKit, urls}; pub struct OfficeManager { office: OfficeKit, @@ -21,12 +21,16 @@ impl OfficeManager { ConversionError::LibreOffice(format!("Failed to convert path to URL: {}", e)) })?; + let output_url = urls::local_into_abs(output_path).map_err(|e| { + ConversionError::LibreOffice(format!("Failed to convert output path to URL: {}", e)) + })?; + let mut doc = self .office .document_load(doc_url) .map_err(|e| ConversionError::LibreOffice(format!("Failed to load document: {}", e)))?; - if !doc.save_as(output_path, "pdf", None) { + if !doc.save_as(&output_url.to_string(), "pdf", None) { return Err(ConversionError::LibreOffice( "Failed to save PDF file".to_string(), )); @@ -50,6 +54,10 @@ impl OfficeManager { ConversionError::LibreOffice(format!("Failed to convert path to URL: {}", e)) })?; + let output_url = urls::local_into_abs(output_path).map_err(|e| { + ConversionError::LibreOffice(format!("Failed to convert output path to URL: {}", e)) + })?; + let mut doc = self .office .document_load(doc_url) @@ -59,7 +67,7 @@ impl OfficeManager { let pdf_options = "SelectPdfVersion=1;UseTaggedPDF=true;ExportNotes=true;ExportNotesPages=false"; - if !doc.save_as(output_path, "pdf", Some(pdf_options)) { + if !doc.save_as(&output_url.to_string(), "pdf", Some(pdf_options)) { return Err(ConversionError::LibreOffice( "Failed to save PDF/A file".to_string(), )); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b841337 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,89 @@ +# DocOxide Test Suite + +Integration tests for the DocOxide document conversion service. + +## Test Structure + +### Integration Tests (`integration_tests.rs`) + +The integration tests boot the HTTP server in-process (via a thread, sharing a single `OnceLock`-guarded instance across all tests) and exercise it through `reqwest`. + +#### Health & Monitoring +- **Health Endpoint**: `/health` returns proper health status +- **Metrics Endpoint**: `/metrics` returns conversion statistics +- **Health Status Changes**: status calculation based on success rates +- **Metrics Accuracy**: metrics increment correctly across requests + +#### Document Conversion +- **DOCX Conversion**: Word → PDF +- **XLSX Conversion**: Excel → PDF +- **PDF/A Conversion**: PDF/A output format +- **Multiple File Formats**: covers the supported Office formats + +#### Security & Validation +- **Empty File Rejection**: empty payloads are rejected +- **Invalid File Type Rejection**: plain-text and other unsupported types are rejected +- **Content Type Mismatch**: extension/content mismatch is rejected +- **Large File Rejection**: payloads above the validator's 100MB limit are rejected (the test allocates the oversized buffer in-memory; no fixture is checked in) +- **Filename Sanitization**: path-traversal characters are stripped + +#### Reliability +- **Concurrent Conversions**: multiple simultaneous requests +- **Conversion Timeout Handling**: client-side timeout behavior +- **Invalid Endpoints**: 404 for unknown URLs + +## Test Data + +`fixtures/` contains: + +- **test_document.docx** — valid Word document +- **test_spreadsheet.xlsx** — valid Excel spreadsheet +- **test_document.txt** — plain-text payload (for unsupported-type rejection) +- **fake_document.docx** — `.docx` extension with non-Office content +- **empty_file.docx** — zero-byte file + +The oversized payload used by `test_large_file_rejection` is generated at runtime; it is intentionally not checked in. + +## Running Tests + +### Prerequisites + +Tests require LibreOffice (both the LibreOfficeKit headers for compilation and the LibreOffice runtime at `/usr/lib/libreoffice/program`). Easiest path is Docker. + +### Docker (recommended) + +```bash +docker build -f Dockerfile.test -t docoxide-test . +docker run --rm docoxide-test +``` + +### Local + +If you have LibreOfficeKit dev headers and LibreOffice installed at `/usr/lib/libreoffice/program`: + +```bash +LO_INCLUDE_PATH=/path/to/LibreOfficeKit cargo test --test integration_tests +``` + +Specific tests: +```bash +cargo test --test integration_tests test_health +cargo test --test integration_tests test_successful +``` + +## Test Configuration + +- **Server port**: 8765 (constant in `integration_tests.rs`) +- **Server boot wait**: up to 30 × 500ms while polling `/health` +- **LibreOffice path**: `/usr/lib/libreoffice/program` (matches the Docker image) + +## Expected Behavior + +### Successful Conversions +- 200 status, `Content-Type: application/pdf` +- Body starts with `%PDF` +- `Content-Disposition` carries the sanitized filename + +### Validation Failures +- 400 status with a JSON body of the form `{"error": "...", "status": 400}` +- Specific messages: `"Empty file"`, `"File too large: ..."`, `"Unknown or unsupported file type"`, `"File content does not match extension"` diff --git a/tests/fixtures/empty_file.docx b/tests/fixtures/empty_file.docx new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/fake_document.docx b/tests/fixtures/fake_document.docx new file mode 100644 index 0000000..18c7730 --- /dev/null +++ b/tests/fixtures/fake_document.docx @@ -0,0 +1 @@ +This is not a real docx file diff --git a/tests/fixtures/test_document.docx b/tests/fixtures/test_document.docx new file mode 100644 index 0000000..477bbe5 Binary files /dev/null and b/tests/fixtures/test_document.docx differ diff --git a/tests/fixtures/test_document.txt b/tests/fixtures/test_document.txt new file mode 100644 index 0000000..fa83fe5 --- /dev/null +++ b/tests/fixtures/test_document.txt @@ -0,0 +1 @@ +Hello World Test Document diff --git a/tests/fixtures/test_spreadsheet.xlsx b/tests/fixtures/test_spreadsheet.xlsx new file mode 100644 index 0000000..a2ada09 Binary files /dev/null and b/tests/fixtures/test_spreadsheet.xlsx differ diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..e375b17 --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,683 @@ +use docoxide::http_server; +use reqwest::multipart; +use std::fs; +use std::path::Path; +use std::sync::OnceLock; +use std::thread; +use std::time::Duration; + +const TEST_SERVER_PORT: u16 = 8765; +const TEST_SERVER_URL: &str = "http://localhost:8765"; + +static SERVER_STARTED: OnceLock<()> = OnceLock::new(); + +fn ensure_server_running() { + SERVER_STARTED.get_or_init(|| { + thread::spawn(|| { + http_server::serve("/usr/lib/libreoffice/program".to_string(), TEST_SERVER_PORT); + }); + }); +} + +async fn wait_for_server_ready() { + let client = reqwest::Client::new(); + let mut attempts = 0; + + while attempts < 30 { + if let Ok(response) = client + .get(format!("{}/health", TEST_SERVER_URL)) + .send() + .await + && response.status().is_success() + { + return; + } + tokio::time::sleep(Duration::from_millis(500)).await; + attempts += 1; + } + + panic!("Server failed to start within timeout"); +} + +fn get_test_file_path(filename: &str) -> String { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(filename) + .to_string_lossy() + .to_string() +} + +#[tokio::test] +async fn test_health_endpoint() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let response = client + .get(format!("{}/health", TEST_SERVER_URL)) + .send() + .await + .expect("Failed to make health request"); + + assert_eq!(response.status(), 200); + + let health_data: serde_json::Value = response + .json() + .await + .expect("Failed to parse health response"); + + assert!(health_data["status"].is_string()); + assert!(health_data["version"].is_string()); + assert!(health_data["uptime_seconds"].is_number()); + assert!(health_data["metrics"].is_object()); +} + +#[tokio::test] +async fn test_metrics_endpoint() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let response = client + .get(format!("{}/metrics", TEST_SERVER_URL)) + .send() + .await + .expect("Failed to make metrics request"); + + assert_eq!(response.status(), 200); + + let metrics_data: serde_json::Value = response + .json() + .await + .expect("Failed to parse metrics response"); + + assert!(metrics_data["total_conversions"].is_number()); + assert!(metrics_data["successful_conversions"].is_number()); + assert!(metrics_data["failed_conversions"].is_number()); + assert!(metrics_data["total_bytes_processed"].is_number()); +} + +#[tokio::test] +async fn test_successful_docx_conversion() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let file_path = get_test_file_path("test_document.docx"); + let file_content = fs::read(&file_path).expect("Failed to read test document"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("test_document.docx"), + ) + .text("file_name", "test_document.docx"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 200); + assert_eq!( + response.headers().get("content-type").unwrap(), + "application/pdf" + ); + + let pdf_content = response.bytes().await.expect("Failed to read PDF response"); + assert!(!pdf_content.is_empty()); + + // Verify PDF header + assert_eq!(&pdf_content[0..4], b"%PDF"); +} + +#[tokio::test] +async fn test_successful_xlsx_conversion() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let file_path = get_test_file_path("test_spreadsheet.xlsx"); + let file_content = fs::read(&file_path).expect("Failed to read test spreadsheet"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("test_spreadsheet.xlsx"), + ) + .text("file_name", "test_spreadsheet.xlsx"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 200); + assert_eq!( + response.headers().get("content-type").unwrap(), + "application/pdf" + ); + + let pdf_content = response.bytes().await.expect("Failed to read PDF response"); + assert!(!pdf_content.is_empty()); + + // Verify PDF header + assert_eq!(&pdf_content[0..4], b"%PDF"); +} + +#[tokio::test] +async fn test_pdfa_conversion() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let file_path = get_test_file_path("test_document.docx"); + let file_content = fs::read(&file_path).expect("Failed to read test document"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("test_document.docx"), + ) + .text("file_name", "test_document.docx"); + + let response = client + .post(format!("{}/convert2/pdfa", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make PDF/A conversion request"); + + assert_eq!(response.status(), 200); + assert_eq!( + response.headers().get("content-type").unwrap(), + "application/pdf" + ); + + let pdf_content = response.bytes().await.expect("Failed to read PDF response"); + assert!(!pdf_content.is_empty()); + + // Verify PDF header + assert_eq!(&pdf_content[0..4], b"%PDF"); +} + +#[tokio::test] +async fn test_empty_file_rejection() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let file_path = get_test_file_path("empty_file.docx"); + let file_content = fs::read(&file_path).expect("Failed to read empty file"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("empty_file.docx"), + ) + .text("file_name", "empty_file.docx"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 400); + + let error_response: serde_json::Value = response + .json() + .await + .expect("Failed to parse error response"); + + assert!( + error_response["error"] + .as_str() + .unwrap() + .contains("Empty file") + ); +} + +#[tokio::test] +async fn test_invalid_file_type_rejection() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let file_path = get_test_file_path("test_document.txt"); + let file_content = fs::read(&file_path).expect("Failed to read text file"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("test_document.txt"), + ) + .text("file_name", "test_document.txt"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 400); + + let error_response: serde_json::Value = response + .json() + .await + .expect("Failed to parse error response"); + + // Plain-text payloads fail content-type detection before the extension check. + assert!( + error_response["error"] + .as_str() + .unwrap() + .contains("Unknown or unsupported file type") + ); +} + +#[tokio::test] +async fn test_content_type_mismatch_rejection() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + // Valid OOXML bytes (xlsx) under a .docx filename — detect_content_type passes, + // then is_compatible_type rejects on the extension/content mismatch. + let file_path = get_test_file_path("test_spreadsheet.xlsx"); + let file_content = fs::read(&file_path).expect("Failed to read spreadsheet"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("fake_document.docx"), + ) + .text("file_name", "fake_document.docx"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 400); + + let error_response: serde_json::Value = response + .json() + .await + .expect("Failed to parse error response"); + + assert!( + error_response["error"] + .as_str() + .unwrap() + .contains("File content does not match extension") + ); +} + +#[tokio::test] +async fn test_large_file_rejection() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + // Generated in-memory rather than checked in as a fixture; size exceeds the 100MB validator limit. + let file_content = vec![0u8; 101 * 1024 * 1024]; + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("large_file.docx"), + ) + .text("file_name", "large_file.docx"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 400); + + let error_response: serde_json::Value = response + .json() + .await + .expect("Failed to parse error response"); + + assert!( + error_response["error"] + .as_str() + .unwrap() + .contains("File too large") + ); +} + +#[tokio::test] +async fn test_invalid_endpoint() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let response = client + .get(format!("{}/invalid-endpoint", TEST_SERVER_URL)) + .send() + .await + .expect("Failed to make request"); + + assert_eq!(response.status(), 404); +} + +#[tokio::test] +async fn test_concurrent_conversions() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let file_path = get_test_file_path("test_document.docx"); + let file_content = fs::read(&file_path).expect("Failed to read test document"); + + // Test multiple concurrent conversions + let mut tasks = Vec::new(); + for i in 0..5 { + let client = client.clone(); + let file_content = file_content.clone(); + + let task = tokio::spawn(async move { + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("test_document.docx"), + ) + .text("file_name", format!("test_document_{}.docx", i)); + + client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + }); + + tasks.push(task); + } + + let mut success_count = 0; + for task in tasks { + if let Ok(response) = task.await.expect("Task failed") + && response.status().is_success() + { + success_count += 1; + let pdf_content = response.bytes().await.expect("Failed to read PDF response"); + assert!(!pdf_content.is_empty()); + assert_eq!(&pdf_content[0..4], b"%PDF"); + } + } + + assert!( + success_count > 0, + "Expected at least some concurrent conversions to succeed" + ); +} + +#[tokio::test] +async fn test_filename_sanitization() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let file_path = get_test_file_path("test_document.docx"); + let file_content = fs::read(&file_path).expect("Failed to read test document"); + + // Test with dangerous filename + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("../../../etc/passwd.docx"), + ) + .text("file_name", "../../../etc/passwd.docx"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 200); + + // sanitize_filename strips path separators but preserves basename text, so verify only that traversal was neutralized. + let content_disposition = response.headers().get("content-disposition"); + if let Some(header_value) = content_disposition { + let header_str = header_value.to_str().unwrap(); + assert!(!header_str.contains("../")); + assert!(!header_str.contains('/')); + } +} + +#[tokio::test] +async fn test_multiple_file_formats() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let test_files = vec!["test_document.docx", "test_spreadsheet.xlsx"]; + + for filename in test_files { + let file_path = get_test_file_path(filename); + let file_content = + fs::read(&file_path).unwrap_or_else(|_| panic!("Failed to read {}", filename)); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name(filename), + ) + .text("file_name", filename); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(response.status(), 200, "Failed to convert {}", filename); + assert_eq!( + response.headers().get("content-type").unwrap(), + "application/pdf" + ); + + let pdf_content = response.bytes().await.expect("Failed to read PDF response"); + assert!(!pdf_content.is_empty(), "Empty PDF for {}", filename); + assert_eq!( + &pdf_content[0..4], + b"%PDF", + "Invalid PDF header for {}", + filename + ); + } +} + +#[tokio::test] +async fn test_conversion_timeout_handling() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) // Short timeout to test timeout handling + .build() + .expect("Failed to create client"); + + let file_path = get_test_file_path("test_document.docx"); + let file_content = fs::read(&file_path).expect("Failed to read test document"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("test_document.docx"), + ) + .text("file_name", "test_document.docx"); + + let response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await; + + // Either succeeds within timeout or times out - both are valid outcomes + match response { + Ok(resp) => { + assert!(resp.status().is_success() || resp.status().is_server_error()); + } + Err(e) => { + // Timeout is acceptable + assert!(e.is_timeout()); + } + } +} + +#[tokio::test] +async fn test_metrics_accuracy() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + // Get initial metrics + let initial_response = client + .get(format!("{}/metrics", TEST_SERVER_URL)) + .send() + .await + .expect("Failed to get initial metrics"); + + let initial_metrics: serde_json::Value = initial_response + .json() + .await + .expect("Failed to parse initial metrics"); + + let initial_total = initial_metrics["total_conversions"].as_u64().unwrap(); + let initial_successful = initial_metrics["successful_conversions"].as_u64().unwrap(); + + // Perform a successful conversion + let file_path = get_test_file_path("test_document.docx"); + let file_content = fs::read(&file_path).expect("Failed to read test document"); + + let form = multipart::Form::new() + .part( + "file_content", + multipart::Part::bytes(file_content) + .mime_str("application/octet-stream") + .unwrap() + .file_name("test_document.docx"), + ) + .text("file_name", "test_document.docx"); + + let conversion_response = client + .post(format!("{}/convert2/pdf", TEST_SERVER_URL)) + .multipart(form) + .send() + .await + .expect("Failed to make conversion request"); + + assert_eq!(conversion_response.status(), 200); + + // Get updated metrics + let updated_response = client + .get(format!("{}/metrics", TEST_SERVER_URL)) + .send() + .await + .expect("Failed to get updated metrics"); + + let updated_metrics: serde_json::Value = updated_response + .json() + .await + .expect("Failed to parse updated metrics"); + + let updated_total = updated_metrics["total_conversions"].as_u64().unwrap(); + let updated_successful = updated_metrics["successful_conversions"].as_u64().unwrap(); + + // Verify metrics increased + assert!( + updated_total > initial_total, + "Total conversions should have increased" + ); + assert!( + updated_successful > initial_successful, + "Successful conversions should have increased" + ); +} + +#[tokio::test] +async fn test_health_status_changes() { + ensure_server_running(); + wait_for_server_ready().await; + let client = reqwest::Client::new(); + + let health_response = client + .get(format!("{}/health", TEST_SERVER_URL)) + .send() + .await + .expect("Failed to get health status"); + + assert_eq!(health_response.status(), 200); + + let health_data: serde_json::Value = health_response + .json() + .await + .expect("Failed to parse health response"); + + // Health status should be one of the expected values + let status = health_data["status"].as_str().unwrap(); + assert!( + status == "healthy" || status == "degraded" || status == "unhealthy", + "Unexpected health status: {}", + status + ); + + // Verify health data structure + assert!(health_data["timestamp"].is_string()); + assert!(health_data["version"].is_string()); + assert!(health_data["uptime_seconds"].is_number()); + assert!(health_data["metrics"]["total_conversions"].is_number()); + assert!(health_data["metrics"]["successful_conversions"].is_number()); + assert!(health_data["metrics"]["failed_conversions"].is_number()); +}