diff --git a/.github/actions/build-kobo/action.yml b/.github/actions/build-kobo/action.yml index b18fb716..af56c50b 100644 --- a/.github/actions/build-kobo/action.yml +++ b/.github/actions/build-kobo/action.yml @@ -23,6 +23,8 @@ inputs: runs: using: composite steps: + - uses: ./.github/actions/clone-needed-cargo-submodules + - name: Cache Linaro toolchain id: cache-linaro uses: actions/cache@v5 diff --git a/.github/actions/clone-needed-cargo-submodules/action.yml b/.github/actions/clone-needed-cargo-submodules/action.yml new file mode 100644 index 00000000..a8f78134 --- /dev/null +++ b/.github/actions/clone-needed-cargo-submodules/action.yml @@ -0,0 +1,9 @@ +name: Clone needed Cargo submodules +description: Clone only submodules needed for Cargo manifest resolution. + +runs: + using: composite + steps: + - name: Clone html5ever submodule + shell: bash + run: git submodule update --init --depth 1 thirdparty/html5ever diff --git a/.github/actions/install-doc-tools/action.yml b/.github/actions/install-doc-tools/action.yml index 786f3801..441ed818 100644 --- a/.github/actions/install-doc-tools/action.yml +++ b/.github/actions/install-doc-tools/action.yml @@ -13,6 +13,8 @@ inputs: runs: using: composite steps: + - uses: ./.github/actions/clone-needed-cargo-submodules + - name: Setup Node.js uses: actions/setup-node@v6 with: diff --git a/.github/workflows/cadmus-docs.yml b/.github/workflows/cadmus-docs.yml index 69c2c6da..618fc48c 100644 --- a/.github/workflows/cadmus-docs.yml +++ b/.github/workflows/cadmus-docs.yml @@ -14,6 +14,7 @@ on: - docs-portal/** - crates/**/*.rs - .github/actions/install-doc-tools/** + - .github/actions/clone-needed-cargo-submodules/** - .github/workflows/cadmus-docs.yml - package.json - package-lock.json @@ -24,6 +25,7 @@ on: - docs-portal/** - crates/**/*.rs - .github/actions/install-doc-tools/** + - .github/actions/clone-needed-cargo-submodules/** - .github/workflows/cadmus-docs.yml - package.json - package-lock.json diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index f8939ee2..40eb94dd 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -22,6 +22,7 @@ on: - ".github/actions/build-kobo/**" - ".github/actions/build-docs-epub/**" - ".github/actions/cargo-cache/**" + - ".github/actions/clone-needed-cargo-submodules/**" - ".github/actions/install-doc-tools/**" - "build-scripts/**" - ".gitmodules" @@ -39,6 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + - uses: ./.github/actions/clone-needed-cargo-submodules - uses: dtolnay/rust-toolchain@stable id: rust-toolchain with: @@ -59,6 +61,7 @@ jobs: test-matrix: ${{ steps.matrix.outputs.test-matrix }} steps: - uses: actions/checkout@v7 + - uses: ./.github/actions/clone-needed-cargo-submodules - uses: dtolnay/rust-toolchain@stable id: rust-toolchain - name: Restore shared cargo cache @@ -93,6 +96,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + - uses: ./.github/actions/clone-needed-cargo-submodules - uses: dtolnay/rust-toolchain@stable id: rust-toolchain with: @@ -183,6 +187,7 @@ jobs: steps: - uses: actions/checkout@v7 + - uses: ./.github/actions/clone-needed-cargo-submodules - uses: dtolnay/rust-toolchain@stable id: rust-toolchain with: @@ -226,6 +231,7 @@ jobs: steps: - uses: actions/checkout@v7 + - uses: ./.github/actions/clone-needed-cargo-submodules - uses: dtolnay/rust-toolchain@stable id: rust-toolchain with: @@ -270,6 +276,7 @@ jobs: with: fetch-depth: 0 fetch-tags: true + - uses: ./.github/actions/clone-needed-cargo-submodules - uses: dtolnay/rust-toolchain@stable id: rust-toolchain - *set-build-metadata diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 46347cd3..fac7139c 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -8,6 +8,7 @@ on: pull_request: paths: - .github/workflows/copilot-setup-steps.yml + - .github/actions/clone-needed-cargo-submodules/** jobs: copilot-setup-steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40164a23..ca4dfb82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,11 @@ on: pull_request: paths: - .github/workflows/release.yml + - .github/actions/build-docs-epub/** + - .github/actions/build-kobo/** + - .github/actions/cargo-cache/** + - .github/actions/clone-needed-cargo-submodules/** + - .github/actions/install-doc-tools/** permissions: contents: write diff --git a/.gitmodules b/.gitmodules index a374479c..33a2bea7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -50,3 +50,6 @@ path = thirdparty/sqlite url = https://github.com/sqlite/sqlite branch = version-3.49.2 +[submodule "thirdparty/html5ever"] + path = thirdparty/html5ever + url = https://github.com/OGKevin/html5ever diff --git a/Cargo.lock b/Cargo.lock index 7e598fae..2d2cdc15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "aes" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ "cipher", "cpubits", @@ -99,9 +99,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -155,9 +155,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "async-broadcast" @@ -221,15 +221,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -348,9 +348,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", "zeroize", @@ -378,9 +378,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -390,9 +390,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bzip2" @@ -462,6 +462,7 @@ dependencies = [ "rand_xoshiro", "regex", "reqwest", + "roxmltree", "rust-embed", "rustls", "secrecy", @@ -501,9 +502,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.64" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -511,12 +512,6 @@ dependencies = [ "shlex 2.0.1", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cexpr" version = "0.6.0" @@ -600,11 +595,11 @@ dependencies = [ [[package]] name = "cipher" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.2.1", + "crypto-common 0.2.2", "inout", ] @@ -655,30 +650,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -762,9 +757,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -871,9 +866,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -914,18 +909,15 @@ dependencies = [ [[package]] name = "deflate64" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "digest" @@ -939,22 +931,22 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", "zeroize", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -981,9 +973,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] @@ -1064,7 +1056,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1106,9 +1098,9 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fdeflate" @@ -1130,13 +1122,12 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -1197,7 +1188,7 @@ dependencies = [ "fluent-syntax", "intl-memoizer", "intl_pluralrules", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "self_cell", "smallvec", "unic-langid", @@ -1239,12 +1230,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1436,24 +1421,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", "wasm-bindgen", ] @@ -1474,9 +1457,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -1502,15 +1485,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1519,14 +1493,14 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -1564,24 +1538,23 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", ] [[package]] name = "html5ever" version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" dependencies = [ "log", "markup5ever", + "memchr", ] [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1633,18 +1606,18 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1655,7 +1628,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1663,15 +1635,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1792,12 +1763,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1805,9 +1777,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1818,9 +1790,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1832,15 +1804,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1852,15 +1824,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1871,12 +1843,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -1890,9 +1856,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1913,7 +1879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1948,19 +1914,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1986,11 +1942,20 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jemalloc_pprof" @@ -2011,25 +1976,52 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[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.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +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" @@ -2043,11 +2035,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2085,12 +2078,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "levenshtein" version = "1.0.5" @@ -2099,9 +2086,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" @@ -2135,24 +2122,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.48.5", -] - -[[package]] -name = "libredox" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" -dependencies = [ - "bitflags 2.13.0", - "libc", - "plain", - "redox_syscall 0.7.3", + "windows-link", ] [[package]] @@ -2186,9 +2161,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2201,9 +2176,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -2213,11 +2188,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rust2" -version = "0.16.2" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +checksum = "ce716bf1a316f47a280fc76295f6495b5bea4752bca01c3b3885e101b1c23c02" dependencies = [ - "sha2 0.10.9", + "sha2 0.11.0", ] [[package]] @@ -2247,8 +2222,6 @@ dependencies = [ [[package]] name = "markup5ever" version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" dependencies = [ "log", "tendril", @@ -2271,20 +2244,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -2322,9 +2295,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -2362,9 +2335,9 @@ dependencies = [ [[package]] name = "no_std_io2" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" dependencies = [ "memchr", ] @@ -2423,9 +2396,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2595,7 +2568,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "portable-atomic", - "rand 0.9.2", + "rand 0.9.4", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -2651,7 +2624,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -2668,7 +2641,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", "hmac", ] @@ -2754,23 +2727,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plotters" @@ -2821,9 +2782,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2869,16 +2830,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -2951,9 +2902,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -2961,12 +2912,12 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn", @@ -3005,16 +2956,16 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "socket2", "thiserror 2.0.18", @@ -3025,17 +2976,17 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -3056,14 +3007,14 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -3075,23 +3026,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.8.6" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] @@ -3102,20 +3048,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.3", "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -3126,15 +3062,6 @@ dependencies = [ "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.17", -] - [[package]] name = "rand_core" version = "0.9.5" @@ -3161,9 +3088,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -3188,15 +3115,6 @@ dependencies = [ "bitflags 2.13.0", ] -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.13.0", -] - [[package]] name = "regex" version = "1.12.4" @@ -3286,6 +3204,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + [[package]] name = "rust-embed" version = "8.11.0" @@ -3335,9 +3262,18 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +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 = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] name = "rustix" @@ -3349,14 +3285,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "log", @@ -3370,9 +3306,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3382,9 +3318,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -3392,9 +3328,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", @@ -3408,7 +3344,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3461,9 +3397,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3538,9 +3474,9 @@ checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "septem" @@ -3631,7 +3567,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -3653,7 +3589,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -3699,15 +3635,31 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +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 = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3717,9 +3669,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -3756,9 +3708,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3880,7 +3832,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest 0.11.2", + "digest 0.11.3", "dotenvy", "either", "futures-core", @@ -4055,9 +4007,9 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -4102,10 +4054,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4200,12 +4152,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", - "itoa", "js-sys", "num-conv", "powerfmt", @@ -4216,15 +4167,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -4232,9 +4183,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -4253,9 +4204,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -4359,9 +4310,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", "toml_datetime", @@ -4401,20 +4352,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4551,7 +4502,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] @@ -4562,9 +4513,9 @@ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" @@ -4654,12 +4605,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" @@ -4702,7 +4647,7 @@ version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -4759,27 +4704,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -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 = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -4790,23 +4726,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4814,9 +4746,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", @@ -4827,52 +4759,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" 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", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4890,9 +4788,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ "phf 0.13.1", "phf_codegen", @@ -4902,18 +4800,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -4952,7 +4850,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -5022,20 +4920,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.5", ] [[package]] @@ -5047,21 +4945,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -5071,7 +4954,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -5079,10 +4962,21 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] [[package]] name = "windows_aarch64_gnullvm" @@ -5091,10 +4985,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -5103,10 +4997,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -5114,6 +5008,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -5121,10 +5021,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -5133,10 +5033,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -5145,10 +5045,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -5157,10 +5057,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -5169,107 +5069,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" -dependencies = [ - "memchr", -] - -[[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", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "windows_x86_64_msvc" +version = "0.53.1" 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", -] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "wit-component" -version = "0.244.0" +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "memchr", ] [[package]] -name = "wit-parser" -version = "0.244.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xattr" @@ -5309,9 +5133,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5320,9 +5144,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -5388,18 +5212,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -5408,18 +5232,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -5429,15 +5253,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -5446,9 +5270,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -5458,9 +5282,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -5479,7 +5303,7 @@ dependencies = [ "crc32fast", "deflate64", "flate2", - "getrandom 0.4.1", + "getrandom 0.4.3", "hmac", "indexmap", "lzma-rust2", @@ -5496,9 +5320,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" +checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 3fb0a59f..329d1e8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,10 @@ rust-embed = "8" [workspace.lints] rustdoc.private_intra_doc_links = "allow" +[patch.crates-io] +html5ever = { path = "thirdparty/html5ever/html5ever" } +markup5ever = { path = "thirdparty/html5ever/markup5ever" } + [profile.release-minsized] inherits = "release" panic = "abort" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c4a0c842..4a381182 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -41,7 +41,8 @@ secrecy = { version = "0.10", features = ["serde"] } walkdir = "2.5.0" globset = "0.4.18" fxhash = "0.2.1" -html5ever = "0.39" +html5ever = { version = "0.39", features = ["source-positions", "xhtml-self-closing"] } +roxmltree = "0.21" humanize-bytes = "1.0.6" humantime = "2" rand_core = "0.10.0" diff --git a/crates/core/src/document/epub/mod.rs b/crates/core/src/document/epub/mod.rs index 00f65444..44dcf657 100644 --- a/crates/core/src/document/epub/mod.rs +++ b/crates/core/src/document/epub/mod.rs @@ -1,11 +1,11 @@ use super::html::css::CssParser; -use super::html::dom::{NodeRef, XmlTree}; +use super::html::dom::NodeRef; use super::html::engine::{Engine, Page, ResourceFetcher}; use super::html::layout::TextAlign; use super::html::layout::{DrawCommand, DrawState, ImageCommand, RootData, TextCommand}; use super::html::layout::{LoopContext, StyleData}; use super::html::style::StyleSheet; -use super::html::xml::XmlParser; +use super::html::xml::parse_html5; use super::pdf::PdfOpener; use crate::document::{BoundedText, Document, Location, TextLocation, TocEntry, chapter_from_uri}; use crate::framebuffer::Pixmap; @@ -14,6 +14,7 @@ use crate::helpers::{Normalize, decode_entities}; use crate::unit::pt_to_px; use anyhow::{Error, format_err}; use fxhash::FxHashMap; +use opf::{ManifestEntry, OpfDocument, opf_path_from_container, parse_toc}; use percent_encoding::percent_decode_str; use std::collections::BTreeSet; use std::fs::{self, File}; @@ -21,6 +22,8 @@ use std::io::{Cursor, Read, Seek}; use std::path::{Path, PathBuf}; use zip::ZipArchive; +mod opf; + const VIEWER_STYLESHEET: &str = "css/epub.css"; const USER_STYLESHEET: &str = "css/epub-user.css"; @@ -38,7 +41,7 @@ impl ResourceFetcher for ZipArchive { /// Generic EPUB document that works with any Read + Seek source. pub struct EpubDocument { archive: ZipArchive, - info: XmlTree, + info: OpfDocument, parent: PathBuf, engine: Engine, spine: Vec, @@ -61,6 +64,54 @@ struct Chunk { unsafe impl Send for EpubDocument {} unsafe impl Sync for EpubDocument {} +/// Resolves spine `idref` values to [`Chunk`]s by probing the zip archive. +/// +/// For each `idref` the function: +/// 1. Looks up the matching [`ManifestEntry`] to get the file's `href`. +/// 2. Decodes the `href` (HTML entities + percent-encoding) and resolves it +/// relative to `opf_parent` to obtain the archive-relative path. +/// 3. Opens the entry in the archive to read its **uncompressed byte size**. +/// The size is stored on [`Chunk`] and used as the chapter's contribution +/// to the global byte-offset coordinate system (reading positions, bookmarks). +/// +/// Entries with no matching manifest item or with a non-UTF-8 path are silently +/// skipped — those indicate a structurally malformed EPUB. Entries whose file +/// is missing from the archive are logged at error level and skipped. +fn build_spine( + archive: &mut ZipArchive, + manifest: &[ManifestEntry], + idrefs: &[String], + opf_parent: &Path, +) -> Vec { + idrefs + .iter() + .filter_map(|idref| { + let entry = manifest.iter().find(|e| e.id == *idref)?; + let href = decode_entities(&entry.href); + let href = percent_decode_str(&href).decode_utf8_lossy(); + let path = opf_parent.join::<&str>(href.as_ref()).normalize(); + let path = path.to_str()?.to_string(); + + let result = archive.by_name(&path).map(|zf| Chunk { + path: path.clone(), + size: zf.size() as usize, + }); + + match result { + Ok(chunk) => Some(chunk), + Err(e) => { + tracing::error!( + path, + error = %e, + "spine entry missing from archive" + ); + None + } + } + }) + .collect() +} + impl EpubDocument { #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] fn from_archive(mut archive: ZipArchive) -> Result { @@ -68,11 +119,7 @@ impl EpubDocument { let mut zf = archive.by_name("META-INF/container.xml")?; let mut text = String::new(); zf.read_to_string(&mut text)?; - let root = XmlParser::new(&text).parse(); - root.root() - .find("rootfile") - .and_then(|e| e.attribute("full-path")) - .map(String::from) + opf_path_from_container(&text) } .ok_or_else(|| format_err!("can't get the OPF path"))?; @@ -80,57 +127,18 @@ impl EpubDocument { .parent() .unwrap_or_else(|| Path::new("")); - let text = { + let opf_text = { let mut zf = archive.by_name(&opf_path)?; let mut text = String::new(); zf.read_to_string(&mut text)?; text }; - let info = XmlParser::new(&text).parse(); - let mut spine = Vec::new(); - - { - let manifest = info - .root() - .find("manifest") - .ok_or_else(|| format_err!("the manifest is missing"))?; - - let spn = info - .root() - .find("spine") - .ok_or_else(|| format_err!("the spine is missing"))?; - - for child in spn.children() { - let vertebra_opt = child - .attribute("idref") - .and_then(|idref| manifest.find_by_id(idref)) - .and_then(|entry| entry.attribute("href")) - .and_then(|href| { - let href = decode_entities(href); - let href = percent_decode_str(&href).decode_utf8_lossy(); - let href_path = parent.join::<&str>(href.as_ref()); - href_path.to_str().and_then(|path| { - archive - .by_name(path) - .map_err(|e| { - tracing::error!( - "Can't retrieve '{}' from the archive: {:#}.", - path, - e - ) - // We're assuming that the size of the spine is less than 4 GiB. - }) - .map(|zf| (zf.size() as usize, path.to_string())) - .ok() - }) - }); + let info = OpfDocument::parse(opf_text) + .ok_or_else(|| format_err!("failed to parse OPF document"))?; - if let Some((size, path)) = vertebra_opt { - spine.push(Chunk { path, size }); - } - } - } + let (idrefs, _) = info.spine_idrefs(); + let spine = build_spine(&mut archive, &info.manifest, idrefs, parent); if spine.is_empty() { return Err(format_err!("the spine is empty")); @@ -183,119 +191,6 @@ impl EpubDocument { self.vertebra_coordinates_with(|index, _| self.spine[index].path == name) } - fn walk_toc_ncx( - &mut self, - node: NodeRef, - toc_dir: &Path, - index: &mut usize, - cache: &mut UriCache, - ) -> Vec { - let mut entries = Vec::new(); - // TODO: Take `playOrder` into account? - - for child in node.children() { - if child.tag_name() == Some("navPoint") { - let title = child - .find("navLabel") - .and_then(|label| label.find("text")) - .map(|text| decode_entities(&text.text()).into_owned()) - .unwrap_or_default(); - - // Example URI: pr03.html#codecomma_and_what_to_do_with_it - let rel_uri = child - .find("content") - .and_then(|content| { - content.attribute("src").map(|src| { - percent_decode_str(&decode_entities(src)) - .decode_utf8_lossy() - .into_owned() - }) - }) - .unwrap_or_default(); - - let loc = toc_dir - .join(&rel_uri) - .normalize() - .to_str() - .map(|uri| Location::Uri(uri.to_string())); - - let current_index = *index; - *index += 1; - - let sub_entries = if child.children().count() > 2 { - self.walk_toc_ncx(child, toc_dir, index, cache) - } else { - Vec::new() - }; - - if let Some(location) = loc { - entries.push(TocEntry { - title, - location, - index: current_index, - children: sub_entries, - }); - } - } - } - - entries - } - - fn walk_toc_nav( - &mut self, - node: NodeRef, - toc_dir: &Path, - index: &mut usize, - cache: &mut UriCache, - ) -> Vec { - let mut entries = Vec::new(); - - for child in node.children() { - if child.tag_name() == Some("li") { - let link = child.children().find(|child| child.tag_name() == Some("a")); - let title = link - .map(|link| decode_entities(&link.text()).into_owned()) - .unwrap_or_default(); - let rel_uri = link - .and_then(|link| { - link.attribute("href").map(|href| { - percent_decode_str(&decode_entities(href)) - .decode_utf8_lossy() - .into_owned() - }) - }) - .unwrap_or_default(); - - let loc = toc_dir - .join(&rel_uri) - .normalize() - .to_str() - .map(|uri| Location::Uri(uri.to_string())); - - let current_index = *index; - *index += 1; - - let sub_entries = if let Some(sub_list) = child.find("ol") { - self.walk_toc_nav(sub_list, toc_dir, index, cache) - } else { - Vec::new() - }; - - if let Some(location) = loc { - entries.push(TocEntry { - title, - location, - index: current_index, - children: sub_entries, - }); - } - } - } - - entries - } - #[inline] fn page_index(&mut self, offset: usize, index: usize, start_offset: usize) -> Option { if !self.cache.contains_key(&index) { @@ -338,7 +233,7 @@ impl EpubDocument { let mut zf = self.archive.by_name(name).ok()?; zf.read_to_string(&mut text).ok()?; } - let root = XmlParser::new(&text).parse(); + let root = parse_html5(&text); self.cache_uris(root.root(), name, start_offset, cache); cache.get(uri).cloned() } else { @@ -378,7 +273,7 @@ impl EpubDocument { } } - let mut root = XmlParser::new(&text).parse(); + let mut root = parse_html5(&text); root.wrap_lost_inlines(); let mut stylesheet = StyleSheet::new(); @@ -488,31 +383,7 @@ impl EpubDocument { } pub fn categories(&self) -> BTreeSet { - let mut result = BTreeSet::new(); - - if let Some(md) = self.info.root().find("metadata") { - for child in md.children() { - if child.tag_qualified_name() == Some("dc:subject") { - let text = child.text(); - let subject = decode_entities(&text); - // Pipe separated list of BISAC categories - if subject.contains(" / ") { - for categ in subject.split('|') { - let start_index = if let Some(index) = categ.find(" - ") { - index + 3 - } else { - 0 - }; - result.insert(categ[start_index..].trim().replace(" / ", ".")); - } - } else { - result.insert(subject.into_owned()); - } - } - } - } - - result + self.info.categories() } fn chapter_aux<'a>( @@ -625,70 +496,11 @@ impl EpubDocument { } pub fn series(&self) -> Option<(String, String)> { - self.info.root().find("metadata").and_then(|md| { - let mut title = None; - let mut index = None; - - for child in md.children() { - if child.tag_name() == Some("meta") { - if child.attribute("name") == Some("calibre:series") { - title = child - .attribute("content") - .map(|s| decode_entities(s).into_owned()); - } else if child.attribute("name") == Some("calibre:series_index") { - index = child - .attribute("content") - .map(|s| decode_entities(s).into_owned()); - } else if child.attribute("property") == Some("belongs-to-collection") { - title = Some(decode_entities(&child.text()).into_owned()); - } else if child.attribute("property") == Some("group-position") { - index = Some(decode_entities(&child.text()).into_owned()); - } - } - - if title.is_some() && index.is_some() { - break; - } - } - - title.into_iter().zip(index).next() - }) + self.info.series() } - pub fn cover_image(&self) -> Option<&str> { - self.info - .root() - .find("metadata") - .and_then(|md| { - md.children().find(|child| { - child.tag_name() == Some("meta") && child.attribute("name") == Some("cover") - }) - }) - .and_then(|entry| entry.attribute("content")) - .and_then(|cover_id| { - self.info - .root() - .find("manifest") - .and_then(|entry| entry.find_by_id(cover_id)) - .and_then(|entry| entry.attribute("href")) - }) - .or_else(|| { - self.info - .root() - .find("manifest") - .and_then(|mf| { - mf.children().find(|child| { - (child - .attribute("href") - .map_or(false, |hr| hr.contains("cover") || hr.contains("Cover")) - || child.id().map_or(false, |id| id.contains("cover"))) - && child - .attribute("media-type") - .map_or(false, |mt| mt.starts_with("image/")) - }) - }) - .and_then(|entry| entry.attribute("href")) - }) + pub fn cover_image(&self) -> Option { + self.info.cover_image_href() } pub fn description(&self) -> Option { @@ -762,41 +574,13 @@ impl Document for EpubDocument { } fn toc(&mut self) -> Option> { - let name = self - .info - .root() - .find("spine") - .and_then(|spine| spine.attribute("toc")) - .and_then(|toc_id| { - self.info - .root() - .find("manifest") - .and_then(|manifest| manifest.find_by_id(toc_id)) - .and_then(|entry| entry.attribute("href")) - }) - .or_else(|| { - self.info - .root() - .find("manifest") - .and_then(|manifest| { - manifest.children().find(|child| { - child - .attribute("properties") - .iter() - .any(|props| props.split_whitespace().any(|prop| prop == "nav")) - }) - }) - .and_then(|entry| entry.attribute("href")) - }) - .map(|href| { - self.parent - .join(href) - .normalize() - .to_string_lossy() - .into_owned() - })?; - - let toc_dir = Path::new(&name).parent().unwrap_or_else(|| Path::new("")); + let name = self.info.toc_href().map(|href| { + self.parent + .join(href) + .normalize() + .to_string_lossy() + .into_owned() + })?; let mut text = String::new(); if let Ok(mut zf) = self.archive.by_name(&name) { @@ -805,21 +589,7 @@ impl Document for EpubDocument { return None; } - let root = XmlParser::new(&text).parse(); - - if name.ends_with(".ncx") { - root.root() - .find("navMap") - .map(|map| self.walk_toc_ncx(map, toc_dir, &mut 0, &mut FxHashMap::default())) - } else { - root.root() - .descendants() - .find(|desc| { - desc.tag_name() == Some("nav") && desc.attribute("epub:type") == Some("toc") - }) - .and_then(|map| map.find("ol")) - .map(|map| self.walk_toc_nav(map, toc_dir, &mut 0, &mut FxHashMap::default())) - } + parse_toc(&text, &name).map(|toc| toc.into_entries()) } fn chapter<'a>(&mut self, offset: usize, toc: &'a [TocEntry]) -> Option<(&'a TocEntry, f32)> { @@ -1134,14 +904,7 @@ impl Document for EpubDocument { } fn metadata(&self, key: &str) -> Option { - self.info - .root() - .find("metadata") - .and_then(|md| { - md.children() - .find(|child| child.tag_qualified_name() == Some(key)) - }) - .map(|child| decode_entities(&child.text()).into_owned()) + self.info.metadata_value(key) } fn is_reflowable(&self) -> bool { @@ -1156,8 +919,281 @@ impl Document for EpubDocument { #[cfg(test)] mod tests { use super::*; + use crate::document::html::dom::XmlTree; use crate::document::html::layout::DrawCommand; + use crate::document::html::xml::XmlParser; + use opf::OpfDocument; + use std::io::Write; use std::path::PathBuf; + use zip::write::SimpleFileOptions; + + /// Minimal EPUB chapter that resembles a real spine file: XML declaration, + /// DOCTYPE, explicit html/head/body, paragraphs with `id` attributes + /// (needed for `cache_uris` and `DrawCommand::Marker`), and a text span. + const CHAPTER_HTML: &str = concat!( + "\n", + "\n", + "", + "Test", + "", + "

First paragraph.

", + "

Second emphasis paragraph.

", + "

Third paragraph with inline content.

", + "", + ); + + /// Variant of `CHAPTER_HTML` containing only block-level structure with no + /// inline text nodes. Used by the display-list Marker test because the + /// engine's inline-text layout path requires loaded fonts, whereas the + /// block path that emits `DrawCommand::Marker` does not. + const CHAPTER_HTML_BLOCK_ONLY: &str = concat!( + "\n", + "\n", + "", + "", + "", + "
", + "
", + "
", + "", + ); + + /// Collect `(tag_name, id_attr_value, byte_offset)` for every element that + /// has an `id` attribute, in document order. Used to compare bookmark / + /// annotation anchor points between parsers. + fn collect_id_offsets(tree: &XmlTree) -> Vec<(String, String, usize)> { + tree.root() + .descendants() + .filter_map(|n| { + let tag = n.tag_name()?; + let id = n.attribute("id")?; + Some((tag.to_string(), id.to_string(), n.offset())) + }) + .collect() + } + + /// Collect all `DrawCommand::Marker` offsets from a flat display list, in + /// order. Marker offsets are exactly what gets stored as reading positions + /// and bookmark targets. + fn collect_marker_offsets(pages: &[Page]) -> Vec { + pages + .iter() + .flatten() + .filter_map(|cmd| match cmd { + DrawCommand::Marker(offset) => Some(*offset), + _ => None, + }) + .collect() + } + + /// Build an in-memory EPUB zip containing a single spine chapter and + /// return it as a `Vec` suitable for `EpubDocument::from_archive`. + fn build_minimal_epub(chapter_html: &str) -> Vec { + let buf = Vec::new(); + let cursor = std::io::Cursor::new(buf); + let mut zip = zip::ZipWriter::new(cursor); + let opts = SimpleFileOptions::default(); + + zip.start_file("META-INF/container.xml", opts).unwrap(); + zip.write_all( + br#" + + + + +"#, + ) + .unwrap(); + + let chapter_bytes = chapter_html.as_bytes(); + zip.start_file("OEBPS/chapter.xhtml", opts).unwrap(); + zip.write_all(chapter_bytes).unwrap(); + + let opf = r#" + + + + + + + + +"#; + zip.start_file("OEBPS/content.opf", opts).unwrap(); + zip.write_all(opf.as_bytes()).unwrap(); + + zip.finish().unwrap().into_inner() + } + + /// Verify that `parse_html5` and `XmlParser` assign identical byte offsets + /// to every element that carries an `id` attribute in a realistic EPUB + /// chapter. These offsets are what gets stored as reading positions, + /// bookmark targets, and annotation anchors. + #[test] + fn epub_spine_chapter_id_offsets_match_between_parsers() { + let xml_offsets = { + let mut tree = XmlParser::new(CHAPTER_HTML).parse(); + tree.wrap_lost_inlines(); + collect_id_offsets(&tree) + }; + + let h5_offsets = { + let mut tree = parse_html5(CHAPTER_HTML); + tree.wrap_lost_inlines(); + collect_id_offsets(&tree) + }; + + assert_eq!( + xml_offsets, h5_offsets, + "id-attribute node offsets differ between XmlParser and parse_html5\n\ + XmlParser: {xml_offsets:?}\n\ + html5ever: {h5_offsets:?}" + ); + } + + /// Verify that `cache_uris` (the `#anchor-id` → byte-offset map used for + /// in-book link resolution) produces identical mappings from both parsers. + #[test] + fn epub_spine_chapter_cache_uris_match_between_parsers() { + let name = "OEBPS/chapter.xhtml"; + let start_offset: usize = 0; + + let xml_cache = { + let mut cache = UriCache::default(); + let tree = XmlParser::new(CHAPTER_HTML).parse(); + let mut dummy_doc: EpubDocument>> = EpubDocument { + archive: ZipArchive::new(std::io::Cursor::new(build_minimal_epub(CHAPTER_HTML))) + .unwrap(), + info: OpfDocument::empty(), + parent: PathBuf::default(), + engine: Engine::new(), + spine: vec![Chunk { + path: name.to_string(), + size: CHAPTER_HTML.len(), + }], + cache: FxHashMap::default(), + ignore_document_css: false, + }; + dummy_doc.cache_uris(tree.root(), name, start_offset, &mut cache); + cache + }; + + let h5_cache = { + let mut cache = UriCache::default(); + let tree = parse_html5(CHAPTER_HTML); + let mut dummy_doc: EpubDocument>> = EpubDocument { + archive: ZipArchive::new(std::io::Cursor::new(build_minimal_epub(CHAPTER_HTML))) + .unwrap(), + info: OpfDocument::empty(), + parent: PathBuf::default(), + engine: Engine::new(), + spine: vec![Chunk { + path: name.to_string(), + size: CHAPTER_HTML.len(), + }], + cache: FxHashMap::default(), + ignore_document_css: false, + }; + dummy_doc.cache_uris(tree.root(), name, start_offset, &mut cache); + cache + }; + + assert_eq!( + xml_cache, h5_cache, + "cache_uris maps differ between XmlParser and parse_html5\n\ + XmlParser: {xml_cache:?}\n\ + html5ever: {h5_cache:?}" + ); + } + + /// Verify that `build_display_list` emits `DrawCommand::Marker` commands + /// with identical offsets whether the spine chapter was parsed by + /// `XmlParser` or `parse_html5`. Marker offsets are stored as reading + /// positions and bookmark byte offsets, so they must be parser-independent. + /// + /// Uses a block-only chapter variant (no inline text nodes) so the engine + /// does not require loaded fonts — the Marker path is font-free. + #[test] + fn epub_spine_chapter_marker_offsets_match_between_parsers() { + let start_offset: usize = 512; + + let xml_markers = { + let mut tree = XmlParser::new(CHAPTER_HTML_BLOCK_ONLY).parse(); + tree.wrap_lost_inlines(); + marker_offsets_from_tree(tree, start_offset) + }; + + let h5_markers = { + let mut tree = parse_html5(CHAPTER_HTML_BLOCK_ONLY); + tree.wrap_lost_inlines(); + marker_offsets_from_tree(tree, start_offset) + }; + + assert!( + !xml_markers.is_empty(), + "no Marker commands produced — check id attributes" + ); + assert_eq!( + xml_markers, h5_markers, + "Marker offsets differ between XmlParser and parse_html5\n\ + XmlParser: {xml_markers:?}\n\ + html5ever: {h5_markers:?}" + ); + } + + /// Drive `Engine::build_display_list` directly for a pre-parsed tree and + /// collect all `DrawCommand::Marker` offsets. Uses a no-op resource + /// fetcher since the test chapter has no external assets. + fn marker_offsets_from_tree(tree: XmlTree, start_offset: usize) -> Vec { + struct NoopFetcher; + impl ResourceFetcher for NoopFetcher { + fn fetch(&mut self, _name: &str) -> Result, Error> { + Ok(Vec::new()) + } + } + + let mut engine = Engine::new(); + engine.layout(600, 800, 12.0, 265); + + let rect = engine.rect(); + let mut draw_state = DrawState { + position: rect.min, + ..Default::default() + }; + let root_data = RootData { + start_offset, + spine_dir: PathBuf::default(), + rect, + }; + let stylesheet = StyleSheet::new(); + let style = StyleData { + font_size: engine.font_size, + line_height: crate::unit::pt_to_px(engine.line_height * engine.font_size, engine.dpi) + .round() as i32, + text_align: engine.text_align, + start_x: rect.min.x, + end_x: rect.max.x, + width: rect.max.x - rect.min.x, + ..Default::default() + }; + let loop_context = LoopContext::default(); + let mut pages: Vec = vec![Vec::new()]; + + if let Some(body) = tree.root().find("body") { + engine.build_display_list( + body, + &style, + &loop_context, + &stylesheet, + &root_data, + &mut NoopFetcher, + &mut draw_state, + &mut pages, + ); + } + + collect_marker_offsets(&pages) + } fn setup_epub() -> EpubDocumentFile { let root_dir = PathBuf::from( std::env::var("TEST_ROOT_DIR").expect("TEST_ROOT_DIR must be set for epub tests"), @@ -1301,4 +1337,116 @@ mod tests { start_offset += doc.spine[i].size; } } + + /// Build a minimal in-memory zip with arbitrary named entries and return it + /// as a `ZipArchive` ready for `build_spine`. + fn zip_with_entries(entries: &[(&str, &[u8])]) -> ZipArchive>> { + let buf = std::io::Cursor::new(Vec::new()); + let mut zip = zip::ZipWriter::new(buf); + let opts = SimpleFileOptions::default(); + for (name, data) in entries { + zip.start_file(*name, opts).unwrap(); + zip.write_all(data).unwrap(); + } + ZipArchive::new(zip.finish().unwrap()).unwrap() + } + + fn manifest_entry(id: &str, href: &str) -> ManifestEntry { + ManifestEntry { + id: id.to_string(), + href: href.to_string(), + media_type: "application/xhtml+xml".to_string(), + properties: String::new(), + } + } + + #[test] + fn build_spine_resolves_all_entries_in_order() { + let ch1 = b"chapter one content"; + let ch2 = b"chapter two content longer"; + let mut archive = zip_with_entries(&[("OEBPS/ch1.xhtml", ch1), ("OEBPS/ch2.xhtml", ch2)]); + + let manifest = vec![ + manifest_entry("id1", "ch1.xhtml"), + manifest_entry("id2", "ch2.xhtml"), + ]; + let idrefs = vec!["id1".to_string(), "id2".to_string()]; + let parent = Path::new("OEBPS"); + + let spine = build_spine(&mut archive, &manifest, &idrefs, parent); + + assert_eq!(spine.len(), 2); + assert_eq!(spine[0].path, "OEBPS/ch1.xhtml"); + assert_eq!(spine[0].size, ch1.len()); + assert_eq!(spine[1].path, "OEBPS/ch2.xhtml"); + assert_eq!(spine[1].size, ch2.len()); + } + + #[test] + fn build_spine_preserves_spine_order_not_manifest_order() { + let mut archive = + zip_with_entries(&[("OEBPS/ch1.xhtml", b"a"), ("OEBPS/ch2.xhtml", b"bb")]); + + // Manifest lists ch2 before ch1, but spine references ch1 first. + let manifest = vec![ + manifest_entry("id2", "ch2.xhtml"), + manifest_entry("id1", "ch1.xhtml"), + ]; + let idrefs = vec!["id1".to_string(), "id2".to_string()]; + let parent = Path::new("OEBPS"); + + let spine = build_spine(&mut archive, &manifest, &idrefs, parent); + + assert_eq!(spine.len(), 2); + assert_eq!(spine[0].path, "OEBPS/ch1.xhtml"); + assert_eq!(spine[1].path, "OEBPS/ch2.xhtml"); + } + + #[test] + fn build_spine_skips_idref_with_no_manifest_entry() { + let mut archive = zip_with_entries(&[("OEBPS/ch1.xhtml", b"content")]); + + let manifest = vec![manifest_entry("id1", "ch1.xhtml")]; + // "ghost" has no matching manifest entry. + let idrefs = vec!["id1".to_string(), "ghost".to_string()]; + let parent = Path::new("OEBPS"); + + let spine = build_spine(&mut archive, &manifest, &idrefs, parent); + + assert_eq!(spine.len(), 1); + assert_eq!(spine[0].path, "OEBPS/ch1.xhtml"); + } + + #[test] + fn build_spine_skips_entry_absent_from_archive() { + // Manifest references ch2.xhtml but it is not in the zip. + let mut archive = zip_with_entries(&[("OEBPS/ch1.xhtml", b"content")]); + + let manifest = vec![ + manifest_entry("id1", "ch1.xhtml"), + manifest_entry("id2", "ch2.xhtml"), + ]; + let idrefs = vec!["id1".to_string(), "id2".to_string()]; + let parent = Path::new("OEBPS"); + + let spine = build_spine(&mut archive, &manifest, &idrefs, parent); + + assert_eq!(spine.len(), 1, "missing archive entry should be skipped"); + assert_eq!(spine[0].path, "OEBPS/ch1.xhtml"); + } + + #[test] + fn build_spine_decodes_percent_encoded_href() { + // Space encoded as %20 in the manifest href. + let mut archive = zip_with_entries(&[("OEBPS/chapter one.xhtml", b"hello")]); + + let manifest = vec![manifest_entry("id1", "chapter%20one.xhtml")]; + let idrefs = vec!["id1".to_string()]; + let parent = Path::new("OEBPS"); + + let spine = build_spine(&mut archive, &manifest, &idrefs, parent); + + assert_eq!(spine.len(), 1); + assert_eq!(spine[0].path, "OEBPS/chapter one.xhtml"); + } } diff --git a/crates/core/src/document/epub/opf.rs b/crates/core/src/document/epub/opf.rs new file mode 100644 index 00000000..cdd53b18 --- /dev/null +++ b/crates/core/src/document/epub/opf.rs @@ -0,0 +1,778 @@ +//! Lightweight, roxmltree-backed parsers for EPUB structural XML files. +//! +//! Three kinds of XML are handled here — none of which are HTML and none of +//! which require byte-offset tracking: +//! +//! - `META-INF/container.xml` — locates the OPF root file. +//! - The OPF (Open Packaging Format) file — manifest, spine, metadata. +//! - NCX or Navigation Document (NAV) — table of contents. +//! +//! [`OpfDocument`] parses the OPF source once at construction time and stores +//! all data as owned fields. Queries are plain field reads — no re-parsing. + +use crate::document::Location; +use crate::document::TocEntry; +use crate::helpers::{Normalize, decode_entities}; +use percent_encoding::percent_decode_str; +use std::collections::{BTreeSet, HashMap}; +use std::path::Path; + +/// XML namespace URI for Dublin Core Elements 1.1. +/// +/// EPUB OPF files use Dublin Core to store book metadata such as ``, +/// ``, ``, etc. The `dc:` prefix in the source is a +/// local alias that can vary between EPUBs; roxmltree resolves it to this URI, +/// which is the stable identifier used for matching. +const DC_NS: &str = "http://purl.org/dc/elements/1.1/"; + +/// Extracts the OPF root-file path from a `META-INF/container.xml` string. +/// +/// Returns `None` if the document is malformed or the `full-path` attribute +/// is missing. +pub fn opf_path_from_container(text: &str) -> Option { + let doc = roxmltree::Document::parse(text).ok()?; + doc.descendants() + .find(|n| n.is_element() && n.tag_name().name() == "rootfile") + .and_then(|n| n.attribute("full-path")) + .map(String::from) +} + +/// Parsed OPF document with all data pre-extracted into owned fields. +/// +/// Constructed once via [`OpfDocument::parse`]; all accessors are `O(1)` +/// field reads or cheap iterator passes over already-owned `Vec`s. +pub struct OpfDocument { + pub manifest: Vec, + /// `idref` values from `` in reading order. + pub spine_idrefs: Vec, + /// The `toc` attribute of `` — the manifest id of the NCX file. + pub spine_toc_id: Option, + /// Dublin Core metadata keyed by local name, e.g. `"creator"` → value. + dc_metadata: HashMap, + pub cover_href: Option, + pub series: Option<(String, String)>, + pub categories: BTreeSet, +} + +impl OpfDocument { + /// Parse an OPF source string, extracting all fields eagerly. + /// + /// Returns `None` if the XML is malformed. + pub fn parse(source: String) -> Option { + let doc = roxmltree::Document::parse(&source).ok()?; + + let manifest = extract_manifest(&doc); + let (spine_idrefs, spine_toc_id) = extract_spine(&doc); + let dc_metadata = extract_dc_metadata(&doc); + let cover_href = extract_cover_href(&doc, &manifest); + let series = extract_series(&doc); + let categories = extract_categories(&doc); + + Some(OpfDocument { + manifest, + spine_idrefs, + spine_toc_id, + dc_metadata, + cover_href, + series, + categories, + }) + } + + /// Returns an empty `OpfDocument` with no manifest, spine, or metadata. + /// + /// Used in tests that construct a stub [`super::EpubDocument`] without a + /// real OPF file. + #[cfg(test)] + pub fn empty() -> Self { + OpfDocument { + manifest: Vec::new(), + spine_idrefs: Vec::new(), + spine_toc_id: None, + dc_metadata: HashMap::new(), + cover_href: None, + series: None, + categories: BTreeSet::new(), + } + } + + /// Returns the `idref` values of all `` children of ``, + /// together with the `` attribute value if present. + pub fn spine_idrefs(&self) -> (&[String], Option<&str>) { + (&self.spine_idrefs, self.spine_toc_id.as_deref()) + } + + /// Returns the href of the TOC file: first tries ``, + /// then looks for a manifest item with `properties="nav"`. + pub fn toc_href(&self) -> Option { + let via_ncx = self + .spine_toc_id + .as_deref() + .and_then(|ncx_id| self.manifest.iter().find(|e| e.id == ncx_id)) + .map(|e| e.href.clone()); + + via_ncx.or_else(|| { + self.manifest + .iter() + .find(|e| e.properties.split_whitespace().any(|p| p == "nav")) + .map(|e| e.href.clone()) + }) + } + + /// Returns the value of a Dublin Core metadata element by local name, + /// e.g. `"creator"`, `"title"`, `"language"`. + /// + /// Also accepts `"dc:local_name"` form for backward compatibility with the + /// callers that used to pass the full qualified key. + pub fn metadata_value(&self, key: &str) -> Option { + let local = key.strip_prefix("dc:").unwrap_or(key); + self.dc_metadata.get(local).cloned() + } + + /// Returns the cover image href. + pub fn cover_image_href(&self) -> Option { + self.cover_href.clone() + } + + /// Returns the Calibre / OPF3 series title and position. + pub fn series(&self) -> Option<(String, String)> { + self.series.clone() + } + + /// Returns BISAC subject categories. + pub fn categories(&self) -> BTreeSet { + self.categories.clone() + } +} + +/// A single `` entry from the OPF ``. +#[derive(Debug, Clone)] +pub struct ManifestEntry { + pub id: String, + pub href: String, + pub media_type: String, + pub properties: String, +} + +fn extract_manifest(doc: &roxmltree::Document<'_>) -> Vec { + doc.descendants() + .filter(|n| n.is_element() && n.tag_name().name() == "manifest") + .flat_map(|manifest| { + manifest + .children() + .filter(|n| n.is_element() && n.tag_name().name() == "item") + .filter_map(|item| { + let id = item.attribute("id")?; + let href = item.attribute("href")?; + Some(ManifestEntry { + id: id.to_string(), + href: href.to_string(), + media_type: item.attribute("media-type").unwrap_or("").to_string(), + properties: item.attribute("properties").unwrap_or("").to_string(), + }) + }) + .collect::>() + }) + .collect() +} + +fn extract_spine(doc: &roxmltree::Document<'_>) -> (Vec, Option) { + let spine = doc + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "spine"); + + let toc_id = spine + .as_ref() + .and_then(|s| s.attribute("toc")) + .map(String::from); + + let idrefs = spine + .iter() + .flat_map(|s| s.children()) + .filter(|n| n.is_element() && n.tag_name().name() == "itemref") + .filter_map(|item| item.attribute("idref").map(String::from)) + .collect(); + + (idrefs, toc_id) +} + +fn extract_dc_metadata(doc: &roxmltree::Document<'_>) -> HashMap { + let mut map = HashMap::new(); + let Some(md) = doc + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "metadata") + else { + return map; + }; + + for child in md.children().filter(|n| n.is_element()) { + if child.tag_name().namespace() == Some(DC_NS) + && let Some(text) = child.text() + { + map.entry(child.tag_name().name().to_string()) + .or_insert_with(|| decode_entities(text).into_owned()); + } + } + + map +} + +fn extract_cover_href(doc: &roxmltree::Document<'_>, manifest: &[ManifestEntry]) -> Option { + let via_meta = doc + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "metadata") + .and_then(|md| { + md.children().find(|n| { + n.is_element() + && n.tag_name().name() == "meta" + && n.attribute("name") == Some("cover") + }) + }) + .and_then(|meta| meta.attribute("content").map(String::from)) + .and_then(|cover_id| { + manifest + .iter() + .find(|e| e.id == cover_id) + .map(|e| e.href.clone()) + }); + + via_meta.or_else(|| { + manifest + .iter() + .find(|e| { + (e.href.to_lowercase().contains("cover") || e.id.to_lowercase().contains("cover")) + && e.media_type.starts_with("image/") + }) + .map(|e| e.href.clone()) + }) +} + +fn extract_series(doc: &roxmltree::Document<'_>) -> Option<(String, String)> { + let md = doc + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "metadata")?; + + let mut title = None; + let mut index = None; + + for child in md.children().filter(|n| n.is_element()) { + if child.tag_name().name() == "meta" { + match child.attribute("name") { + Some("calibre:series") => { + title = child + .attribute("content") + .map(|s| decode_entities(s).into_owned()); + } + Some("calibre:series_index") => { + index = child + .attribute("content") + .map(|s| decode_entities(s).into_owned()); + } + _ => {} + } + if child.attribute("property") == Some("belongs-to-collection") { + title = child.text().map(|t| decode_entities(t).into_owned()); + } else if child.attribute("property") == Some("group-position") { + index = child.text().map(|t| decode_entities(t).into_owned()); + } + } + } + + title.zip(index) +} + +fn extract_categories(doc: &roxmltree::Document<'_>) -> BTreeSet { + let mut result = BTreeSet::new(); + + let Some(md) = doc + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "metadata") + else { + return result; + }; + + for child in md.children().filter(|n| n.is_element()) { + if child.tag_name().name() == "subject" + && child.tag_name().namespace() == Some(DC_NS) + && let Some(text) = child.text() + { + let subject = decode_entities(text); + if subject.contains(" / ") { + for categ in subject.split('|') { + let start_index = categ.find(" - ").map(|i| i + 3).unwrap_or(0); + result.insert(categ[start_index..].trim().replace(" / ", ".")); + } + } else { + result.insert(subject.into_owned()); + } + } + } + + result +} + +/// Parsed table-of-contents from either an NCX or Navigation Document. +pub struct Toc { + entries: Vec, +} + +impl Toc { + pub fn into_entries(self) -> Vec { + self.entries + } +} + +/// Parses an NCX or EPUB3 Navigation Document and returns a [`Toc`]. +/// +/// `name` is the archive path of the TOC file (used to decide NCX vs NAV and +/// to resolve relative links). +pub fn parse_toc(text: &str, name: &str) -> Option { + let doc = roxmltree::Document::parse(text).ok()?; + let toc_dir = Path::new(name).parent().unwrap_or_else(|| Path::new("")); + let mut index = 0; + + let entries = if name.ends_with(".ncx") { + doc.descendants() + .find(|n| n.is_element() && n.tag_name().name() == "navMap") + .map(|map| walk_ncx(map, toc_dir, &mut index)) + .unwrap_or_default() + } else { + doc.descendants() + .find(|n| { + n.is_element() + && n.tag_name().name() == "nav" + && n.attribute("epub:type") == Some("toc") + }) + .or_else(|| { + doc.descendants().find(|n| { + n.is_element() + && n.tag_name().name() == "nav" + && n.attributes() + .any(|a| a.name() == "type" && a.value() == "toc") + }) + }) + .and_then(|nav| { + nav.children() + .find(|n| n.is_element() && n.tag_name().name() == "ol") + }) + .map(|ol| walk_nav(ol, toc_dir, &mut index)) + .unwrap_or_default() + }; + + Some(Toc { entries }) +} + +fn walk_ncx(node: roxmltree::Node<'_, '_>, toc_dir: &Path, index: &mut usize) -> Vec { + let mut entries = Vec::new(); + + for child in node.children().filter(|n| n.is_element()) { + if child.tag_name().name() != "navPoint" { + continue; + } + + let title = child + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "text") + .and_then(|n| n.text()) + .map(|t| decode_entities(t).into_owned()) + .unwrap_or_default(); + + let rel_uri = child + .descendants() + .find(|n| n.is_element() && n.tag_name().name() == "content") + .and_then(|n| n.attribute("src")) + .map(|src| { + percent_decode_str(&decode_entities(src)) + .decode_utf8_lossy() + .into_owned() + }) + .unwrap_or_default(); + + let loc = toc_dir + .join(&rel_uri) + .normalize() + .to_str() + .map(|uri| Location::Uri(uri.to_string())); + + let current_index = *index; + *index += 1; + + let sub_entries = if child.children().filter(|n| n.is_element()).count() > 2 { + walk_ncx(child, toc_dir, index) + } else { + Vec::new() + }; + + if let Some(location) = loc { + entries.push(TocEntry { + title, + location, + index: current_index, + children: sub_entries, + }); + } + } + + entries +} + +fn walk_nav(node: roxmltree::Node<'_, '_>, toc_dir: &Path, index: &mut usize) -> Vec { + let mut entries = Vec::new(); + + for child in node.children().filter(|n| n.is_element()) { + if child.tag_name().name() != "li" { + continue; + } + + let link = child + .children() + .find(|n| n.is_element() && n.tag_name().name() == "a"); + + let title = link + .and_then(|a| a.text()) + .map(|t| decode_entities(t).into_owned()) + .unwrap_or_default(); + + let rel_uri = link + .and_then(|a| a.attribute("href")) + .map(|href| { + percent_decode_str(&decode_entities(href)) + .decode_utf8_lossy() + .into_owned() + }) + .unwrap_or_default(); + + let loc = toc_dir + .join(&rel_uri) + .normalize() + .to_str() + .map(|uri| Location::Uri(uri.to_string())); + + let current_index = *index; + *index += 1; + + let sub_entries = child + .children() + .find(|n| n.is_element() && n.tag_name().name() == "ol") + .map(|ol| walk_nav(ol, toc_dir, index)) + .unwrap_or_default(); + + if let Some(location) = loc { + entries.push(TocEntry { + title, + location, + index: current_index, + children: sub_entries, + }); + } + } + + entries +} +#[cfg(test)] +mod tests { + use super::*; + + const CONTAINER_XML: &str = r#" + + + + +"#; + + const OPF: &str = r#" + + + My Book + Alice Author + en + 2024-01-15 + Science / Physics + + + + + + + + + + + + + + +"#; + + const NCX: &str = r#" + + + + Chapter One + + + + Chapter Two + + + Section 2.1 + + + + +"#; + + const NAV: &str = r#" + + + + +"#; + + #[test] + fn container_extracts_opf_path() { + assert_eq!( + opf_path_from_container(CONTAINER_XML), + Some("OEBPS/content.opf".to_string()) + ); + } + + #[test] + fn container_malformed_xml_returns_none() { + assert_eq!(opf_path_from_container(">>"), None); + } + + #[test] + fn container_missing_full_path_attr_returns_none() { + let xml = r#" + + +"#; + assert_eq!(opf_path_from_container(xml), None); + } + + fn doc() -> OpfDocument { + OpfDocument::parse(OPF.to_string()).expect("OPF fixture should parse") + } + + #[test] + fn opf_parse_fails_on_malformed_xml() { + assert!(OpfDocument::parse(" + + + + + + + +"#; + let d = OpfDocument::parse(xml.to_string()).unwrap(); + assert_eq!(d.cover_href, Some("img/cover.jpg".to_string())); + } + + #[test] + fn opf_cover_href_none_when_no_cover() { + let xml = r#" + + + + + + +"#; + let d = OpfDocument::parse(xml.to_string()).unwrap(); + assert_eq!(d.cover_href, None); + } + + #[test] + fn opf_series_calibre_meta() { + assert_eq!( + doc().series, + Some(("Great Series".to_string(), "3".to_string())) + ); + } + + #[test] + fn opf_series_none_when_absent() { + let xml = r#" + + + + +"#; + assert_eq!(OpfDocument::parse(xml.to_string()).unwrap().series, None); + } + + #[test] + fn opf_categories_bisac_splitting() { + let d = doc(); + // "Science / Physics" contains " / " so it becomes "Science.Physics" + // after the BISAC replace. + assert!( + d.categories.contains("Science.Physics"), + "expected 'Science.Physics', got: {:?}", + d.categories + ); + } + + #[test] + fn toc_href_via_spine_toc_attribute() { + assert_eq!(doc().toc_href(), Some("toc.ncx".to_string())); + } + + #[test] + fn toc_href_via_nav_properties() { + let xml = r#" + + + + + + + +"#; + let d = OpfDocument::parse(xml.to_string()).unwrap(); + assert_eq!(d.toc_href(), Some("nav.xhtml".to_string())); + } + + #[test] + fn toc_href_none_when_no_toc() { + let xml = r#" + + + + + + +"#; + assert_eq!( + OpfDocument::parse(xml.to_string()).unwrap().toc_href(), + None + ); + } + + #[test] + fn parse_toc_ncx_top_level_entries() { + let toc = parse_toc(NCX, "OEBPS/toc.ncx").unwrap(); + let entries = toc.into_entries(); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].title, "Chapter One"); + assert_eq!(entries[1].title, "Chapter Two"); + } + + #[test] + fn parse_toc_ncx_resolves_paths_relative_to_toc() { + let toc = parse_toc(NCX, "OEBPS/toc.ncx").unwrap(); + let entries = toc.into_entries(); + assert!( + matches!(&entries[0].location, Location::Uri(u) if u == "OEBPS/chapter1.xhtml"), + "expected OEBPS/chapter1.xhtml, got {:?}", + entries[0].location + ); + } + + #[test] + fn parse_toc_ncx_nested_entries() { + let toc = parse_toc(NCX, "OEBPS/toc.ncx").unwrap(); + let entries = toc.into_entries(); + assert_eq!(entries[1].children.len(), 1); + assert_eq!(entries[1].children[0].title, "Section 2.1"); + } + + #[test] + fn parse_toc_nav_top_level_entries() { + let toc = parse_toc(NAV, "OEBPS/nav.xhtml").unwrap(); + let entries = toc.into_entries(); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].title, "Chapter One"); + assert_eq!(entries[1].title, "Chapter Two"); + } + + #[test] + fn parse_toc_nav_nested_entries() { + let toc = parse_toc(NAV, "OEBPS/nav.xhtml").unwrap(); + let entries = toc.into_entries(); + assert_eq!(entries[1].children.len(), 1); + assert_eq!(entries[1].children[0].title, "Section 2.1"); + } + + #[test] + fn parse_toc_malformed_xml_returns_none() { + assert!(parse_toc("`, ``), and implicitly- -/// closed block tags correctly per the HTML5 spec. Node offsets are **synthetic** -/// (not byte positions in the source), so this type is **not** suitable for -/// persisting reading positions to disk. Use it for ephemeral rendering such -/// as the dictionary view. -/// -/// For documents where offset accuracy matters (EPUB spine chapters, standalone -/// HTML files) use [`HtmlDocument`](super::HtmlDocument) instead. +/// closed block tags correctly per the HTML5 spec. Node offsets are +/// byte-accurate source positions supplied by the `source-positions` feature +/// of `html5ever`, matching those produced by [`XmlParser`](super::xml::XmlParser) +/// for the same input. pub struct Html5Document { /// Shared rendering state (tree, engine, page cache, stylesheets). pub(super) base: HtmlBase, @@ -52,7 +42,8 @@ impl Html5Document { /// [`set_user_stylesheet`](Self::set_user_stylesheet) to override them. #[cfg_attr(feature = "tracing", tracing::instrument(skip(text), fields(len = text.len())))] pub fn new_from_memory(text: &str) -> Html5Document { - let content = parse_html5(text); + let mut content = parse_html5(text); + content.wrap_lost_inlines(); Html5Document { base: HtmlBase::new( content, @@ -69,7 +60,9 @@ impl Html5Document { #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, text), fields(len = text.len())))] pub fn update(&mut self, text: &str) { self.base.size = text.len(); - self.base.content = parse_html5(text); + let mut content = parse_html5(text); + content.wrap_lost_inlines(); + self.base.content = content; self.base.pages.clear(); } diff --git a/crates/core/src/document/html/mod.rs b/crates/core/src/document/html/mod.rs index 4900a8cc..371a4012 100644 --- a/crates/core/src/document/html/mod.rs +++ b/crates/core/src/document/html/mod.rs @@ -3,14 +3,13 @@ //! This module provides two concrete document types that share a common //! rendering pipeline through [`HtmlBase`]: //! -//! - [`HtmlDocument`] — backed by the hand-rolled [`XmlParser`]. Node offsets -//! are exact byte positions in the source string, which is required when -//! reading positions, bookmarks, and annotations are persisted to disk. -//! Used for standalone HTML files and EPUB spine chapters. +//! - [`HtmlDocument`] — backed by the html5ever parser. Node offsets are +//! byte-accurate source positions, which is required when reading positions, +//! bookmarks, and annotations are persisted to disk. Used for standalone +//! HTML files and EPUB spine chapters. //! -//! - [`Html5Document`] — backed by html5ever. Node offsets are synthetic. -//! Used for ephemeral rendering (e.g. the dictionary view) where HTML5 -//! conformance matters more than offset precision. +//! - [`Html5Document`] — also backed by html5ever. Used for ephemeral +//! rendering where no positions are persisted (e.g. the dictionary view). //! //! The shared [`HtmlBase`] struct holds the parsed [`XmlTree`], the layout //! [`Engine`], the page cache, and stylesheet paths. Both document types @@ -33,7 +32,7 @@ use self::engine::{Engine, Page, ResourceFetcher}; use self::layout::{DrawCommand, ImageCommand, TextAlign, TextCommand}; use self::layout::{DrawState, LoopContext, RootData, StyleData}; use self::style::StyleSheet; -use self::xml::XmlParser; +use self::xml::parse_html5; use crate::document::{BoundedText, Document, Location, TextLocation, TocEntry}; use crate::framebuffer::Pixmap; use crate::geom::{Boundary, CycleDir, Edge}; @@ -455,11 +454,11 @@ impl ResourceFetcher for PathBuf { } } -/// HTML document backed by the hand-rolled [`XmlParser`]. +/// HTML document backed by html5ever. /// -/// Node offsets are exact byte positions in the source string, making this -/// suitable for EPUB spine chapters and standalone HTML files where reading -/// positions are persisted to disk as absolute byte offsets. +/// Node offsets are byte-accurate source positions, making this suitable for +/// standalone HTML files where reading positions are persisted to disk as +/// absolute byte offsets. pub struct HtmlDocument { /// The raw source text, retained so that [`Document::save`] can write it /// back to disk unchanged. @@ -472,7 +471,7 @@ unsafe impl Send for HtmlDocument {} unsafe impl Sync for HtmlDocument {} impl HtmlDocument { - /// Opens the file at `path`, parses it with [`XmlParser`], and returns a + /// Opens the file at `path`, parses it with html5ever, and returns a /// ready-to-render document. #[cfg_attr(feature = "tracing", tracing::instrument(skip(path), fields(path = %path.as_ref().display())))] pub fn new>(path: P) -> Result { @@ -480,7 +479,7 @@ impl HtmlDocument { let size = file.metadata()?.len() as usize; let mut text = String::new(); file.read_to_string(&mut text)?; - let mut content = XmlParser::new(&text).parse(); + let mut content = parse_html5(&text); content.wrap_lost_inlines(); let parent = path.as_ref().parent().unwrap_or_else(|| Path::new("")); @@ -503,7 +502,7 @@ impl HtmlDocument { #[cfg_attr(feature = "tracing", tracing::instrument(skip(text), fields(len = text.len())))] pub fn new_from_memory(text: &str) -> HtmlDocument { let size = text.len(); - let mut content = XmlParser::new(text).parse(); + let mut content = parse_html5(text); content.wrap_lost_inlines(); HtmlDocument { @@ -523,8 +522,9 @@ impl HtmlDocument { #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, text), fields(len = text.len())))] pub fn update(&mut self, text: &str) { self.base.size = text.len(); - self.base.content = XmlParser::new(text).parse(); - self.base.content.wrap_lost_inlines(); + let mut content = parse_html5(text); + content.wrap_lost_inlines(); + self.base.content = content; self.text = text.to_string(); self.base.pages.clear(); } diff --git a/crates/core/src/document/html/style.rs b/crates/core/src/document/html/style.rs index 0f0f59c8..4f6753ff 100644 --- a/crates/core/src/document/html/style.rs +++ b/crates/core/src/document/html/style.rs @@ -227,12 +227,12 @@ fn expand_and_insert(name: &str, value: &str, props: &mut PropertyMap) { #[cfg(test)] mod tests { use super::super::css::CssParser; - use super::super::xml::XmlParser; + use super::super::xml::parse_html5; use super::specified_values; #[test] fn comma_selector_matches_ol() { - let xml = XmlParser::new("
  1. item
").parse(); + let xml = parse_html5("
  1. item
"); let mut css = CssParser::new("ul, ol { margin-left: 1.5em }").parse(); css.sort(); let ol = xml.root().find("ol").unwrap(); @@ -246,8 +246,8 @@ mod tests { #[test] fn simple_style() { - let xml1 = XmlParser::new("").parse(); - let xml2 = XmlParser::new("").parse(); + let xml1 = parse_html5(""); + let xml2 = parse_html5(""); let mut css = CssParser::new( "a { b: 23 }\ .c.x.y { b: 6; c: 3 }\ diff --git a/crates/core/src/document/html/xml.rs b/crates/core/src/document/html/xml.rs index ae45e0a0..74cc752d 100644 --- a/crates/core/src/document/html/xml.rs +++ b/crates/core/src/document/html/xml.rs @@ -1,24 +1,21 @@ -//! HTML and XML parsers that produce an [`XmlTree`]. +//! HTML parser and associated utilities that produce an [`XmlTree`]. //! -//! Two parsers are provided, each suited to a different use-case: +//! **Production code** should always use [`parse_html5`] to parse HTML content. +//! It handles entities, void elements, and the full HTML5 error-recovery +//! algorithm. Node offsets are byte-accurate source positions supplied by the +//! `source-positions` feature on the `html5ever` crate. //! -//! - [`XmlParser`] — a hand-rolled recursive-descent parser. Node offsets are -//! exact byte positions of each token in the source string. Use this wherever -//! reading positions need to be persisted to disk (EPUB spine chapters, -//! standalone HTML files). -//! -//! - [`parse_html5`] — a thin wrapper around `html5ever`. Handles entities, -//! void elements, and the full HTML5 error-recovery algorithm. Node offsets -//! are **synthetic** (a monotonically increasing counter, not source -//! positions). Use this for ephemeral rendering where offset precision is -//! not required (e.g. the dictionary view). +//! [`XmlParser`] is only compiled in **test builds** (`#[cfg(test)]`). It is +//! retained exclusively for parity tests that compare its output against +//! `parse_html5` to validate byte-offset equivalence. Do not use it in +//! production code. use super::dom::{Attributes, NodeId, XmlTree, element, text, whitespace}; use fxhash::FxHashMap; use html5ever::tendril::{Tendril, TendrilSink}; use html5ever::tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink}; use html5ever::{Attribute, QualName}; -use std::cell::{Ref, RefCell}; +use std::cell::{Cell, Ref, RefCell}; /// Extension trait that adds XML whitespace detection to [`char`]. pub trait XmlExt { @@ -35,14 +32,15 @@ impl XmlExt for char { /// Hand-rolled recursive-descent parser for XML and basic HTML documents. /// +/// **Only available in test builds.** Production code uses [`parse_html5`] for +/// HTML content and `roxmltree` (via `epub::opf`) for XML metadata files. +/// This parser is retained solely for parity tests that compare its output +/// against `parse_html5` to validate byte-offset equivalence. +/// /// Produces an [`XmlTree`] where every node's `offset` field is the exact byte /// position of the opening `<` (elements) or first character (text nodes) in -/// `input`. This byte-accuracy is required when reading positions are -/// persisted across sessions. -/// -/// The parser is intentionally lenient: unknown tags, processing instructions, -/// and CDATA sections are skipped silently. Self-closing tags (`
`, -/// ``) are supported. +/// `input`. +#[cfg(test)] #[derive(Debug)] pub struct XmlParser<'a> { /// The full source string being parsed. @@ -51,6 +49,7 @@ pub struct XmlParser<'a> { pub offset: usize, } +#[cfg(test)] impl<'a> XmlParser<'a> { /// Creates a new parser positioned at the start of `input`. pub fn new(input: &str) -> XmlParser<'_> { @@ -229,11 +228,12 @@ impl<'a> XmlParser<'a> { /// [`TreeSink`] implementation that bridges html5ever's push-based API into /// an [`XmlTree`]. /// -/// Node offsets are assigned from a monotonically increasing counter rather -/// than from source byte positions, because html5ever's `TreeSink` callbacks -/// do not receive source positions. The counter advances by 1 per element and -/// by `text.len()` per text chunk, preserving the non-overlap invariant needed -/// by the layout engine's page-finding binary search. +/// Node offsets are byte-accurate source positions supplied by +/// [`TreeSink::set_current_byte`], which html5ever calls before every tree +/// mutation when the `source-positions` feature is enabled on the `html5ever` +/// crate. This makes offsets stable and comparable to those produced by +/// [`XmlParser`], enabling html5ever-parsed documents to be used wherever +/// persisted byte offsets are required (EPUB spine, bookmarks, annotations). struct Html5Sink { /// The tree being built. `RefCell` is required because multiple `TreeSink` /// methods need mutable access and Rust's borrow checker cannot see that @@ -245,34 +245,22 @@ struct Html5Sink { /// Maps `