From c8e5274164214bf98227228f0906a8cd98bbd98e Mon Sep 17 00:00:00 2001 From: Pat Hawks Date: Sat, 4 Apr 2026 21:00:47 -0500 Subject: [PATCH 1/4] Support password-protected zip files Add --password flag (repeatable) to try passwords on encrypted zip members. Enable AES encryption support in the zip crate. Track the successful password in CandidateSource and emit it in GEX Python output. --- Cargo.lock | 528 +++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 4 +- src/gex.rs | 15 +- src/main.rs | 9 +- src/pipeline.rs | 1 + src/types.rs | 10 +- src/utils.rs | 117 +++++++---- 7 files changed, 611 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f761b2..2c067d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,11 +8,22 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -25,15 +36,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -64,6 +75,21 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -78,9 +104,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", "jobserver", @@ -111,11 +137,21 @@ dependencies = [ "zstd", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -123,9 +159,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -135,9 +171,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -147,15 +183,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 = "colorchoice" -version = "1.0.4" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] name = "crc" @@ -181,6 +232,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -204,6 +276,22 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -212,8 +300,32 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", ] [[package]] @@ -228,14 +340,40 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", ] [[package]] @@ -244,21 +382,43 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jobserver" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.182" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "log" @@ -298,18 +458,44 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[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-macro2" version = "1.0.106" @@ -343,6 +529,77 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -357,9 +614,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "smallvec" @@ -373,6 +630,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -390,18 +653,36 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -411,6 +692,94 @@ 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" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +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", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -431,18 +800,113 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zip" -version = "8.2.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004" +checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464" dependencies = [ + "aes", + "constant_time_eq", "crc32fast", "flate2", + "getrandom 0.4.2", + "hmac", "indexmap", "memchr", + "pbkdf2", + "sha1", "typed-path", + "zeroize", "zopfli", ] @@ -452,6 +916,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zopfli" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index bc5908f..710ac1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0" -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4.6", features = ["derive"] } crc32fast = "1.5" nohash-hasher = "0.2.0" quick-xml = "0.39.2" @@ -23,5 +23,5 @@ smallvec = "1.15" sha1_smol = { version = "1.0", features = ["std"] } flate2 = "1" lzma-rs = "0.3" -zip = { version = "8", default-features = false, features = ["deflate"] } +zip = { version = "8", default-features = false, features = ["aes-crypto", "deflate"] } zstd = "0.13" diff --git a/src/gex.rs b/src/gex.rs index 4b806bc..d670d85 100644 --- a/src/gex.rs +++ b/src/gex.rs @@ -104,7 +104,11 @@ class GeneratedTask(BaseTask): )); out.push_str(" contents = f.read()\n"); } - CandidateSource::Zip { archive, member } => { + CandidateSource::Zip { + archive, + member, + password, + } => { let archive_name = archive .file_name() .map(|s| s.to_string_lossy()) @@ -114,7 +118,14 @@ class GeneratedTask(BaseTask): out.push_str(&format!( " with zipfile.ZipFile(os.path.join(in_dir, {py_archive})) as z:\n" )); - out.push_str(&format!(" with z.open({py_member}) as f:\n")); + if let Some(pw) = password { + let py_pw = py_str(pw); + out.push_str(&format!( + " with z.open({py_member}, pwd={py_pw}.encode()) as f:\n" + )); + } else { + out.push_str(&format!(" with z.open({py_member}) as f:\n")); + } out.push_str(" contents = f.read()\n"); } CandidateSource::Kpka { archive, index } => { diff --git a/src/main.rs b/src/main.rs index 71683a6..e3e9d90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,10 @@ struct Opt { #[arg(long)] no_expand: bool, + /// Passwords to try on encrypted zip members (may be repeated) + #[arg(long)] + password: Vec, + /// Verbose logging (-v) #[arg(short, long)] verbose: bool, @@ -68,7 +72,7 @@ struct Opt { fn run_extract(opt: &Opt, spec: chisel::types::ExtractionSpec) -> anyhow::Result<()> { std::fs::create_dir_all(&opt.output_dir)?; - let cands = utils::load_candidates_from_paths(&opt.input_files, opt.verbose)?; + let cands = utils::load_candidates_from_paths(&opt.input_files, &opt.password, opt.verbose)?; for cand in &cands { let mut s = spec.clone(); if s.size == 0 { @@ -110,7 +114,8 @@ fn main() -> anyhow::Result<()> { .as_ref() .ok_or_else(|| anyhow::anyhow!("either --dat or --spec is required"))?; let mut roms = load_rom_list(dat, opt.game.as_deref())?; - let mut cands = utils::load_candidates_from_paths(&opt.input_files, opt.verbose)?; + let mut cands = + utils::load_candidates_from_paths(&opt.input_files, &opt.password, opt.verbose)?; if opt.gex.is_none() { std::fs::create_dir_all(&opt.output_dir)?; diff --git a/src/pipeline.rs b/src/pipeline.rs index 79091be..b72472d 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -41,6 +41,7 @@ fn cost_ratio(work: u64, value: u64) -> u64 { ((work as f64) / (value.max(1) as f64)).ceil() as u64 } +#[allow(clippy::too_many_arguments)] pub fn run_pipeline( roms: &mut [RomInfo], cands: &mut [Candidate], diff --git a/src/types.rs b/src/types.rs index 67b94b8..7780042 100644 --- a/src/types.rs +++ b/src/types.rs @@ -235,7 +235,11 @@ pub enum CandidateSource { /// Single file decompressed from a gzip stream. Gzip { archive: PathBuf }, /// One member extracted from a zip archive. - Zip { archive: PathBuf, member: String }, + Zip { + archive: PathBuf, + member: String, + password: Option, + }, /// Decompressed from an LZMA/XZ block found at `offset` inside `parent`. Lzma { parent: PathBuf, offset: usize }, /// One entry extracted from a KPKA/PAK archive (index is entry ordinal, 0-based). @@ -268,7 +272,9 @@ impl std::fmt::Display for Candidate { .unwrap_or("???".into()); write!(f, "[gzip in {}]", a)?; } - CandidateSource::Zip { archive, member } => { + CandidateSource::Zip { + archive, member, .. + } => { let a = archive .file_name() .map(|s| s.to_string_lossy()) diff --git a/src/utils.rs b/src/utils.rs index 3043146..d9d027e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -28,7 +28,7 @@ const LZMA1_VALID_PROPS: [bool; 256] = { /// Check if a dict_size is plausible for real LZMA1 streams. /// Real dict sizes are powers of 2 or 2^n + 2^(n-1) (i.e. 3 * 2^(n-1)). fn is_valid_lzma_dict_size(d: u32) -> bool { - d.is_power_of_two() || (d % 3 == 0 && d / 3 > 0 && (d / 3).is_power_of_two()) + d.is_power_of_two() || (d.is_multiple_of(3) && d / 3 > 0 && (d / 3).is_power_of_two()) } use crate::{Candidate, RomInfo}; @@ -211,7 +211,11 @@ pub fn load_rom_list(dat_path: &Path, maybe_game: Option<&str>) -> anyhow::Resul /// - `.gz` / gzip magic (`1f 8b`): decompress into one candidate. /// - `.zip` magic (`PK\x03\x04`): one candidate per unencrypted file member. /// - Everything else: one plain candidate. -pub fn load_candidates_from_paths(paths: I, verbose: bool) -> anyhow::Result> +pub fn load_candidates_from_paths( + paths: I, + passwords: &[String], + verbose: bool, +) -> anyhow::Result> where I: IntoIterator, I::Item: AsRef, @@ -257,45 +261,86 @@ where let cursor = std::io::Cursor::new(&data); let mut archive = zip::ZipArchive::new(cursor) .with_context(|| format!("Opening zip {}", path.display()))?; - for i in 0..archive.len() { - let mut entry = archive - .by_index(i) - .with_context(|| format!("Reading zip entry {} in {}", i, path.display()))?; - if entry.is_dir() { - continue; - } - if entry.encrypted() { + + // Collect metadata first to avoid borrow conflicts when retrying + // passwords on encrypted entries. + let entry_meta: Vec<_> = (0..archive.len()) + .filter_map(|i| { + let entry = archive.by_index_raw(i).ok()?; + if entry.is_dir() { + return None; + } + Some((i, entry.name().to_string(), entry.encrypted())) + }) + .collect(); + + for (i, member_name, encrypted) in entry_meta { + if encrypted { + let mut decrypted = None; + for pw in passwords { + if let Ok(mut entry) = archive.by_index_decrypt(i, pw.as_bytes()) { + let mut buf = Vec::new(); + if entry.read_to_end(&mut buf).is_ok() { + decrypted = Some((buf, pw.clone())); + break; + } + } + } + let Some((member_data, pw)) = decrypted else { + if verbose { + eprintln!( + "skipping encrypted zip member '{}' in {}", + member_name, + path.display() + ); + } + continue; + }; if verbose { eprintln!( - "skipping encrypted zip member '{}' in {}", - entry.name(), - path.display() + " zip member: {} ({} bytes, decrypted)", + member_name, + member_data.len() ); } - continue; - } - let member_name = entry.name().to_string(); - let mut member_data = Vec::new(); - entry.read_to_end(&mut member_data).with_context(|| { - format!("Reading zip member '{}' in {}", member_name, path.display()) - })?; - if verbose { - eprintln!( - " zip member: {} ({} bytes)", - member_name, - member_data.len() - ); + let logical_path = path.join(&member_name); + cands.push(Candidate { + path: logical_path, + data: member_data, + source: CandidateSource::Zip { + archive: path.clone(), + member: member_name, + password: Some(pw), + }, + coverage: Coverage::default(), + }); + } else { + let mut entry = archive.by_index(i).with_context(|| { + format!("Reading zip entry {} in {}", i, path.display()) + })?; + let mut member_data = Vec::new(); + entry.read_to_end(&mut member_data).with_context(|| { + format!("Reading zip member '{}' in {}", member_name, path.display()) + })?; + if verbose { + eprintln!( + " zip member: {} ({} bytes)", + member_name, + member_data.len() + ); + } + let logical_path = path.join(&member_name); + cands.push(Candidate { + path: logical_path, + data: member_data, + source: CandidateSource::Zip { + archive: path.clone(), + member: member_name, + password: None, + }, + coverage: Coverage::default(), + }); } - let logical_path = path.join(&member_name); - cands.push(Candidate { - path: logical_path, - data: member_data, - source: CandidateSource::Zip { - archive: path.clone(), - member: member_name, - }, - coverage: Coverage::default(), - }); } } else { // plain binary From 97995b0cd065c3f753ac9db689dffdd451b61d35 Mon Sep 17 00:00:00 2001 From: Pat Hawks Date: Sat, 4 Apr 2026 21:43:09 -0500 Subject: [PATCH 2/4] Remove unnecessary transitive deps zopfli and sha1_smol Use deflate-flate2 instead of deflate for the zip crate to avoid pulling in zopfli (compression) when we only need decompression. Replace sha1_smol with sha1, which is already a transitive dependency. --- Cargo.lock | 28 +--------------------------- Cargo.toml | 5 +++-- src/lib.rs | 6 +++--- src/test_support.rs | 6 +++--- 4 files changed, 10 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c067d0..ae8e2f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,7 +131,7 @@ dependencies = [ "lzma-rs", "nohash-hasher", "quick-xml", - "sha1_smol", + "sha1", "smallvec", "zip", "zstd", @@ -273,7 +273,6 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -600,12 +599,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - [[package]] name = "shlex" version = "1.3.0" @@ -907,33 +900,14 @@ dependencies = [ "sha1", "typed-path", "zeroize", - "zopfli", ] -[[package]] -name = "zlib-rs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" - [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 710ac1a..fb3ad44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,9 @@ crc32fast = "1.5" nohash-hasher = "0.2.0" quick-xml = "0.39.2" smallvec = "1.15" -sha1_smol = { version = "1.0", features = ["std"] } flate2 = "1" lzma-rs = "0.3" -zip = { version = "8", default-features = false, features = ["aes-crypto", "deflate"] } +zip = { version = "8", default-features = false, features = ["aes-crypto", "deflate-flate2"] } +sha1 = "0.10" zstd = "0.13" + diff --git a/src/lib.rs b/src/lib.rs index 622b11c..1506496 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ pub fn apply_found( where F: FnMut(&RomInfo, &[u8]) -> anyhow::Result<()>, { - use sha1_smol::Sha1; + use sha1::{Digest, Sha1}; use types::MatchedData; let MatchedData::Spec(ref spec) = found.data; @@ -53,12 +53,12 @@ where let mut hasher = Sha1::new(); hasher.update(roms[rid].header.as_ref().unwrap()); hasher.update(&bytes_owned); - hasher.digest().to_string() + format!("{:x}", hasher.finalize()) }) .as_str() } else { if sha1_cache.is_none() { - sha1_cache = Some(Sha1::from(&bytes_owned[..]).digest().to_string()); + sha1_cache = Some(format!("{:x}", Sha1::digest(&bytes_owned[..]))); } sha1_cache.as_ref().unwrap().as_str() }; diff --git a/src/test_support.rs b/src/test_support.rs index 1673a15..de41708 100644 --- a/src/test_support.rs +++ b/src/test_support.rs @@ -1,6 +1,6 @@ use crate::types::{Candidate, CandidateSource, Found, Heuristic, MatchRecord, Pending, RomInfo}; use crc32fast; -use sha1_smol::Sha1; +use sha1::{Digest, Sha1}; use std::collections::HashMap; pub fn make_rom(name: &str, data: &[u8]) -> RomInfo { @@ -9,7 +9,7 @@ pub fn make_rom(name: &str, data: &[u8]) -> RomInfo { game: String::new(), size: data.len(), crc32: crc32fast::hash(data), - sha1: Some(Sha1::from(data).digest().to_string()), + sha1: Some(format!("{:x}", Sha1::digest(data))), matched: false, unverified: false, region: None, @@ -25,7 +25,7 @@ pub fn make_rom_with_header(name: &str, header: &[u8], content: &[u8]) -> RomInf full.extend_from_slice(header); full.extend_from_slice(content); let full_crc = crc32fast::hash(&full); - let full_sha1 = Sha1::from(&full).digest().to_string(); + let full_sha1 = format!("{:x}", Sha1::digest(&full)); let content_crc = crate::utils::derive_content_crc(full_crc, header, content.len()); RomInfo { name: name.to_string(), From 208cde319481fa026996c62a4b78f9f6f9336ead Mon Sep 17 00:00:00 2001 From: Pat Hawks Date: Sat, 4 Apr 2026 22:05:33 -0500 Subject: [PATCH 3/4] Improve error handling for encrypted zip entries Propagate errors from by_index_raw instead of silently swallowing them. Distinguish wrong-password errors from real I/O failures when decrypting zip members, accounting for AES HMAC validation at end-of-stream. Update doc comment and note Python zipfile AES limitation in GEX output. --- src/gex.rs | 2 ++ src/utils.rs | 53 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/gex.rs b/src/gex.rs index d670d85..749c70a 100644 --- a/src/gex.rs +++ b/src/gex.rs @@ -120,6 +120,8 @@ class GeneratedTask(BaseTask): )); if let Some(pw) = password { let py_pw = py_str(pw); + // NOTE: zipfile only supports ZipCrypto; AES-encrypted + // archives need pyzipper or another AES-capable library. out.push_str(&format!( " with z.open({py_member}, pwd={py_pw}.encode()) as f:\n" )); diff --git a/src/utils.rs b/src/utils.rs index d9d027e..4957937 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -209,7 +209,9 @@ pub fn load_rom_list(dat_path: &Path, maybe_game: Option<&str>) -> anyhow::Resul /// Detect compressed archives by magic bytes and expand into `Candidate`s. /// /// - `.gz` / gzip magic (`1f 8b`): decompress into one candidate. -/// - `.zip` magic (`PK\x03\x04`): one candidate per unencrypted file member. +/// - `.zip` magic (`PK\x03\x04`): one candidate per file member. Encrypted members +/// are decrypted using the provided `passwords`; members that cannot be decrypted +/// are skipped with a warning (when `verbose` is set). /// - Everything else: one plain candidate. pub fn load_candidates_from_paths( paths: I, @@ -265,24 +267,55 @@ where // Collect metadata first to avoid borrow conflicts when retrying // passwords on encrypted entries. let entry_meta: Vec<_> = (0..archive.len()) - .filter_map(|i| { - let entry = archive.by_index_raw(i).ok()?; + .map(|i| { + let entry = archive.by_index_raw(i).with_context(|| { + format!("Reading zip entry metadata {} in {}", i, path.display()) + })?; if entry.is_dir() { - return None; + return Ok(None); } - Some((i, entry.name().to_string(), entry.encrypted())) + Ok(Some((i, entry.name().to_string(), entry.encrypted()))) }) + .collect::>>()? + .into_iter() + .flatten() .collect(); for (i, member_name, encrypted) in entry_meta { if encrypted { let mut decrypted = None; for pw in passwords { - if let Ok(mut entry) = archive.by_index_decrypt(i, pw.as_bytes()) { - let mut buf = Vec::new(); - if entry.read_to_end(&mut buf).is_ok() { - decrypted = Some((buf, pw.clone())); - break; + match archive.by_index_decrypt(i, pw.as_bytes()) { + Err(zip::result::ZipError::InvalidPassword) => continue, + Err(e) => { + return Err(anyhow::Error::from(e).context(format!( + "Reading encrypted zip entry '{}' in {}", + member_name, + path.display() + ))); + } + Ok(mut entry) => { + let mut buf = Vec::new(); + match entry.read_to_end(&mut buf) { + Ok(_) => { + decrypted = Some((buf, pw.clone())); + break; + } + // AES HMAC validation failure at end-of-stream + Err(e) + if e.kind() == std::io::ErrorKind::InvalidData + || e.kind() == std::io::ErrorKind::InvalidInput => + { + continue; + } + Err(e) => { + return Err(anyhow::Error::from(e).context(format!( + "Reading zip member '{}' in {}", + member_name, + path.display() + ))); + } + } } } } From c04137fe4281813d3b390122f520a61697209b9e Mon Sep 17 00:00:00 2001 From: Pat Hawks Date: Sat, 4 Apr 2026 22:14:08 -0500 Subject: [PATCH 4/4] Add --password-file, tests, and cleanup for zip password support Add --password-file option to avoid exposing passwords in ps output and shell history. Emit AES limitation note into generated Python output instead of only as a Rust source comment. Deduplicate candidate construction between encrypted and unencrypted zip branches. Add tests for plain zip extraction, correct/wrong/missing password handling. --- src/gex.rs | 8 ++- src/main.rs | 30 +++++++-- src/utils.rs | 185 ++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 176 insertions(+), 47 deletions(-) diff --git a/src/gex.rs b/src/gex.rs index 749c70a..8d3b0cd 100644 --- a/src/gex.rs +++ b/src/gex.rs @@ -120,8 +120,12 @@ class GeneratedTask(BaseTask): )); if let Some(pw) = password { let py_pw = py_str(pw); - // NOTE: zipfile only supports ZipCrypto; AES-encrypted - // archives need pyzipper or another AES-capable library. + out.push_str( + " # NOTE: zipfile only supports ZipCrypto; AES-encrypted\n", + ); + out.push_str( + " # archives need pyzipper or another AES-capable library.\n", + ); out.push_str(&format!( " with z.open({py_member}, pwd={py_pw}.encode()) as f:\n" )); diff --git a/src/main.rs b/src/main.rs index e3e9d90..cd8ce2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![deny(warnings)] +use anyhow::Context; use chisel::utils; use clap::Parser; @@ -65,14 +66,32 @@ struct Opt { #[arg(long)] password: Vec, + /// Read passwords from a file, one per line + #[arg(long)] + password_file: Option, + /// Verbose logging (-v) #[arg(short, long)] verbose: bool, } -fn run_extract(opt: &Opt, spec: chisel::types::ExtractionSpec) -> anyhow::Result<()> { +fn load_passwords(opt: &Opt) -> anyhow::Result> { + let mut passwords = opt.password.clone(); + if let Some(ref path) = opt.password_file { + let content = std::fs::read_to_string(path) + .with_context(|| format!("Reading password file {}", path.display()))?; + passwords.extend(content.lines().filter(|l| !l.is_empty()).map(String::from)); + } + Ok(passwords) +} + +fn run_extract( + opt: &Opt, + passwords: &[String], + spec: chisel::types::ExtractionSpec, +) -> anyhow::Result<()> { std::fs::create_dir_all(&opt.output_dir)?; - let cands = utils::load_candidates_from_paths(&opt.input_files, &opt.password, opt.verbose)?; + let cands = utils::load_candidates_from_paths(&opt.input_files, passwords, opt.verbose)?; for cand in &cands { let mut s = spec.clone(); if s.size == 0 { @@ -105,8 +124,10 @@ fn main() -> anyhow::Result<()> { anyhow::bail!("--dat and --spec are mutually exclusive"); } + let passwords = load_passwords(&opt)?; + if let Some(spec) = opt.spec.clone() { - return run_extract(&opt, spec); + return run_extract(&opt, &passwords, spec); } let dat = opt @@ -114,8 +135,7 @@ fn main() -> anyhow::Result<()> { .as_ref() .ok_or_else(|| anyhow::anyhow!("either --dat or --spec is required"))?; let mut roms = load_rom_list(dat, opt.game.as_deref())?; - let mut cands = - utils::load_candidates_from_paths(&opt.input_files, &opt.password, opt.verbose)?; + let mut cands = utils::load_candidates_from_paths(&opt.input_files, &passwords, opt.verbose)?; if opt.gex.is_none() { std::fs::create_dir_all(&opt.output_dir)?; diff --git a/src/utils.rs b/src/utils.rs index 4957937..1b5637f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -282,7 +282,7 @@ where .collect(); for (i, member_name, encrypted) in entry_meta { - if encrypted { + let (member_data, password) = if encrypted { let mut decrypted = None; for pw in passwords { match archive.by_index_decrypt(i, pw.as_bytes()) { @@ -319,7 +319,7 @@ where } } } - let Some((member_data, pw)) = decrypted else { + let Some((data, pw)) = decrypted else { if verbose { eprintln!( "skipping encrypted zip member '{}' in {}", @@ -329,51 +329,40 @@ where } continue; }; - if verbose { - eprintln!( - " zip member: {} ({} bytes, decrypted)", - member_name, - member_data.len() - ); - } - let logical_path = path.join(&member_name); - cands.push(Candidate { - path: logical_path, - data: member_data, - source: CandidateSource::Zip { - archive: path.clone(), - member: member_name, - password: Some(pw), - }, - coverage: Coverage::default(), - }); + (data, Some(pw)) } else { let mut entry = archive.by_index(i).with_context(|| { format!("Reading zip entry {} in {}", i, path.display()) })?; - let mut member_data = Vec::new(); - entry.read_to_end(&mut member_data).with_context(|| { + let mut buf = Vec::new(); + entry.read_to_end(&mut buf).with_context(|| { format!("Reading zip member '{}' in {}", member_name, path.display()) })?; - if verbose { - eprintln!( - " zip member: {} ({} bytes)", - member_name, - member_data.len() - ); - } - let logical_path = path.join(&member_name); - cands.push(Candidate { - path: logical_path, - data: member_data, - source: CandidateSource::Zip { - archive: path.clone(), - member: member_name, - password: None, - }, - coverage: Coverage::default(), - }); + (buf, None) + }; + if verbose { + eprintln!( + " zip member: {} ({} bytes{})", + member_name, + member_data.len(), + if password.is_some() { + ", decrypted" + } else { + "" + } + ); } + let logical_path = path.join(&member_name); + cands.push(Candidate { + path: logical_path, + data: member_data, + source: CandidateSource::Zip { + archive: path.clone(), + member: member_name, + password, + }, + coverage: Coverage::default(), + }); } } else { // plain binary @@ -700,6 +689,7 @@ pub fn decode_lz77(data: &[u8]) -> Option<(Vec, usize)> { #[cfg(test)] mod tests { use super::*; + use std::io::Write; #[test] fn parse_hex_header_basic() { @@ -843,4 +833,119 @@ mod tests { assert!(roms[0].matched); assert_eq!(records[0].header.as_deref(), Some(header.as_slice())); } + + /// Helper: create a zip archive in memory with one stored (uncompressed) file. + fn make_zip(name: &str, content: &[u8]) -> Vec { + let buf = Vec::new(); + let mut writer = zip::ZipWriter::new(std::io::Cursor::new(buf)); + let opts = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored); + writer.start_file(name, opts).unwrap(); + writer.write_all(content).unwrap(); + writer.finish().unwrap().into_inner() + } + + /// Helper: create an AES-256 encrypted zip archive in memory. + fn make_encrypted_zip(name: &str, content: &[u8], password: &str) -> Vec { + let buf = Vec::new(); + let mut writer = zip::ZipWriter::new(std::io::Cursor::new(buf)); + let opts = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored) + .with_aes_encryption(zip::AesMode::Aes256, password); + writer.start_file(name, opts).unwrap(); + writer.write_all(content).unwrap(); + writer.finish().unwrap().into_inner() + } + + /// Helper: write bytes to a temp file and return the path. + fn write_temp(dir: &std::path::Path, name: &str, data: &[u8]) -> std::path::PathBuf { + let p = dir.join(name); + std::fs::write(&p, data).unwrap(); + p + } + + #[test] + fn zip_plain_candidate() { + let dir = std::env::temp_dir().join("chisel_test_zip_plain"); + std::fs::create_dir_all(&dir).unwrap(); + let zip_bytes = make_zip("rom.bin", b"hello"); + let zip_path = write_temp(&dir, "test.zip", &zip_bytes); + + let cands = load_candidates_from_paths(&[&zip_path], &[], false).unwrap(); + assert_eq!(cands.len(), 1); + assert_eq!(cands[0].data, b"hello"); + assert!(matches!( + &cands[0].source, + CandidateSource::Zip { password: None, .. } + )); + + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn zip_encrypted_correct_password() { + let dir = std::env::temp_dir().join("chisel_test_zip_enc_ok"); + std::fs::create_dir_all(&dir).unwrap(); + let zip_bytes = make_encrypted_zip("secret.bin", b"payload", "hunter2"); + let zip_path = write_temp(&dir, "enc.zip", &zip_bytes); + + let passwords = vec!["hunter2".to_string()]; + let cands = load_candidates_from_paths(&[&zip_path], &passwords, false).unwrap(); + assert_eq!(cands.len(), 1); + assert_eq!(cands[0].data, b"payload"); + assert!(matches!( + &cands[0].source, + CandidateSource::Zip { + password: Some(_), + .. + } + )); + + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn zip_encrypted_wrong_password_skipped() { + let dir = std::env::temp_dir().join("chisel_test_zip_enc_wrong"); + std::fs::create_dir_all(&dir).unwrap(); + let zip_bytes = make_encrypted_zip("secret.bin", b"payload", "correct"); + let zip_path = write_temp(&dir, "enc.zip", &zip_bytes); + + let passwords = vec!["wrong1".to_string(), "wrong2".to_string()]; + let cands = load_candidates_from_paths(&[&zip_path], &passwords, false).unwrap(); + assert_eq!(cands.len(), 0); + + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn zip_encrypted_no_passwords_skipped() { + let dir = std::env::temp_dir().join("chisel_test_zip_enc_none"); + std::fs::create_dir_all(&dir).unwrap(); + let zip_bytes = make_encrypted_zip("secret.bin", b"payload", "pw"); + let zip_path = write_temp(&dir, "enc.zip", &zip_bytes); + + let cands = load_candidates_from_paths(&[&zip_path], &[], false).unwrap(); + assert_eq!(cands.len(), 0); + + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn zip_encrypted_multiple_passwords_finds_correct() { + let dir = std::env::temp_dir().join("chisel_test_zip_enc_multi"); + std::fs::create_dir_all(&dir).unwrap(); + let zip_bytes = make_encrypted_zip("secret.bin", b"data", "third"); + let zip_path = write_temp(&dir, "enc.zip", &zip_bytes); + + let passwords: Vec = ["first", "second", "third"] + .iter() + .map(|s| s.to_string()) + .collect(); + let cands = load_candidates_from_paths(&[&zip_path], &passwords, false).unwrap(); + assert_eq!(cands.len(), 1); + assert_eq!(cands[0].data, b"data"); + + std::fs::remove_dir_all(&dir).ok(); + } }