rawzip-0.4.4/.cargo_vcs_info.json0000644000000001360000000000100123550ustar { "git": { "sha1": "6d7b20aa693a54bb3f65dac0cef67138fff3f2eb" }, "path_in_vcs": "" }rawzip-0.4.4/Cargo.lock0000644000000402140000000000100103310ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "cc" version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "env_logger" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ "log", "regex", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "filetime" version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys", ] [[package]] name = "flate2" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn 2.0.103", ] [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" [[package]] name = "indexmap" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "jiff" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "portable-atomic", "portable-atomic-util", ] [[package]] name = "jiff-static" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", "syn 2.0.103", ] [[package]] name = "jobserver" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libredox" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ "bitflags", "libc", "redox_syscall", ] [[package]] name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro-crate" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quickcheck" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger", "log", "rand", ] [[package]] name = "quickcheck_macros" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rawzip" version = "0.4.4" dependencies = [ "filetime", "flate2", "jiff", "paste", "quickcheck", "quickcheck_macros", "rstest", "zstd", ] [[package]] name = "redox_syscall" version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rstest" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" dependencies = [ "futures-timer", "futures-util", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" dependencies = [ "cfg-if", "glob", "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", "syn 2.0.103", "unicode-ident", ] [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "semver" version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "toml_datetime" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" [[package]] name = "toml_edit" version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "unicode-ident" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] [[package]] name = "zstd" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", ] rawzip-0.4.4/Cargo.toml0000644000000034620000000000100103600ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70" name = "rawzip" version = "0.4.4" authors = ["Nick Babcock "] build = false include = [ "src/**/*.rs", "/examples", "LICENSE.txt", "assets", "tests", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A Zip archive reader and writer" readme = "README.md" keywords = [ "zip", "archive", ] categories = [ "filesystem", "compression", "parser-implementations", ] license = "MIT" repository = "https://github.com/nickbabcock/rawzip" [lib] name = "rawzip" path = "src/lib.rs" [[example]] name = "big" path = "examples/big.rs" [[example]] name = "extract" path = "examples/extract.rs" [[example]] name = "list" path = "examples/list.rs" [[example]] name = "read" path = "examples/read.rs" [[example]] name = "write" path = "examples/write.rs" [[test]] name = "it" path = "tests/it/main.rs" [dependencies] [dev-dependencies.filetime] version = "0.2" [dev-dependencies.flate2] version = "1.0.35" [dev-dependencies.jiff] version = "0.2.15" default-features = false [dev-dependencies.paste] version = "1.0" [dev-dependencies.quickcheck] version = "1.0.3" [dev-dependencies.quickcheck_macros] version = "1.0.0" [dev-dependencies.rstest] version = "0.24.0" [dev-dependencies.zstd] version = "0.13.3" rawzip-0.4.4/Cargo.toml.orig000064400000000000000000000012531046102023000140350ustar 00000000000000[package] name = "rawzip" version = "0.4.4" authors = ["Nick Babcock "] edition = "2021" description = "A Zip archive reader and writer" repository = "https://github.com/nickbabcock/rawzip" readme = "README.md" license = "MIT" keywords = ["zip", "archive"] categories = ["filesystem", "compression", "parser-implementations"] include = ["src/**/*.rs", "/examples", "LICENSE.txt", "assets", "tests"] rust-version = "1.70" [dependencies] [dev-dependencies] filetime = "0.2" flate2 = { version = "1.0.35" } jiff = { version = "0.2.15", default-features = false } paste = "1.0" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" rstest = "0.24.0" zstd = "0.13.3" rawzip-0.4.4/LICENSE.txt000064400000000000000000000017761046102023000130030ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.rawzip-0.4.4/README.md000064400000000000000000000147551046102023000124400ustar 00000000000000# Rawzip A low-level Zip archive reader and writer. Pure Rust. Zero dependencies. Zero unsafe. Fast. ## Use Cases In its current state, rawzip should not be considered a general purpose Zip library like [zip](https://crates.io/crates/zip), [rc-zip](https://crates.io/crates/rc-zip), or [async_zip](https://crates.io/crates/async-zip). Instead, it was born out of a need for the following: - **Efficiency**: Only pay for what you use. Rawzip does not materialize the central directory when a Zip archive is parsed, and instead provides a lending iterator through the listed Zip entries. For a Zip file with 200k entries, this results in up to 2 orders of magnitude performance increase, as other Zip libraries need 200k+ allocations to rawzip's 0. If storage of all entries is needed for further processing, callers are able to amortize allocations for arbitrary length fields like file names. - **Bring your own dependencies**: Rawzip pushes the compression responsibility onto the caller. Rust has a myriad of high quality compression libraries to choose from. For instance, just deflate has a half dozen implementations ([#1](https://crates.io/crates/libdeflater), [#2](https://crates.io/crates/miniz_oxide), [#3](https://crates.io/crates/zune-inflate), [#4](https://crates.io/crates/libz-ng-sys), [#5](https://crates.io/crates/zlib-rs), [#6](https://crates.io/crates/cloudflare-zlib-sys)). This allows Rawzip to reach maturity easier and be passively maintained while letting downstream users pick the exact compressor best suited to their needs. The Zip file specification does not change frequently, and the hope is this library won't either. ## Features: - Pure Rust. Zero dependencies. Zero unsafe. Fast. - Read and write Zip and large Zip64 archives (100k+ entries, >100 GB archives, >5 GB entry) - Facilitates concurrent streaming decompression - Zero allocation and zero copy when reading from a byte slice ## Example ```rust use std::io::Read; // Let's create a Zip archive with a single file, "file.txt", containing the text "Hello, world!" // and read it back out. let data = b"Hello, world!"; // Create a new Zip archive in memory. let mut output = Vec::new(); let mut archive = rawzip::ZipArchiveWriter::new(&mut output); // Start of a new file in our zip archive with deflate compression. let (mut entry, config) = archive.new_file("file.txt") .compression_method(rawzip::CompressionMethod::Deflate) .start()?; // Create the deflate compressor let encoder = flate2::write::DeflateEncoder::new(&mut entry, flate2::Compression::default()); // Wrap the compressor in a data writer, which will track information for the // Zip data descriptor (like uncompressed size and crc). let mut writer = config.wrap(encoder); // Copy the data to the writer. std::io::copy(&mut &data[..], &mut writer)?; // Finish the file, which will return the finalized data descriptor let (_, descriptor) = writer.finish()?; // Write out the data descriptor and return the number of bytes the data compressed to. let compressed = entry.finish(descriptor)?; // Finish the archive, which will write the central directory. archive.finish()?; // Now it is time to read back what we've written! Here we are reading from // a slice, but there's another set of API that take advantage of reading from a file. let archive = rawzip::ZipArchive::from_slice(&output)?; // Rawzip does not materialize the central directory when a Zip archive is parsed, // so we need to iterate over the entries to find the one we want. let mut entries = archive.entries(); // Get the first (and only) entry in the archive. let entry = entries.next_entry()?.unwrap(); // While we can access the raw bytes of the file name, let's use the normalized path // for demonstration purposes. assert_eq!(entry.file_path().try_normalize()?.as_ref(), "file.txt"); // Assert the compression method. assert_eq!(entry.compression_method(), rawzip::CompressionMethod::Deflate); // Assert the uncompressed size hint. Be warned that this may not be the actual, // uncompressed size for malicious or corrupted files. assert_eq!(entry.uncompressed_size_hint(), data.len() as u64); // Before we need to access the entry's data, we need to know where it is in the archive. let wayfinder = entry.wayfinder(); let local_entry = archive.get_entry(wayfinder)?; let mut actual = Vec::new(); let decompressor = flate2::bufread::DeflateDecoder::new(local_entry.data()); // We wrap the decompressor in a verifying reader, which will verify the size and CRC of // the decompressed data once finished. let mut reader = local_entry.verifying_reader(decompressor); std::io::copy(&mut reader, &mut actual)?; // Assert the data is what we wrote. assert_eq!(&data[..], actual); Ok::<(), Box>(()) ``` ## Security Zip files have a checkered past with maliciously crafted zips causing major headaches. By virtue of rawzip being a minimal library, several mitigations become the responsibility of the consuming application. What rawzip provides: - Memory safety - Structural validation of EOCD, central directory, and local file headers - An opt-in file path normalization to protect against zip slips - An opt-in CRC and size verification of inflated data What consumers must handle: - Zip bombs by implementing max compression ratios, max file sizes, and checks for overlapping file data - Symlink attacks with safe file system operations - Zip quines and potentially infinite recursion by limiting the amount of nesting - Multiple file entries with the same file name - Unexpected central directory entry count. When the central directory iterator ends or errors, check against the number of expected entries to know whether an error should be raised or suppressed. ## Benchmarks ![Chart depicting rawzip performance of parsing through the central directory compared to other Rust zip implementations (view image on github if reading on docs.rs)](assets/rawzip-performance-comparison.png) ![Chart depicting rawzip performance of writing a Zip file (view image on github if reading on docs.rs)](assets/rawzip-write-performance-comparison.png) If you want to rip through zips as fast as possible, rawzip is for you. Doesn't matter if the zips are 100 GB+ with 200k entries, nothing will be faster. Reproduce the benchmarks with the following command: ```bash (cd compare && cargo clean && cargo bench) find ./compare/target -wholename "*/new/raw.csv" -print0 | xargs -0 xsv cat rows > assets/rawzip-benchmark-data.csv ``` The data can be analyzed with the R script found in the assets directory. Keep in mind, benchmarks will vary by machine. rawzip-0.4.4/assets/analysis.R000064400000000000000000000074701046102023000144250ustar 00000000000000library(scales) library(tidyverse) library(readr) library(RColorBrewer) df <- read_csv("./rawzip-benchmark-data.csv") function_names <- c("rawzip", "async_zip", "rc_zip", "zip") # Calculate throughput in MB/s (bytes per nanosecond * 1000 to get MB/s) df <- df %>% mutate( fn = `function`, throughput_mbps = (throughput_num / 1e6) / (sample_measured_value / iteration_count / 1e9), is_rawzip = fn == "rawzip", # Create a factor for consistent ordering in legend fn_factor = factor(fn, levels = function_names) ) # Filter data for parse group parse_df <- df %>% filter(group == "parse") # Define colors for each zip reader using RColorBrewer Set1 palette pal <- brewer.pal(4, "Set1") colors <- setNames(pal, function_names) # Calculate mean throughput by implementation for parse group parse_mean_throughput <- parse_df %>% group_by(fn_factor) %>% summarise(mean_throughput_mbps = mean(throughput_mbps), .groups = 'drop') # Parse performance graph parse_p <- ggplot(parse_mean_throughput, aes(x = fn_factor, y = mean_throughput_mbps, fill = fn_factor)) + geom_col(width = 0.7) + # Color scale scale_fill_manual(values = colors, guide = "none") + # Axis formatting scale_y_continuous( "Throughput (MB/s)", breaks = pretty_breaks(8), labels = comma_format() ) + scale_x_discrete("Zip Reader Implementation") + # Theme and labels theme_minimal() + theme( plot.title = element_text(size = 14, face = "bold"), plot.subtitle = element_text(size = 12), axis.title.x = element_text(margin = margin(t = 15)) ) + labs( title = "Rust Zip Reader Performance Comparison", subtitle = "Mean central directory parsing throughput (higher is better)", caption = "Data from rawzip benchmark suite" ) print(parse_p) ggsave('rawzip-performance-comparison.png', plot = parse_p, width = 8, height = 5, dpi = 150) # Filter data for write group and prepare for hierarchical analysis write_df <- df %>% filter(group == "write") %>% mutate( # Extract feature type (extra_fields or minimal) and implementation feature_type = str_extract(fn, "^[^/]+"), impl_name = str_extract(fn, "(?<=/).*$"), # Create factors for consistent ordering feature_factor = factor(feature_type, levels = c("minimal", "extra_fields")), impl_factor = factor(impl_name, levels = c("rawzip", "zip", "async_zip")) ) # Calculate mean throughput by feature type and implementation write_detailed_throughput <- write_df %>% group_by(feature_factor, impl_factor) %>% summarise(mean_throughput_mbps = mean(throughput_mbps), .groups = 'drop') # Create a combined write performance graph write_detailed_p <- ggplot(write_detailed_throughput, aes(x = feature_factor, y = mean_throughput_mbps, fill = impl_factor)) + geom_col(position = "dodge", width = 0.7) + # Color scale with rawzip and zip colors scale_fill_manual(values = c("rawzip" = colors[["rawzip"]], "zip" = colors[["zip"]], "async_zip" = colors[["async_zip"]]), name = "Implementation") + # Axis formatting scale_y_continuous( "Throughput (MB/s)", breaks = pretty_breaks(8), labels = comma_format() ) + scale_x_discrete("Zip Format Type", labels = c("minimal" = "Minimal Format", "extra_fields" = "With Extra Fields")) + # Theme and labels theme_minimal() + theme( plot.title = element_text(size = 14, face = "bold"), plot.subtitle = element_text(size = 12), axis.title.x = element_text(margin = margin(t = 15)), legend.position = "bottom" ) + labs( title = "Zip Writer Performance", subtitle = "Mean writing throughput by format type and implementation (higher is better)", caption = "Data from rawzip benchmark suite" ) print(write_detailed_p) ggsave('rawzip-write-performance-comparison.png', plot = write_detailed_p, width = 8, height = 5, dpi = 150) rawzip-0.4.4/assets/crc32-not-streamed.zip000064400000000000000000000004721046102023000165120ustar 00000000000000PK eh@e2~foo.txtUT UYOUYOux foo PK fh@鳢bar.txtUT UYOUYOux bar PK eh@e2~foo.txtUTUYOux PK fh@鳢Ebar.txtUTUYOux PKrawzip-0.4.4/assets/filename_mismatch_test.zip000064400000000000000000000003501046102023000176750ustar 00000000000000PK6 [FgSG XxfԬ2g,d"י5OksflyKZnΒ{gIey&\a6Vl3jd #VW.:!ծk&\1(]e`K{YZA6Z|Om(ᄀ1X]í廯9:se1y n.@&}#Xϵ@G-5;xYJ]_rcKa [ ٖe [L8tW%wE|t"#X.}ܹ2 #oC'r̩&Q{%` xdaÜypW KŽ^Lΰ#at0Qxz90ce`dPX_$D#T̩ADGP @Y{E&(V#U$jT+48 3de_Nrp7-yD岔Vll,Uʼn%@ E{]ݫ=#F+:`ݿ16T&Jv Ԍ"*.h(5u'v;Dh,18ھ/0q0/a7ľT7xVhHwЄxI7˷777dA˅BJx&!jn5jW=,=E!;utF4P3o (U/zj EX~JjJ mڴ)L&S{5s̳gϖN<>l&MUV: g?BBBj׮ݯ_ŋ߸qѕuslY$%NgnذaȑuV0qDD_,J_Yb?[n}77ntRБ#GΟ?ɓ999p$Iofz&Nwߝ={6;;;''… 6m6mZZ^BoG CBX6mꫯz; ݡ]SPP0iҤ 6觜:ujƍ%pS^^ސ!CfΜQTٳg'$$flM>BQ]wݥ)I$I;vX,… N6-**K1jRRC:(p~M388*RvhРAVҥKV ̘18u:<<|UT)ςkO_[-VZRR 6̙3g p 'l/]vڄu~~ѣG 9QQQvwDަMO???ʟW\Q^Ι}v{=ׯqqq:u*SR￯^r҉򜜜e˖'&&& rʍ77n?l[=r'M$زeKnCBBZlo;[ ܹŋ6q9|A!DRR҄ bcc4hп_~n=!\2wWR%((Yf k֬)Sܮ^{My㓘8~x%Bw}[nTRFjA6;Zl+بrjf|}}_u>|~EUm?XeѢEtT'(ĨTTǎSFEElRU_{'l X,ҥKK"¢:ܹsvvUFJ/==ݩU.~ !Zn=ohQ%ǎ=*g[г ǝfuE%g.W;PKKfr9ܵl6ѣSYS"&EKe#ڵSH5j 3ghĝj$iĈ+P}YfM;t?AAAkЩƍ~B>i޸qcID%!ܳgmݺ|kBV=ud?Ս.nY˖-\NEQ~!8{lQ%/^.gȵ;jultxrz@B.ǙIffԷOYZB?k5kַ~y^llذAy߀O:ljժ/^*%!UVmĉS#ӧ`ܩv#O!/=q\^݅͝;/\lո;׮]_|Y}e288~gU ٨gx&Iڴi;boVK{䓅 o^ܹs\u!֭۬Yl{]v3fx衇ԧZR>OVVcΜ9SZS~yYfΜ9ΦԃU)S(ۉr\ؖk-rNuE=iu !t:3)V˖-oܸ׵p)S߲xbe̙3'YYY7V6?Ә6n899Yyל9sUAEBwK.ʪ>@ɓ'+ƍ'/|衇+WTWRuVyڈ/V[ QYUZuӦM֛PetʕCBB%Nr{*ɴ}veUAA]oݺs 77P;vbW} e*={޼yS!C(NeSonݺW\QV[NYմiSyUÇ+۴i㸰-Zг fuE={Kt]3[oo\Mx eyڵ׬Y|X^57nW)}aʪhey^ ,ڣ:uȫ ]hW)\g[gȵj:6:{<ȗBcN[oխ[*rmݦйs={w}w۷7ʟ#F:u[ Re !|||| 0um>|Xd2Y5$IA$!DJJٳgׯ.s=zرc{9}'Ͻ fU`ذa;vZBTV[#nvyHD(l6**[nݭ=cǟ[N̂OSJppUE?QHSv!uݭseʰX ].~ @j>4p;{S5k2dԩS=5qܶmwUug￝ڠSծ>ڴi-_tI>sȘ?w}wq\~]>BXPµ+WV{J޽W\)?j7iQOvg7])s Q J@@gd_UVZkn6Zddj&-oA4©Towx~ju !$3:uuByyy[lq3!TҞ={.]ZM'HNN^hG};\gذaE3""o)j{lOȄnB[n9Av/233MuU=rsӧ=^L!OJfM&5hРUV%AAAuݻRDBC{eÝȩ2wvBRkѢҥK)))QQQvK^pa޽k$~ z;Tsx^֪P`zi;ڛLIEm3 `'N8z諯ڮ];3t`u?F_|aSf6={VyeEU{DDA2#?~r^?iiik׮XZ5@~~~rr=Uw}_VX1w\ :[o껋I9=\Vreŋ`c$魷*<瞓~)f9u3ό7nxի{ɒ%ϟ-S x)fux^֪Pa6~Ⱥu*c9rdɒ%v־}f͚q޽[d_ ~[}>3Zި̙3^/*?uƞ={zSV{Օs/R!C$$$$$$=PPM}3ի>U2|H_~og*v=+/^Z꧟~R,,,>}zVVghh՜rÅ@2ΥK?wر`իW^zϞ=mg}VyݥK۷h8tP߾}Չѣe|JY2XgK+''Gyg2z5|5k>}o _۶m_fs=f͚հaܤ-[([kР2 /6k׮͜9jFUTjժ)iqرVZ%$$TR%11Q 4HCMEU)SƏ/0a׻v횗w{O0;c :umӣGAEEE/˗/'N\dׯyÆ "8uҏpĈsΕo{7nYFybMRJP=\3v^{Mիm۶_v}zbzgR?ZlrVޱ<$vQk׮Ӵ|:Pfr_ʪʕ+WշS`[m۶UrɹoMoMZг dz <R8u=Ҡ%{^{56kLÿ/_dŠh?~2 QLIIq<`WŬٱcG=AEUhܰaSݻw+WWXa[a֬YƍS;̙3m4-+:UJwu׶m34p۷k=\3iҤE s=ׯW7߼8 &V,S-SjLU)"vFZiTe5չW^ycǎzNNݻ?ne˖ݻyQQQ;w>}ٳgmc8p@>}WܫW/;UW;u$IҊ+ڶmԬY_|133S[K']njlѢѣc˗/?7^a8 IҮ]:tH;w&$$ԨQϯz#GQQQo1c#pϜ9cUի&)**~  zh߾%Kn޼iq6lmѬYzܹs2?bРA$[SNaaa:uo4ӧnjSvm0ŴD1CZ5k(o9rdNNN׮]*PKlذAѩS'-;w#F4j(00q/RNNN߾}WVQ||>u\EU,))'ܹsʕccc{ڶm_|v*y !n6rH!?pڵ'xBvǏmkv4//oٲe:un֬ɓmnFڿ'O4hPժU6mh""Uʕ+۶mѱcǯZKZ?B 0*6ƹlgT{;8w{[׮]Сmju)Zi?~ ^.,Yrҥc֬Y3((u&Lp c)U$6?g$nQtuȣ?СC>}e˖rd4ߧ:5kvyvԣ\+%Μ9SN|_}ՐÇ 򾾾ʇZWcY`A߾}[:wܷo]v)o1bĈ#ԬYS@=!|7}|5sekmڴn۷j/$IڲeKPP\^!DVRRR͛*UBݻתN+WtPorF!!!={4hP5SvۆӮ];uvB-ߋk׆Xg޽;u$&w1e}֭lw)vǵS =Π1N.͞=[ʩ<^KOm/vɩӫU&KegwnS:h_T_-#!t:!O Vet"رcmBh6[n-hݺ?iӦ yJḻcʿ޽QFB'*'Xv:u}SlBhZXv(##jժBm߿?///%%O>78a„b$iذaB'|R]'{۽s-Z\xQ^7n8!DÆ ]ƶ Bb|$d]h|Mի={B[Y{B}Oǯ38ViQ;Bg_sJ=~ڲ ^.9!4_~ezzzFFƦMԩ#LOOwv& IDATw"vŋB͛;Y9BŒ @H.]2f6!O4h`˗哿?\^? {7}BQB>L]2??_yX}ք sr_|ȝ3ia јiFѯ_?O<Ƅ6H%{U3!ׯ~\E&ĉ$oBԬYS]ǏB..{z6*5nOӧUPxܹ٪B-{=~흡B9]rz9%QK.?ml}KիW<˰wGҜzjԨa0WVvcBCC{#[֮]kX &*'OV/~پ}SN'Nz2>??_a;݅OBBzoDD֭Bgjg~kժlnƣ] pBKMM|WV͙3G}SAj%n<3<^ޭ[:f儩8={ ONN !yѣmݦ^b0{D]Vleo.gWP$-{9Κ5;f0M^RfM^DqZ\,T{Z:CIX,/^4Lr`E{%Y-!Ν;TZuBe.%gwG VQ5$IQF=iСׯ_frC<!C?y/B(?-)'W^=zÇwUY`:ulӘbYKOO~ByrCѡC:;(FwB;uRJ͚5ZUZӧOZ^vBVZٮjٲ}B>~_~)?wߥ7l;tI&E-wV8jN4i۶mׯVڀw޾}ƍkPhɷn2 Vl[Y5j԰'d2ٖqg[X.tpu\J+J=~jE!߷iE>DȇMh*B\|EΆ:ABI}TҶm/^gϞ;-/f\ޱcǂ <"0LM4i۶Ν;mߨ $!|"w9?*ı˗/ !ۖlhImXKګVjj͛7իW][fM!)6l؇~vwyh4ZJh<(RBB͹s,|ܩV o߾? /k׮?\״VZC={퉸b?J*edd8ނQQQKZ lK$.5v-q:%55U!?WiK{%gE}gECeZZZ~~(,,Lq gcQO ׯ_~~k׮$)!!Ka;v(%ׯ_&&&i߷o_VV֑#Glo2tGtt"//G ;JNNVN]Rc-iv[$W\]+/xtk׮իWrʮ]RRRlb2OfΝ;'g[n;wޱcGJJʺuz;… of۶m]@+|k*77733ӝk$nU҅V%_V4'ɅJ=~3ߗK..Iw*׬Y#TUX~ö\r)*_X,?͛' zGZuرcΝ;Kg+D\!;;رcVΞ=ZkI{2 4B矶kvڏ?X8s%׮][x_=[ooԨ/^޲eK9]&x7ܸ;|FyKh"%Z:8b4k֬i6]FJJ޽{vFL%;ܦ|㡹@ZP>B3g*m(((kn߾}$>|XyfY.ٽ{w!1yw!Dddv'Vɷ&?l;#i#F={V7oFd2/FT<)6lPvqډ{};f63f! )v גj[EiiicK;v? W\)_u?~|qիWOy"b)駟ݼysBXYf)))/`0LOQoO8!O&#$%%Ity_qhƒ$q)^UR{++ǙsH9n8$بL}Oǯ3hnE<IԒGl k%4֭[5jdRMiSU$մiS-EB <@?P&$ >>>Su$i߾}AAA5Oa/_.g2 P>L[Oa0x u1;OSnmƍޮB4&$GO-[Ǘw!!^Kګݶ$Iڴiq֭[߸qCKٳg}'15J>5T^}JIC/_:?C&>g?w ލ% %I4h*$$~Fh4.[L PhiSgB-8v{)nIג-/v ŋw222R_ݑhJ =RE!ӦMR[2ys[~\\\Cշjرc=\n*WҡCz8p`qqqO=ɓ'ǍaÆ!Ct޽nݺ3fL˖-Fc 믿ʗ˜)n˖-M&SZZU+l۶mĈ7_9svݡC#Gʗ84V*ӧѣG'Oܱcǀڵkwݽ{#*N/*ÇyjժnzҤIR ~hǏoڴi```~Ç6Q]cƌٽ{СCccc}||:uyccTV۵wchoիW/\y%55{&L۷ȑ#!ES{ׅ(Z*-w/:u9rd߾}Çg֫Woʔ)ǏMmJ =RErOPnTe4LJ˗ӵkW_/ e-!>GyyvskԨQW^xԩSK4pyn ̝=- aĈ_|ř3gK~CUXXXf͈'N3R+Tx\!ԑի4d_e-!-[&>>Æ kԨ6##㣏>:~lnԨѣ>ZJ6帰;kB2H޳gς bcc΂1==7hР"33s֬Yiii]v5L;w7l7帰;k+䚵ʨJ$I7nBL&oxlNKKBDFF oxAAAAFF`v,(=޼BgEGG[>>>Bo„ |ͬY۷oONN~W7o.ҥˌ3ѣGnqaw@Eg󓓓۶m+gB*U$''&&&֭[WN҄ 6lҤɎ;^t\؝PQy-!4 0`$???55^zB˗/7mTMeeeYmqawzdOl-7_ܹ355_~)((۷B?,,LPyBpnnk>ʭ[2P_+PffC,==!$IPP2^eTtҼYr W\[x|||q !T)h1l6 ? !@~ixg{ԩR/HF_ttɓ'kO<b\u7ZBg2%v ^{gϞ=s挼699رc{vg-TTyy o~ȑC-_… SN["&&f߾}||0i$yė;wKfYqawVlYv(BP䧩y$IfyHBSAAAFF`(v( T$Lݴk׮˗/2ہc$( ސ$ >e˖QQQ[j׮}n*33s֬Yiii]v5L;w7bKY!|!TIt !Dxxdv8洴4!Ddd`v8ddd Hoǂ++W4 .V"!!aΜ9k֬ٳgXXXrrb̘1jl߾=99W^i޼K.3fGZ SZh!gBԽ{w|!ĥKL&S5l*11nݺrF'hذa&Mv!_tg-TT^BXXXػwF^vM'HNN ]bsrr֭nvS/_۷zaӦM?P@ƈ 7JKDCFx ߂ {བ1Bʕ+rxҥGvڵpϞ=/ĉ{e)0B9KKKuy͛ہG~ :/KMMv7)Ov\ˣ*۷tҜy !֭۸qѣG :mڴO?K.AAAfee ! 2.~@099?vX\\ԩSԩ#/2eXPPP~>o]*$$D^x-!DJص# оeUo2rJ !Dhh#"(Чl`u/-{9!LLL\|yXXO?ݹsghaFꂞ|022RZFNPQx"&qĴ',!!>)p+l={,YM6O>ս.\{ݬY3e|7sLLvO<^xɐPZ&M^%Iҧ~s>ZJ.??ӦMUV;-aϞ=+W!HNN>vؽ+Z U6))iԩoذժ~lٲeٲeqqq:tݽ{kמ-[ !vܹrʾ}8PQS IDAT1s bQQQnؒkɮ!D JS.<<[FOfY<22O'Px+W!Ξ={YUwygLLL^6nܸn:gy&66V. `+Vlڴb4nx̘1JFZv}v|!CH9B+zB+W{;(kBxĉujہ@yU^vP+7NNNNNNNNNNNNNNNNNNNNNNNN'Oرrʕ+W$InժUnnvGY6IB%IOow#oAd0)/\|>EGG?#&L]0˧\oGᖼ;Zx;QvpK@@@pp2䧟~믿6ͭZСCv6mi0nܸr_u߾}h4h… cbb<;eNRLOv(U$ !*Wl2f9==]Q@TPPi0""" <Wyjժ?#bccs}|ɕ+W^y)S82JQrZeT J7n!IOf9--M!p/(((0 ގ~?Q=E0͟}ً/xY7"D "!DQH !HǕgd.%E!!B !ԧ3=-ƍO>ݺuj MC߶hg2eʀfΜ٭[QFfF(N';w߿C gΜy###~ ^zݺu%'ÜN,X Xh+"ظqby|m۶iNsјiӦܹS1tP!DlllFN>%+kIuyIXXիW= d8֩Sܹs1?SJJJ.].\PE@YtBx]w ϟ?O>K^xqPʹs7o%w __߮]B͛701=zLLON_![}swoܸQzƍ!!!˖-#rQF޽{+KnΪئMjժe42=M<9**OS #ŧp/_޾}{z^xݻ/]ҥKƻkϟ߿v,YӡC7|̙3%㜻ɳUV'N8ѯ_AwqNj/x P\|QFgΟ?裏O-Z5kց<"$8=aQ_qyyy&J(C7C0!J6(BXeB φ( 'f9((j!C@yu @CB:i’jժڵk߿cǎڌ>fQF><] *G5 !,,L033bc./vsM6-88{KJJ1bĐ!CM͚5+--k׮&iΝoFTTvgm(( 2 0ʨ>iB(Iݙ$ܜ^bʕFq…ժUB$$$̙3g͚5={ ۾}{rr+Ҽys!D.]f̘ߏ=vS *>ԩS-ZA!d޽Ϝ9ӪUĺuIaÆM4ٱcC=d-}+>!lժUI|paaa޽5j^x5!_AA˗^۴iǏgeeYMzp``kϮaX,3; Jl6k` Ed6TFh,fQ *3b+W$&&FFF6jHE9=KKKuy֭[ہQ~effz;222ME=,( 88qwy:uoٷoҥKsrr͛뛕% T NVpm{H O>FJcǎM:N:B!DNN[*UڂrNZǑpqeFم2 0ʨ>ieh4vԩYf=D&&&._<,,駟ܹqyB kt@기|ϵn]'ܲeKNNή]\pw{YdI6m|IKm~~~'OT/#d G]dI]-OIwڵkSSSFcTTɓ!mϞ=}Qbby*&c#~jC PjJKK߿u۷߽{WѼyk׮٦*>^΁ Kajj9B}iiiBÇ9a';/tڵg` ;a a a'P^Ch4'p={P r@sQ2|IYYY^VVf͚ %Kڵknݺš/pݺuڵ{뭷P^ ӦM 8q]~Mڵg 6mZBBBjjM F.^[.//OѲeɣdeeݹsܹsW\BL0a֬Ymڴk#!00!8SM,//﫯o8Y744o߾{'}||lZ'@!  Bu.Z̼u۷!!!6 vG Ն@:U?bBP)!T@*E "JY;wޯwƌ[IG::uJݯɓu5>**x{8Mvv۷mSj͏+Lվ6 `g5 ݓO>}ɒ%NZe6tFh4*]`eeeJ2L`~ ozYY$I(`0 p.....\$X'?1bD׮]OլD*S(@+]$nj^^^UO#Kš)]0 KCL,!D@@FQ@A"&00#P'^#IR``ҵq>cb...nnn V;Q &أPmY}UVZJJJt:hh4?x׮]mT/^XRRm۶9s ^zSF+jnjsovҥ6Y&le]ty衇6l`eĖPxu.` ۱cǂmLX}SkV￯Y&//oȑu `wV)STۼy ֡X_J%IsU:.Zuw4V!~?Õ+W ###y9s4iĶD2Lγm۶S޽{\{FV\9vXӅ5SSaJ2LYYYBFt9 Cvv"00P$srr$I T8է?W_=~xVVVFF?eʔSN٣PmY}[o8cCCCk-[lɒ%_~M؞G#FXAEi'Nآ0}Y333*rwwɩsU:v1%%Ү˗/wڵU@8gΜ;wn߾bWRRRiiԩSmQLBB˗FТE ^իW9rd:t8,m۶]۰zZSCaBaBq!T'١ Y;{p0o*pV!edd͛ס$#Xz9sV^]\\|i V>`BP;pu6mzm_QW^}mM&ɓ}^z G]nʘ TO?455`0knҤI7I/8%W@@mؿ;w5fdd!bbb̍>`K͝;wnvvv\\FINN~ז-[&[^pVVA޽;??QFu\waa_RW !&OܬYjw^NxN: !bccv=q:z·~[Վ7.''...>wh؛h6mZE۷/""BNtB6mڴoN Ϊ#oFlڴe˖ j&?~MBӧOϛ7\N]v?XTTOtڵrz}ffСC-;v옚Y^__*/++3 5y@SRRt PFiiտNh4JJJ$IR@=[vFqu&Uww~v0V-###;;̙3qqqeee^p\nl!e岳k[u ,..b0FAS(@+]$nBʕ+mTu"""&N.=zٳׯ_e9zzzZ6斕պ ]\\4M-_Pm0=Ԍo䬟s=gb6k,˧^^^Æ [zիW;t`#(**l,,,Bx{{˙vUWU.68Jz7F=g2@ |Α?Bz}NN$IwUPHHݻEza@_zm :8q:$7nχ C_Mf٘+IR{*>:Vzrw>srwיּ_~%++kɒ%K|M&M*6mtRV:ٳÇOK/8+n"z?999׮]Bm6,,Lߦ$I'Onժd{U۷ow>h_Y={N8{UVeffΙ3',,LpB%?z|̙3;w.++:u<,: !>̛7ҥK.]4i2iӦO~9sجRMY=섥 .k׮y\0a'Ԇa'vBjsЬiӦM6U)G:T=[m8w΄ Q(>B(VRIIN3ڵjؑՁP^m۶͙3`0K( `_V2Z)V;f̘;w~K.2ve@(ҥC=a.`' Bׯv{e ߎ;leʬ]}͚5yyy#GsU:N2͛/\@W.IRddd|||@@@؝ՁpѢE`6(فN_"I?_zR좤DRvrqjL&_v{P?xk˖-B4kLqM$ .% P8=z IDATdBh4`0,u999$*] 6-]={j믿ޣGo4 էsatM!Dxx8@b] eX?>z.]v*]Cح[7OO\;p ahh֭[5͌34o*7{ŋ߿M6"4|!Dk1!8!T!`Buʕ+Q==8u78 222w`Pz~Μ9Wb n*Ձ>X|"22244%@n:!ĦM~iۗpo*sՇ~4 Ձ+ @8hР_~%??@okqأ cTS7x\ˀ6mԲeAEDDhr̟?fCv@I](6fJz*LwCP&)++Kh.P`Bbpz>''G@kTpʕ`{uʼs@)G>XbݺuPV;pBP)!TwBt/4,,F֭[5_(@W@ho&i>l^,srr>TЮ]I&5n~ zS5aaaְ߹s\cnnܹs;ֵkמ={k'ugU#$iZ믿NOOOIIػw^NxN: !bccv=qDk'K/8+%2Z\\|9Qw߾}rHBiӦ}Ū'K/8+%!||M!ӧ͛g٥333jرcԼ<__ߚOYr+*T[@"IPBe+b48 777ww7(;;[g(dzr9kk[m @PTTt P3L| dA ;wnչ^^^B\&.++uoEj477DPo5m B-?@d8 FS4%Kآ ?ɧpx{{[5qFj[udt63LYYYB`9g^ɑ$U #VM,g@TO{HHHZZecZZOWXZ5@U߿˗/]$?tgϞ0`@gqT=q]zY#BA߿ѢEÇ$i!7l0tQFU;q]zY@tҵk~7F1**jAAAroqq;w7zð68LLLTLLPOn(]|р2 u2e400FN滌roEUVɓJ UC Νҥ҅@CU!ZtttttU@P@*E "J@RBP)!T@*E "J@RBP)!T@*E "J@RBP)!TUR^t](]QTT$IU 0LBe+b48 777ww!֞d2 ۶jC?LjQkOjZ @ըQ#KCLb!FQ@Dqr^/--$UBP)!T@*E "J@RBP)!T@*E "J@RBP)!T@*E "J@RBP)!T@*E rU*,,=ztƘ T:}NNΧ~j0ڵk7iҤƍۤR B s>XĹsΎh4ɯڲey ΪBN'}BY6]ŵOqq^5~QIP̠|e+" L&Nãio 8qbѳg^~}ll&i(O[VVVު+,+++))7m*--U@a-p5YfY>6lի^ڡC.!DQQecaaۻQFBww;:uO_I 6vL&|BFcII$ISBCV47V"{nvQသ|000PovU.l)]j1Lr h4J(`0ȁˋENz^>6nӛܸqcҥgΜlB=$$$--Ͳ1--׷.|IP@/lܸ`0-|M&M*/_tTӝ={v/|ugY`5TBx{{ٳĉ'O\jUff9s„ .4 QQQBGʃ^cƌa%+JG32RO+| !Ba7fR}~߱cGFFVmժոqZh!wٳ磏>JLL;vr޽kצƨɓ'U^' p+ P8dOB` $Bz}NN$IJΤ^ª}l%RϷ+]ꩆ-pu>=!, UϫjkABxzzj4k! \\jd4;$)]PTT$IRh(jg>>k֬B|vtF144tȐ!B/tׯ]nݶmۆ:m4eʔ)K.=ztu}g ,}/ݻ^ڬYA/~[h˖-zz1f-[lܸqժU򔹹'N2dԩS,X>mڴ?0--GygӿK.uq„ M4B/'*V XbFEB4xnztұcG!ѣG{6m<'NXb>sٛ7o !Ν;'HMMoFjIIIիWkZook?x۷'Nw}'Ԫؒߏ?޽{}?믿.((9sfϞ=7nxQF9r`޻w6l،3N8rJ0k,!O?dɒCݻ… ?<== rتrؼQ ˗g͚տirrrPPo*;vO:裏vyÆ gΜ /--tRͯ]ߨQ#9h4^z|򒒒yyxxT\3gV\"h߾}qq͛Y*cǎ :tԩ˖-7ర͛[CiF~hѢ~X1xcǎ>}Za0֬Y;B >|+تؼQwEݯ_?ә3g._\އBƒ!D˖-Μ9#xbYYYBBdJKKB={ã} N4C=::ZR6x_~֯SŖ,IܹsYYYr﫯ˏ{y…w !޽{ٸ8b}||4( y[n 6LoBtgiܸqkYE` G2Ǐ_zҥKϟr$I;w#;wc=Gv-55SNo!ą VZիWaÆoaaa޺u˶*TŖO'On߾}TT#<Ҷm[gϞ_|ŏ?8dȐÇL&BrNffYf]Ì5_j l0!ۻw'u&ٳmSLVYީ۶m7ڤR|_P?UbF9!S}^oݺyɓ9996Zh{¨(!Dǎ öm‚֯_̙_~SZZZ_jZkZzKx#wy{{?#Bv޳gOť_Yddd@@]W^]n۷ 'jRl(#p*aaaAAA7o p‘#GN:~N111[nhٲ"22C 9q۵kg29b^x֭+ފ 00p =zkƎW T裏_z>~`>B_5::[V}g'%%Sٳ'((hРAU[5P7*"©Ο?͚5;wzw׭[fc/}'O4/!HOO-+XޭQֿݻwtf͚YZ[ 6mtԩC#11<gϞ_d*ҨQ#q\\-[mѩS &T/تrؼQd>kz?޿ ,SUÉy),,$I}ժ7nl&Mܺu/blllYYV N@qzjTT"**X-[,x킂+Vh4G._+EC#I:fFUܹs9eooo!ij>co^^^3f8}'|߾+[.8 !@رC?Сػw޽{;`={OΟ?u֭[?ޭ[ka.8Nz3<#2eʄ V$7ސG$!֭[{ѨQ֭[?gΜcՃϟ?3tۻm۶gϾqFiJJJVZգGOOcǎh4~}3f̥KX1IDAT*-ڕo˗_~e˖} _%IڴiSJJǀ~!>ڤItuy}EʪwlӦMiӚ6mݩS GG/HOO$sr{o3te5jTdd^Νom|jO=Խ{:uT.]+DGGoݺömRCrr"..βqӦM111ׯhݺ7>Ν;㏖M0駟:t萝y^zYA͞=swwwssݵk׳gϖ+B?Ӓ$4mԪ{ر}FEE;>p@>}/_߮] v}׮]?xaa匵xkjWU͗ou=++_7o^vv/$6lB_~yرr{f,gܹ sssǥKFGGYU'8!@f̘!pB+W !,X ?m|<̀7ĉ?B}OkBw}תWT몬z|||̅Lk׮yxx-Bk󰰰u@F p”)S3g|ꩧj8WBBByCU;oq6m*駟g9Ç%Ix|T}ݤIرrΝ;Ӡ[nݺuJe %W||O׎;Vl4־VZTe}QFSk,j?jL'OL<ٲ]Վ?^qĉ: w5***JHHڵ|2iiֲeK!ĕ+Ww˖--eeek׮}NڻwH!^rdjܸq2?.--MNN>cA[N,Jj׮]mcV*AKnnn5l,Ǫ7WdmU.Yf[T|u͛͢W ZB\xu@E 5zSRRl^+!\]]{;vݷo |r`Emڴ;wk$Iڵk޽gϞݬY3ȯNEg9jRYօUoZ.a[Cn: TgÆ k׮B[NK]tJ3!|F9Nǎ޾}[~٬Yׯϛ7bI&]vmܹgnҤܘa9|` ^V*Ȫ7ޯXM>j{zz޸qDZv۶mk!BPT6IIIξjժr-`dz|Ν;.]!rS.\GB Ǐ,\МGu־'O[oU%9r䥗^ϲ+--;w;5j4gΜZW%g>B(XpFyWw%L 6,Zŋ=zjڴhL|;wL&S`̘1(O`JeoWdgggjVE !?ٳGn7n{5pZ N@*c$Ϸnݺ}l߾~K7n:ttRV׺*$guvwϏ ѣGxxĉw%O+!׻wCm6...!!m۶7olٲɓ'/r_ޣGMFFFo֮͛TA־~E]V}v=fΜ)jV~Ohܸ%gB4xڵOJJJ-[fuL&!V-^TT?{^WJJJffV@9CgA 3СÖ-[-y|}}cbb֬Y#gΜ9BW=q9 SLBDDD ؽ3r"bvU*) VdITiiOccX,{GFFXJBDZ699ȑ#gΜٰa{sbbryDDĦMvؑFDeeeŋbqllmیF\.uuua>#jO>?x@ (..fypp>h4_uXccce2YIIImm3ٴdUU AAA׮]sMMMDtI.M&#?! VkrJH)%ܹslJ eUVp84MDDW_}?x299YP.&lǚVr ERR+N+/ Bݮf̘n޼RXX>O1!t{]n;{#[[[kXBCCYȈ{H$Vbm6[wwƍgϞͶ̚5kÆ ?3ے4g9::Z-m111cccSz表ӧO]._MD"h…/ihhؿvv-Z7n8qgcV`9,,v8O~z{{u"##YmHHHIIȈlf["##sssJ젾˗/kaaa*//wމgEHT\\> :r3OzIx"ᅨ?n4|Ƅ]gΜM=|pooS4x@ `g͚uզ7xCd2I-ojM*wj%"\of̘U {T?r@h"6UXX(M&L{Kxl6oڴh7Ūͣ]  #˖-#A֘KJKK۲e+Ξ=w7[E7AeF&y~~ё- M&ӕ+WKqe˖t{͸oeofF^xZ"뮻2IMzIx" iiiٷo˗޲X,t*j7CJ<:p'0L]]]-Wiis=W__lfZ',YmtyMݾIG'N<7ܹrrro~1"tDQQpkk+[WWޢɿbƹs禤QPPPNNW믿I$S*Ɗ˗wnxU*Ղ jkkݻrʄ6W\ILL4F/--knnDrVH$ē0Cw_ -++#~ݮT*_Ǐ͊SlMQgggYYY. 6TYYi۷n… w_uu5+J$ŋӍK1,,-/m6^O !9rv{WWW{{HeeT*%TLV^^>88tGdիWO<1|5zhww~xwQEEZV(k֬ ǎo^QQ v{uuL&[~-ї֏̗^z_.=ÉٽZvϞ=K.-((Zuuu… o9DXUز@ϷY,\Xb vYZ|l>hxx:icNppfNϿpWӹbOO:t+̝;{5Ke_BD---Lؐ<0 ,̬9NәZUUT*bqVVǺli3gfee}~s8qBH$Tʎf;^:&&}Vֻ#nBfhh(222%%qaׄkOj|||֭RT"jT;?c|`0޽='B @!!Px @a @!!PHB @!!PHB @!!PH-l@]ҽ{*K/TI9h믿7xUV~atʔ)ZJJJaaaAA7`F$5Uǎէ,B#G(o ~oq\N:e OWPs=V={V}"VZt:ݐ!C>sV… ׮]S%NyL[h#_˗/=zT9rNsRNR^ڵN:N`Bkÿj:w?MOOW^;) :toF㿧|Ӄ"!uIL`eڵkǏ߳g|{̙/6iIdff*tQ;L)S9sF)YbE׮]뼼YO??O<2dH:ujժնmۅ fddhaРAz^=yR$cǎBJ  gɒe6o޼gϞ-ׇ~8bĈ۷+/GOO?͘1ym۶}'ԧѤ ð[:u>uIJJJD(>h5]vVf͚w^7))I~a!Ď;ةS~TQZZw1 35 Ço۶m```hhh뮯ڬ畹3Fqܹ9s4mϯE#GSzB?ݺuSvƍvkX,^XgΜU_((,,z駟*uٿ0z|pժUJy>}d'|Z~;P>zjYxq0<<<++K9sح[kΜ9eZ#Rqqlꫯ0P{G,F͇~['B+ٷo_ k9bĈ,櫜Bƾ⋖߿VVVkn݊s)+?Я\"~Ϛ5Ky[ɄpeQW&$|˖-ΈPKBxooo3UZWwNRQ^z1srr6lh=ݻyW]Q\-uԩȾ VԩSAAAOϺu^tɬPWރ>/[]>|p-, (--]z|ۣGBej^^^zԠmێ9ro dv H=Ҧ߫zȑ'U(;q00!oՋpIgD{ 2]vJ:u 2dȐ!2.**R߻g͚jժ˗wM?}B<-Ewu޼y{:/U~Rϯ[>2iիI79s󕫩ӧ+sRHlً֬/v{N=JlX6ZR~zzzݻײƄUVʧ{9Ji9Ih4Ox{{}gw&򭏏<Ͳj*eF-Rfnݺ۶mӲ\& r_jܹJլY4aEy e4>>^@y"T縸z]{W 8_P!^uu̙3F/r\F||zFS22dH^^'LT)ML~[)JOOW6nܨTmVں` mF;LcBߪ?URRҴiSV}OӦMS>r!*77ol&K~#(ٷo2)k֬Ѹ\œe[Ck FyZIǎT{fv?qPcǎ?>}uuY7nVڗYvmTEDD(CUʗ-[fDJ]v͎+x ^}M6)o'Oc=58u1c/_.ߪ0B_~K.]tQ^رCqСΝ;+Ϝ ޽{yS1b Scǎ)cQzd2t:$8{& R6m+.P}j\o 0r"T}êeU+W:t?8~UيRQt*=zTy=p@$%%)LiӦDW!?LG>|BJ_ [v*ƒΙ3G >p@Oϲڵko)۶mfaÆˑu\ze&-v[ʈ5f~׶nݚ#YMdݺuj͖ fH5믿&Nh4M67JF٠BNe:!!!]v駟ѣGƍ68%ɩS훞.]N>23,sUUrc aG)zxx|WSy9weٳgO6lLO:ܩױK.RgϪ ?x`;"IhhsU`~\axxrCٳgov {~͞=[;t0}86Q aڵ.] N/̙3r,]tРAl^^SQZZGQ(ʰ&IUPҲeK|ѪU+ukՇK"T(/ذaY;v '_߿ܹ24L)))Q6m꥖,X0_^w'ozP1c,XQ?+W/rf:v쨜Nx|N  ӷb+srr֭[nyƍ &7nܸqӦM+HgsIÇW^^Tޖ>䓹mPP3*TP_f͚;jFd}˗/+o-[f͚5k8p ,,N5 ZsKJKKNZ^z)cjq񘘘qթS'!!Àc*WH 2DkTtYݦk׮&ATzGgϞ-_ϙ3ڵk}-**:v￯p3o޼*/LUɓT!`4hٳ{qk 2SDFFzyy{  4v?g4sW^yEzʕ.]̝;E[o4ScXw'El= ҼysA-!T?hLxx˗m uڵ)DGG5P?@WPP`F2f9Njˏ4HV sԺu댌 aXQރsRVCh2e}]vU OAytIv6uDtʕZS`Vb K.JTn,X%.ӡC5JLL_MSPުGڷoo߾;00Py7X9!ٯ_90$޽{ܹAG޵kWy +W]YN{gf͚?[o0`z@ BBJʕ+t߮]^zĉڵuR!!!ʳ% BOOO3>hdd?0|{zz֪U+$$D ڱc֭[Ǐ߸qc;N6.__w``>soܸ`ԮiӦ)))=PV|}}ׯ?qăkcǎ?QO>}z޼y:t k߾n?4h@ݸN5Τzm˖-F{ǵpg!7EBn !)BpS2 n3HM"!7EBn !)BpS$HM"!7EBXg'11J˕+W*- CE;޽[uՁة?qDN8{FaQQQ~gcĈ:/pu e۲eNիA}Ь,QUlM5I:w裞={FDDԪU]vsMMMpMj/N;H^6{ ŐV@I7j͞=ѣӧ;u$zl׮݅ Q㣴 ?sL \ti``I^xᅑ#GymڴQS~R zb߾}L'O*򊊊Ri&iǎfQ !bbb222)ؔj\R_|I  2v !~g[,oI~y?ՓQv ݱcl 駟̦мys!Dt_qqqAAAB]mٲEVi@mbs5jTf͞z'xN:/+شg-:{ɖ^Lx e#*ܹNؾ}ej\5HㆰVfW/3-P… -w_f;uSe2]*G2@Whhh^┹X 6l  ?\1n86}B$&&Z&!66Vwނ7on۶e˖MiyM͜9SVRRh!ă>NvgM4LIIQe^dy@_/_,K󽽽e٩S'֭[!!!2B9@7|377WaFG52!ԲϟBL8ԩSӓ&MR~"XIǣ뽼z-+WԐ ]n]!D.]C)} WʴLJ}F6}X-}߆j֭B[ ;uSZfe ^B4jh%%%Fȑ#r;N4Ô7Z:j]tIѾ}{ zH+N}||=˗=<<""" eB(dh¬IKK_x_~,ٽ{(ү/REvgB/BݲXRVԿਣG޸qC?+lf[V2!ԲSNB1lAnZB<@\Uނ˟_͚5+**RO8!K'ZڗT{򗫇ɓ'-Fc۶mW52Wi{z]rE1 !6lgϞ-xTVB}q!ӧBy¤$!DZf6U.!ԸlZerFWֹs2GyDN}eZ&Wo%>#S,J[ǎ׾oC$ӕtu_э۱)z2Y//\b͛7onYhL4h'wc'l4lذ L믍Fرc䯁s֪UK]!/Wصk,ٳgVV։'ne...BX>sܸq//P!Aג'ݻW1tݻ?\&L_RRL7 !bȐ!!!!ԖHt1S[7PyvzZh.-,TzviiZerxWv y{ҥK>sU汓vo\ɾkwvEn4/]e`F{pR7nǦZݽ{͛7QF֏z޾=L4h`2o[cj4F=ܳyk*WO0)))BuY>P^|]r?8vؾ}+"##- ikذa˗/ !#ӧOzz={~ǻ+###))IkWv mEK) 5n5h1I3  !bbb,w cIߺu G^cFNs)k̘1~ud֭[Zl٣G;fj*o 1+iVfm7V=ATV IDAT{U&wer' h׮Yz5mV+wXٺz]փȯ]f0ԩSfcIݸo~xΝ7oWިQح[7yUFZzJ0er* Bu06qqq;w tҁ5jtw^ɤso)77Wylٲ#Gddd!z}֭t"s63=ٳgPPŋO>yСhѢ/{1Lݻw7!;Jp)Ν;' weggg=p-ƍyyyB[6nذ… ֧i2oXR˳fϟ7e.3N;5?ĉ?ӯwXz N˞`SU&veiii;FٝV}UWF]ݎoܸ!&Z*ԍۺ) /۷/6n{YpejIKoC(88Xqu[cj4.#7o,M&Ӹq3qB=xly;3!!s|?$p //A !v}СB9(:))e^/*7oEbVUXXSk_!!!r\tƲP9hŵkoXR񧦦Z?^f%/_,ߍ5R-۷~رc^`vl UvD [V=A{U&/˫Eﰪծf-U#gy#v NmݔzꕘqƧzŋx.]hӴ3fr_@uFBh3yڵky5w;v̲*==ԩST!/l4/^}z[nK1++ዠF(o {zzd2ݻ5mh4!Ԏ?n+t:yWnY{M0f͒o2䟩r6qСC]<<<Ə/X~W_}URR2h ~ٱ*hN˞*\>?͛Ǐ7<{}<퇕Z%WvU6#n|X,/=(:СCu:ݏ?(?yP1`!Ě5kN:٦MVz;BkfUo+"/yuٲefC&+S!UnP[N]C%$$K^;O?dv7իWd͆OP {o '#Zh^uZV{ά^+ox*eeZծf-^eGÆ իW2A'uU)KKK۴i-'uI)STTTDXNS^t O9f>>>w}`6md̗gΜ9Zp€<(ޒz/!/?VF:wԩS7m$_;\!Dzbbb233[n]!!\UӃҩ{===7n8}t9K.͙3gW<:<iѢENׯѣG CvvիgΜ Ѿ}{!ĩS^xR!DnnٳCsԒi̘1YYY ݺunԨѦ8Ww߭}Ν]tΎ~-vzm]v 뮻^^I8- Bb˖-GNNN6/^?믿nyVUpeZΩ3RիȔy>ƫfè̝;W9v_~_VhX2wuVQZZZjjj۶mʻ9J[:|RnBo)cxzz*?_n6&GGGGDD!|}}?cy^ꫯL#24.ӦN*u:2~]o_LwyG깇… eGyZ|I*oeʧEGGPf꒷6yzz*%ڏhiUgӞ*28SNr4Chվ2-ChҼz+ٴ!,JeνoI<"jpx7nrĦdȑ#؛?cSÔYU$OhY30`y> &&-߿ڵwo&''Ӥnݺ:thԨQaaa͛7ꩧRRRf͚7L0aQQQZ!t:]~S{:uOʘ1c瞦Mzzzs3gΔ:ԫW㑆̝;;mҤɈ#{κf͚˗oh4޸qc s̉2e:J.iybbb;v׫W;66>zU^ԸJ{9eʔÇO4Iޱsm=e;JRZE%l:VZjYuV\%KܹsɭZl֬s=ݻO2iӦN42?h5BrtƮ^eA;3g:v옜<{mt6mw}ǎ6B"'Xe:##]|yÆ ׮]su q)JKKCBB&*̒Sãry \]Μ9'TlVϕ=mG>i$NwY>_X5WIIh4:qR~ck͖bҤI7?v֬Y[WV|G^fͦNjTݺ2o\-dui\^ҍ\۷ozO>YO\].].]xbM!͛'1cBs0YRKYYYo//@ VP ZrQ4+xM6nܘ.aSN2w܄۷wرUVΝ7Nkp%g{ !ϟ_H >]v+Vxꩧ*9)wk={ܹshF[ !;^߮^o߾999G [r;>Kj)22;88{]NMª*n{W^y啬JNʝg:uСC]:@a+V !).7EBn !)BpS$HM"!7EBn¢"///W___W !P @ETs$HM"!7EBn !)BpS$HMҙL&WP}: 픖z///WXc G5:Nӹ: }q-.zZ;Bk]vJKK===\ RF`K'!7EBn !)BpS2j'acWpjz! !)BpS$HM"!7EBn !)BpS$HM"!7d2͘1իyyysܹ>fggg!::zuX nZ!ܽ{wFF|Y :t(66GIIId_|ř3g7o.HMM=~ȑ#u:]oBgϞѣGWxw~ew߅iT}Œ #/[lʕ۶m3Z1cFxxZpO:ʵo߾ &:TԆ]-aEWI^^^AACPsU8;!,..>rHf\ܚoBxĉW{a:t(U} !)BpS$HM"!7EBn !)BpS$HM"!75`4]0%%%Ayƚ0y0,,L.=Z\-?AROOϚ0ХCK8p`Ŋ;w~I?wDDDJJ0%%%000((HYZd5jh"lP8pٳg#(Ǐ;YD={ō=͚5?ilĈ5~Ek֬ٸq8 e˄o֒%K[l1-z7v'8>LtPi2f̘qJ>,99`0DGGO>N:ESbt<l>C?5jԨ_~[l~z``G}dk6(ؽ{wFF6999 ,8tPlll=yST(ׯ6lXZ6mlݺլٶm7naCy'O&%%Ux׮]K,i߾O>MVZpOos 뮻VZuEFFڔ ! O8a4*l%3:!D˖-[n(/yL-߿ӦM/̙33zQF5hР ]tرc/Ҳ$---..N]ضm\???k̷T \&35%`uP IDAT.zzӳãw޽{~w>iӦ+V<#]v=zwݼys\L!DppPrvZO ǁ[2TPRRRRR(P\\\\\(뫾L'j111111K.=yM֯_?ۏ=zڵD_~~~B!DNNNiiݵ+?PMԔd2FNg.p -ǝm "::z… .7o޼y_~9**j̘1Gڵ}-z@]/Y}/SGT|Wp quNx{{ Kc'ꩲ4i۳gOZZڂ ?޻woDVvmaqBO6 LC3ӧO6kx*)))” NgwCBsέZ_^n2ի7nܾ}-[֮]{IYϞ={655wyNd-'MgWXO+GFF~.\Fffy󒒒ٞ={F-~>~xNVX1{lOO#GL2ecƌ9rd#+,,P ^lʕ+mf4[j5cƌ{ҙL&-F>쳯,y{=!ĢE,Y<<<44Ti8$$$;;!8q"***&&!SB`p7n8'СC: pg}0="!7EBnJӃSSSCBB%999BBPiJM&SVVey~8Uℐ'-AeMU|wߵu=]@M텝t[Y ?nDI4ʨãgϞڵtN P5*NGcǎ}7nܸq? 8T<ƍ]~ǧ.]C-ZXp^!A(ƍ[nݵk6m4iҤ+Wꫝ:uj֬ق >@ec'/ڵk~{ƍe˖6mt8!1b~ʕ۷O>=''7ڵkTTC#8QF=tСC>C</\Ȫ’WG8LnnCФTQRRRSb(kMe¤ 6_BooAUrՇgeWP}Ԕh4 @.p ^_a;?cÆ ֭ytܸqwuWpp}Ӭ]H~~~AXRRkJp TO%Ǐ_~O:%뮻dN)!LNNyɓ'y *N[n"9r*Ne6Ѿ}UVZGyCh4y<J*N*P*N\qF '|RZZjKKK?S> pW_}5::zժUڧjժW^y099yܸqf͊6m֭[]V^k׮mݺWެYƍЀQ2~~~UV !n֭[ !_qĉK8mڴG}e˖^}>EҥK7n?$&&n۶ͬM'O<`c::T#iMB_rEQ^;#D3ؖկ_ j.Cn !)BpS$e_k֬9s`h֬٤I>,99`0DGGO>N:k |cǎ˗//:thB*۹s͛_ 8pذa/_^`AJJY`Cbcc{葔3dddhd£G۷#G=zRc:7ސgfϞvڗ^zIk׮%Ko^ѧOy}wӦMܓքUV?￷lyʕF9&ҥKW t|%=!D˖-[nxt:Έ? ɓ'יe6n石.]ijIIIZZZ\\}۶msssYdr¢.Sv0}ep8.Q 0 aVV|Qv|W_-Y@@S0aBJJ… ᑐ=e!Ne^fffaaZ a^^8pk~CAQQQQQլ@y \v|}}kժeMIFk_UzuoOdСC6l(B !rrrJKKVIP|6oΈBÇynݺtÇ< uXf-\@0Q\@5BTܱ qrM35M*]lSZZ%me{ "h8| :02˹gלs'dee-[QQgB!kxvvv}"uKЈX)***..VTo7PŽ0CV+:XYUS agpYYYY222N81nܸ-o޸qs:v(ɣϽz ʪ&/"Pn$Ij(|x@d_fGƍW_o=w/pwwoT*www= eɎNNN$5mЀ}]Zη>|xϞ=M^c:z$I۷B 80###==]B'Ow& 9x`dddhh$Iwuujp/(d{o}Ytkfm;y~{ҥ|pȑ}޾}{Æ III~~~ӦMkڴi {Qdzt geKP#EEEjZ2AK8/ t t!gOk4j'` BTnJ÷!L C:L<ۥKLM92޸q#!!E&&LTfÆ wmׯ_OԺ*Onu˗/E=:bt \x]%I5jkс022uꘉg4F!~G?  }||~ 4oܴ~Ν;g̘q-]˕+W=']vĉ+`.F28y_~_^zӧ9sL#ofiiC}Q]c=hѢwy筷ڶmIGO<9zh4V7S0/aVV]T*ONNNс?11]={uU3:.X`Ϟ=vڵp’3f0y=LXXXFFFXXѣڴiSZZzO?4..nɝ;wNMM_cǎ+`REEq+H0vPLV.̼ljHV;::ZK8/ t t!gOk4K.>Bl2u@xascFBEjeee0bKP#]yyyC){hsꞵM5>Vz.lݺm))))))tY)//BhچR0̇8hZylPjaii >;wkF3PKЈX'QT hRICy^!SL*OFUV}B3 F͛7 !'L`ru.^صkW 4tFB{{{WWWsKFӧO監@1:jzҤI999(P7TfٲeZ ݶmېoooZ]i%K@yH3P$c7hCle-˖.Ft>K~t SKZaHB}F>@TµkA:V} ׿UuѓVoussswwEI`t ٳg˸{#Ffg)s CuE!IҴiڴi?9rҥ&`2Fˆ4m۶ݾ};11ԩS{iժ׿SN?~E+VHOO7GZ2:.Yի~رco#G޽{߾}˗/B<~YYY\\)с޽{խ[o޼Y~c !Ο?_ fat ʲ6[8:: !2Y{葑WJNNNOOѣPAw*сpBѣG;vL=..nO=ܹsgϖ$i`BF߇pԩG߮]v !222kCCCgϞ-޽V1cj---ݾ}Çsrr<==zG}TכqƤ2__ߩS6k֬@F!Blڴ/NOO?pΟ?߼y5kرC^k׮κuLUhEEo}:3F;ʽ ={۷obbEn޼Y^P& !&M4iҤWݹs׷u֒$9yBӧO:u1b'={_~#!~+VtEѿ ߿ʔ)2B-[2[? ?:l0J5k֠AZ"&&[{B:tСC2SZZjBO>ݣG++k׮G)--_?;;;//p9JSF /0nܸ~빻;w۷СC!!!$);;[쬿";;Ν;zKQQQIIi `A999.F˅ `q(..PT*F1сPVT_qqqfffyy!Czi6%%%%?S֬Y3*=U{{{!Dnn<ܫ~t~.//!ai C:`.ct o/QIqqΝ;,XPVV/j !JKK?#WWW!Dxxok׮#F8:: !W),,B8884i@T*s_-]@#VODDiiVVTP޴tllllmm-]85YFRǏСC>}^{5lVBiP!IҀ~Qp|M2q^UT|{-]@#V DAAVi(|x4t Oe=zK~t SKZaHB}FB~2:>ͲuGmݺu|C 4aCǐA 7O]x]%I5jn"P@}ft 4G:fYF 2zСSN?cǎ6ae@9rR{~֮]gfft 1bą &Occc_;vlĈgϞ ό˗/p˜1cj?ĉX6i3zRvƍiPV7nܨR?nbt LJJرS.'''??sΙ0ynܸq7n4k֬v%сK.7nػwoծ>++k׮( `^F^ɓ'wܑ׮];qDIHpœYf988x{{{{{;88̜93;;{ 2G2: !:Էo_Z__**00~zwL^"lW\BxyyIde\ :vX˖-$j<˸@믿7G'O4SAa5zh4)))f*P7 ;v찶5kIe͛bŊ4e˖vvvYdTQQa 5Mm[{][FK}KhF|Kh%V!,ήI&1ڵk﷞^X Źf1WWWKP#EEEwQTP޴tNԽ3:_b$++㮱9kkkKP#%IRC){hҁz eBVz꽮lݺu-Jaii >S4Ie3:Z?Bxxx$@]0:n޼Y=aӗ+FO*sŮ]3:sW(h!!!O7G5:ct |jIrrrQnT?̲e* 4(::m۶!!!jK,1Y󐪽g$In62=[YF3K"ZhZjϒ,]B,%V!P=h,]ڵk@>_:uIe^x_Jԥի7olJuN!(YFZ633zuFڵk^^^5(7F)((xz5sL]cNNƍ|}}NڬYBOO;0wk֬y~KnnnDDDBBBϞ=훘h"2{@jtP$ZmRjСCqqq+VtEѿ ߿ʔ)25YF]nݺJ111rBtСSNf4 Ԑ!,++{ZnSO}WҬ#F/h :99ciiiYYi`AupFIweee `qj9u2 )~ח.]۳rΖG{ ^hL-]ZmMF IDAT*{hJKKKKK-]8vvv&SSHNNo̙ӢErcU{EEEVB8884i@u4b....F T* %k(oZ C:dj],a ػw.^w۷mVT9'6tss ޫ; #TTTl۶M1%%%%%%<<=99Y799I$uQ=K #?~G:t̙3喁FGGkNyܹPIejZ!!!c]]]u0 HsTTԆ W^^7mڴM֤AB++={Tjtqqy饗^P n E "B@PBP(!(@ E "B@PBP(!(@ E "BXzΝ;0Ɓ!! ^hL(++BX[[7a>Ɔ?g~BP(!(@ E "B@PܘPu[-]Bխ+0G@PBP(!(;퀥K@ݓ,]#PBP(!(@ E jHx_|qŢ|044Glܸ1))wԩ͚5a/(P9BΟ?G...ڶmܛгgϾ}&&&.Z͛5ej0G?s++w}EBŋC qvv333WXѥK!D.\)S! 25#ݻwӠzeeeBooo9 !:tЩSCUTTT 0jaÆ7޸qCRJKKFh :99/,q t$ #L8Qڵk111nnnBggg䤗}aAA:"K0 gggפI4@Xɉ'>㢢K !42B\Vkz5k֜;w]v/b֭B"% r&WWJ,]%V0׽j +Ĭ[yAAA"ɉ^he`&F%L!~j0066vսz;wnaU*{rr~crr$Izt-[xyykw,r-(Ν4h|p/(S8B^vgڵFrPIꪻՄ^P]&Ȩշo_///gg稨 6۷oڴiM61 0a``={ /K/_/(Pø`rBP(!(@ E "B@PBP(!(@ E "B@PBP(!( JKK,](ם;w,]4략ehڒKWU\\l^T*V4FtoΖ.@!~BP(!(@ E "B@PBP(!(@ E "B@PBP(!(@ ecHNNƍ|}}NڬY3K# ={۷obbEn޼iq\bE.]_pLbbq0&&[NB:tСC- ,Ҭ,F켼ƁkEݻw\\\'N899Z>-[d5_)))IOOt`m233k׮ݸqcђ$9sFdZZ@XGf3 YG۷O?t…mO8ѧO]E&K.UT=G}h~g}VBHkkc~ϟٳ͛嗜N:͚5MѣGݛY^^1lذiGGGgddHԶmۉ'kN4?rJGGכ^{BL8188x&y.ܹs={{#</[%F{Kׯ_OSSS{9mڴ^.==}F:u7lݺފï0A#_e˖6mڌ3F~:vؾ}.]Բeˁw}7<8~x;;8B(E׮]5͉'t-WVVV``n߾{^zܸq?A^˗7o_T:.ݺuk֬Y!!!BA͝;711@U[!?/ !&Lzw}w͚5nnn'O׮+ Apb}êU];?냂~Wڵ{뭷jb-{ F1c wIJJ2Z Pcǎ]|UVB'NK0GGG9 u...B]]]KJJϞ=[$~ˋk,*M4'~_5i$Cq RTڵ;|EEťKrrrt"STO ҟQZvvv -IR6mN>m`N쩻@R͞=W_}דt* 1kחܹI&邨~TܹsEEEcǎۣG'O !Gj_6PРѣJ vŋǍgxf͚?zC_^蘕믿^x1===55^eee !Zn({ʒS[Pge}OIIt|vmmm;w,foo]h.20˪esCީS'KV~cDɔB[nݺRWnnëW p+F0a—_~9mڴN:=;v4w" bggpĉxJ`xo߾ 6ѣ_~ӧO׿}(_VV&?ݶ>l0ǎ)))gϞmٲeӦMnݺ>}ʕ+iii:uXmkkk oܸ!pBEEYi5ӈ*r|ꩧ~yzz?~cxxx```llٳgw7<ï9>"ҷoڵkqqq=z0pzܹsgӦM3gc$s._ܻwo]KD|e:uNNNNJJ߿]>q5<_:tɑ#Gݻ\22$IUfgggff:88/.x!_%0;;[|6m^zվN^G@YzemmTKKK۷opԩ3{V|%//oժa賳 Eemڴqvv'47.ݺu> 1cF>}l?d gJ)X|ʕ++]0ooo7.p/"3gh={4|! ?--MzH(IR.]?.O>EUgs!8N tLMXYY5i>v*I͒%K֯_g??w}7;;{ɵ_?mvѢEdڲ{wޢ"!߹sn eYݏ?ޱc{M]q)66ѣٳgO<9o}G ڬm۶>Çn?]oL?zx`߾}v4iR +++;;KFGG9s㮮 $}2q !(@ E "B@PBP(!(@ E "B@PBP(!(@ E "B@PBP(!(@ E "B@PBP(!(@ E "ꅡCJٽ{iwjx4-W^$^tfcBMzyyݵ ;;[gݼysppÇM͛Tvj²_ۊL!}pqq@q***,]j믿:880޾}̙3]v5I뛜\勋W\4suz#z-"""..oqppB<쳏>K#ݻw{BO>src~gѺwڵ_W^{bR@=ugyF1}~Z׾n:I-[&?0`$IB;v6iҤ}&LM[OLL̓O>\ضm۾mێ5ѣB-[H8|pWWWGG<11Q;j(͛5*%%E֭ twwh4>>> x ڿIw~nӦM$M8Rw}'IRPP0NMM}gtбcy]|jP;]t]>***<QF._w{2dHaan~/ڹs۷QS1rș3g !\]]Ν[5ѣmmm[\\-kKe˖Wo˫V޽{||0 Yf !*u]VtRappfΝe;foo؋|le˖w!/v!!]۷o?BJY[[O8ۺ͛7 !bccu t}Bg!E(--]hbرn߾-5ܹsGpaÆ !٣kwppP5~/]h4֭[喢y !ZjW:u#zgk֬B_}5YeOkYFHQQQXXX^^^Ϟ=EkM6Zڶm+pB-KrJQQQ֭Ƽv !Ο?ߨݠ>[[6+))9rzt-Me駟 4o<$$D1/uii ***5kv׽?)p?z^HLLtuuoT*UWQ'h yvJllli?OIzٯ_yjJ>Ǝo߾;v 6l۶mZv„ 1Rkڲ2J%ZUS;~a!͛Z7==] =!+IҀ,]0=!4B999$ي!tzg}G=<<,]Byxx\v҅>内 ! ׯ_~,]r5k%5>内 Q Ֆ-[$INLL>|?(5jGG[q$ !vؤIO0v}ܹlr_|yժUݻw7IU57z?,^_Ξ;w… ޑ#GΜ9S:w܉'O?_~յs۷o :z+ osss㣢u떜lT СCߢE0/ݻS@@`Sm_~eI̙kiӦM.]6̔ثW/yyI6lذ}nٲI))"{W۷oooop‚!ěo)IRzznɛ7oھBC>W\ iҤs=[8...$$s„ WE*?P NаY}BL>}111/RBBB@@bƌqqqԭx;v<Ǐ2dƌtrr˗/Ϙ1CuV9)ݹsW_Ǝ4iҤ6U%K,Y䥗^??RTSN3f'|ҢE>@^&66v^^^j߾B.^8**?߿+Vrᨨ(gg-[ !rrrNs &Ѕ &O}@@a6ld111cƌsl/^_qѺ4( 5kʕ+W^ꫯVђ%KVZ;nfgg痢߿SNMUFm}ܹ[~衇~׿K{U]~=88xɺlllrW7ސӠ/9rÇoPquNڸq>+?ܺuױcټy9GsogggpȐ!O6p߾}?9r$55G裏3fhڴi]>>{?-͜9sϞ=?tޝEq}fa@E`8T4 994 zA\xEQ5Nj へe<"JQETd;;3 j~3]]]U]IGuWׯĄw^։(oSԩ!E;Q=}ߏA9qΊN:0[cǎ{تV=Gr?U>ySsyÇ_Y=:e=w ĉLBȮ]𒒒wB޽+[aiiH$e^pƍ , !$00p͚5-o3.7a7##&j)NNN7ojg~͛7U*US=z-jmMG}}}aa%5/_lU wܩuttرƮ={B_vD"9s?ŋϟ?BAw1 pBɓ'"""fϞ-BBBd2ٰa_-))?~dd$6L'ggg޽{&nvkn133KNNNJJrtt q%''۷oܹ{illd%hx4$UrpB(X>i=C544(Jccs,M㑨Z[ư[K\z>dF"(qS!!|-ԦM/^lff6rȉ'~˴_W^=y򤷷7d///asVUU7gϞӯ(JMOYݒ8v={=ztݺuy<3RwΝ]O6GUTTGd211}vbbkơ.]YYtҘ+++H@k9;;;_~EPtA}THϟM4);;~1BH`` 0G=}4]w'l ڦh,rM)[߿/:tH-==~.Аֻwo%K4Qd2vҺ+W{8xsӇP( #j܆744DEEx<_pwwްa;t_ZZ1ÖN@BӧO/\0--ظ{^^Ou_bommJQT;vHII177Jmlϛ(dzsNqqM,**o>|PR-%%e˖-stt &DGG߾};00~9;;٥>xٳvvv999ӧO'^绹B|~YY;j~~RJR"(888::k׮}\.OIIQ_])Nj℄~۷O,/uK.MMMҦN*J EFFݬY Y&OqL''7nw |:󻸸LJ:88ܾ}ݻizԩS~7T:p͛7KR@pO>)77]@/^nݺ#FD]^z^,oݺ8p 66/ %%%[dɩSh@z!Cq!CCC//pB(..ѱ)EEE999m޼>V9)-LLL}ɓ'o{?,Y'CTTTjjj+v!KR޽vÆ m/pԩSNm{9~@@>p+6w?!Ga8v !(u8 #pBB@Q8 !G! (pBB@Q8 !G! (pBB@Q8 !G! (О|ҥKXX؉'Z,X  ZRd&99-;b\)eaa1?6B@OxxxBBBBBB\\;un:a=z Unڴ޼ypxbcc0̟ E[n` m/-8qݵkBCC-ZfkkFy&!dժUowA׮]&kBh铕P(֬YRT:h֞;wM{ So󢿡k~~~۳g}f8}tPPPΝE"ѤI~gCCCvjcc3cƌWy= ^pAOjjj/_ޫW/SS=z=0`@hhzkӟP999{j8q"0uuu**772xԸ8TgJu9sB~'5\r%..sN\R-,,"""U*Ç.]l2GGG##ÇBBBz=lذ+W{xx$%%UTTڵh:].=-OMM%TTRJ7iW__SMNN@ 5J$z!߿Gw>qٳ !lhh ͟?>۷/!$>>fx@ ]dIRR 0۶mcOsȐ!>>>{uW_}0Ltt4ݛghhؿX@УGOmmm͛5vXBȌ3^B(Λ7ORxG 輈G644 _bG}B77!!!...lZhS?О iv5J5f;;;BAw 3g͵kB>|H7g@G*++fpp0!dtSTDДں666T^XXH`5jΪ5r:,eJU[[kllb``→T;vB=QEttL&ckTy<^EEyʔ)|>=S 9s MyT*566'#̌B !GQ?qG/^< =033jlltuuurryB ԩɓ'U؅ )w B ?+s0feercccFTxB灭ʬM(N6|9stBS:w<{۷o'''GGG[n4kkږԥ :~8!ٳ˗/ollM3 CCG|ƅ |222СCIII:ޜ'N`MݵkWYY-Ԓ!'~aΝ=zcǎŋٱ_jikUfm" t"tԉE#픖ԥ q'OZYYEDDLFdÆ cBHII###.\Tݛ16Npww@㟄OǍGyx}Bcݻx;sΜ-ג!'._lkkKܴiS߾}-ZTUU5qb{{lUfmhcR 4r/Rz]WUU]zɓ<[&ݿNCUUU7gϞzjW*ډMQ1;+B.]ZϏf64Խ<;1jи##^ƍzfMLL JrݺuR4,,>k-O=!d2٥K,ϟ?4iRvv6;^ԠM27QO?5Mr ^kf[0ѣGO>L]| !AAA4[CCCddduuuAAK.oY4_"U'77zٳ'!ח{޽rPMLKK0ac[nSJgeeZjʔ)ݻwS]]}}Xɓm۶燄BWTT|? F=po[n) D>:tJin0s 999}_}Ǐie˖}_.Ͷ-==~.АֻwoXL-Y0''L&oBV\ijjcǎkkkWXann$333M6a### pwwްa*$ߺuKj600HOOW(111yΝ;קO ) ɵkTVV꩑Ֆ$!hݗ/_&(K.ԤD"B]jjΞ=kggWPP3}t_9rРA3aׯ H$'OVT|o}vOڮ$^绹B|~YY٢Eh!ׯJ?Z<<<4>#D]~wr<%%E}Cu 1bDTTT.]rss/_qF BHZZkTTۿ?ct255MKKcq "##n֬Y؉c㏳%|b8!!_~eeeN+W=:<i6e'X%%%AAAݺu֭[pps]!!!nnnk!| 2w^z FO`\V. 0: 袎R Q[ 5P0?1H ] LeI7W΍E,pdp *OPꦍ "v7x h&:OIG\Z]o..f E hmֶ3}wCv0#Z=o @{ƄMDer N߾ޭC~>n3 \5Nsxw ḍRm{)DHذSʚP 4F%S''^{+wv{'6q죎=PtΧu.Oͱr/ۛ֏9IZL2ɍ\5))>ӌX%czfiYC]Jel M8wم E =Z)d7w=WF|^'b%':-v{9jLqrl#һQ #L~7 ɵ"fC~PK _"= VHREADMEUTKLux KPLeThis is the source code repository for the Go programming language. For documentation about how to install and use Go, visit https://golang.org/ or load doc/install.html in your web browser. After installing Go, you can view a nicely formatted doc/install.html by running godoc --http=:6060 and then visiting http://localhost:6060/doc/install.html. Unless otherwise noted, the Go source files are distributed under the BSD-style license found in the LICENSE file. -- Binary Distribution Notes If you have just untarred a binary Go distribution, you need to set the environment variable $GOROOT to the full path of the go directory (the one containing this README). You can omit the variable if you unpack it into /usr/local/go, or if you rebuild from sources by running all.bash (see doc/install.html). You should also add the Go binary directory $GOROOT/bin to your shell's path. For example, if you extracted the tar file into $HOME/go, you might put the following in your .profile: export GOROOT=$HOME/go export PATH=$PATH:$GOROOT/bin See doc/install.html for more details.rawzip-0.4.4/assets/readme.zip000064400000000000000000000035361046102023000144370ustar 00000000000000PK _"= VHREADMEUT KLLux mTMO@ﯘRAJlN"qR!Ck{loޱ#!3kJ(ϛ7o7{@55G &?BK> FO`\V. 0: 袎R Q[ 5P0?1H ] LeI7W΍E,pdp *OPꦍ "v7x h&:OIG\Z]o..f E hmֶ3}wCv0#Z=o @{ƄMDer N߾ޭC~>n3 \5Nsxw ḍRm{)DHذSʚP 4F%S''^{+wv{'6q죎=PtΧu.Oͱr/ۛ֏9IZL2ɍ\5))>ӌX%czfiYC]Jel M8wم E =Z)d7w=WF|^'b%':-v{9jLqrl#һQ #L~7 ɵ"fC~PK _"= VHREADMEUTKLux PKLeThis is the source code repository for the Go programming language. For documentation about how to install and use Go, visit https://golang.org/ or load doc/install.html in your web browser. After installing Go, you can view a nicely formatted doc/install.html by running godoc --http=:6060 and then visiting http://localhost:6060/doc/install.html. Unless otherwise noted, the Go source files are distributed under the BSD-style license found in the LICENSE file. -- Binary Distribution Notes If you have just untarred a binary Go distribution, you need to set the environment variable $GOROOT to the full path of the go directory (the one containing this README). You can omit the variable if you unpack it into /usr/local/go, or if you rebuild from sources by running all.bash (see doc/install.html). You should also add the Go binary directory $GOROOT/bin to your shell's path. For example, if you extracted the tar file into $HOME/go, you might put the following in your .profile: export GOROOT=$HOME/go export PATH=$PATH:$GOROOT/bin See doc/install.html for more details.rawzip-0.4.4/assets/symlink.zip000064400000000000000000000002551046102023000146630ustar 00000000000000PK C@ symlinkUT  X,O X,Oux ../targetPK C@ symlinkUT X,Oux PKMJrawzip-0.4.4/assets/test-badbase.zip000064400000000000000000000022221046102023000155270ustar 00000000000000PKa%=test.txtUT qLvLux  ,VD QQPK ~%=1Tgophercolor16x16.pngUT :0L;0Lux PNG  IHDRsO/gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATxt_HSQ{u[V*%mlQV_#""zAK bE/zI*5d./S鶆(]^uٟ9{?}9Cvlϝ:z#Xeėe%VWv̏Vhd4oCil&`h555^@g @:UpQWVöX,J6kI(ecuwoVaY a'|^|l'~T'\ϰoȉ/ZN 6f9G5ق&/+o10ȇVʊABc`Àk6zX73'2)2gL02D~ã=2eAyLchYOŹTdBqvHhp-vU>ܫCvK36N*Zi  ꬤIf5 *Hɘf0T5 ѤR(g`/dF* ?jW4nS hNHڲPKUE!PK3I[ META-INF/PK3I[kDE=META-INF/MANIFEST.MFPK.I[UE!HelloWorld.classPK"rawzip-0.4.4/assets/test.zip000064400000000000000000000022221046102023000141500ustar 00000000000000PKa%=test.txtUT qLvLux  ,VD QQPK ~%=1Tgophercolor16x16.pngUT :0L;0Lux PNG  IHDRsO/gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDATxt_HSQ{u[V*%mlQV_#""zAK bE/zI*5d./S鶆(]^uٟ9{?}9Cvlϝ:z#Xeėe%VWv̏Vhd4oCil&`h555^@g @:UpQWVöX,J6kI(ecuwoVaY a'|^|l'~T'\ϰoȉ/ZN 6f9G5ق&/+o10ȇVʊAB Self { ZeroReader { remaining: size } } } impl Read for ZeroReader { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { if self.remaining == 0 { return Ok(0); } let len = std::cmp::min(buf.len() as u64, self.remaining) as usize; buf[..len].fill(0); self.remaining -= len as u64; Ok(len) } } fn main() -> Result<(), Box> { let output_file = std::fs::File::create("big.zip")?; let writer = std::io::BufWriter::new(output_file); let mut archive = ZipArchiveWriter::new(writer); // Add 100,000 small files with Store compression for i in 0..100_000 { let filename = format!("file_{:05}.txt", i); let (mut entry, config) = archive .new_file(&filename) .compression_method(rawzip::CompressionMethod::Store) .start()?; let mut data_writer = config.wrap(&mut entry); data_writer.write_all(b"x")?; let (_, output) = data_writer.finish()?; entry.finish(output)?; } println!(" Added 100,000 small files"); println!(" Adding 5GB zero file with Zstd compression..."); let (mut big_entry, config) = archive .new_file("big_zeros.dat") .compression_method(rawzip::CompressionMethod::Zstd) .start()?; let encoder = zstd::Encoder::new(&mut big_entry, 3)?; // Compression level 3 let mut data_writer = config.wrap(encoder); let mut zero_reader = ZeroReader::new(5 * 1024 * 1024 * 1024); // 5GB std::io::copy(&mut zero_reader, &mut data_writer)?; let (encoder, output) = data_writer.finish()?; encoder.finish()?; big_entry.finish(output)?; archive.finish()?; println!("Successfully created big.zip with 100,001 entries"); let zip_file = std::fs::File::open("big.zip")?; let mut buffer = vec![0; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = rawzip::ZipArchive::from_file(zip_file, &mut buffer)?; assert_eq!( archive.entries_hint(), 100_001, "Expected 100,001 entries in the archive" ); let mut big_file_wayfinder = None; let mut big_file_compression = rawzip::CompressionMethod::Store; let mut entries = archive.entries(&mut buffer); let mut entry_count = 0; while let Some(entry) = entries.next_entry()? { entry_count += 1; if entry.file_path().as_ref() == b"big_zeros.dat" { big_file_wayfinder = Some(entry.wayfinder()); big_file_compression = entry.compression_method(); break; } } assert_eq!( entry_count, 100_001, "Expected 100,001 entries in the archive" ); let wayfinder = big_file_wayfinder.expect("big_zeros.dat wayfinder not found"); let big_file_size = wayfinder.uncompressed_size_hint(); assert_eq!( big_file_size, 5 * 1024 * 1024 * 1024, "Expected big_zeros.dat to be 5GB" ); let zip_entry = archive.get_entry(wayfinder)?; let reader = zip_entry.reader(); assert_eq!( big_file_compression, rawzip::CompressionMethod::Zstd, "Expected big_zeros.dat to use Zstd compression" ); let decoder = zstd::Decoder::new(reader)?; let mut reader = zip_entry.verifying_reader(decoder); let total_read = std::io::copy(&mut reader, &mut std::io::sink())?; assert_eq!( total_read, 5 * 1024 * 1024 * 1024, "Expected to read exactly 5GB from big_zeros.dat" ); Ok(()) } rawzip-0.4.4/examples/extract.rs000064400000000000000000000316311046102023000150070ustar 00000000000000//! This example demonstrates how to safely extract ZIP archives. It implements //! several security measures to prevent common ZIP-based attacks while //! providing a basic ZIP extraction. Limitations of this example (but not of //! rawzip). //! //! - Supports only store and deflate compression methods //! - Supports only UTF-8 file paths fn main() -> Result<(), Box> { let args: Vec = std::env::args().collect(); if args.len() != 3 { eprintln!("Usage: {} ", args[0]); std::process::exit(1); } let archive_path = &args[1]; let target_dir = &args[2]; extract_zip_archive(archive_path, target_dir)?; Ok(()) } fn extract_zip_archive>( archive_path: P, target_dir: P, ) -> Result<(), ExtractionError> { use rawzip::{CompressionMethod, ZipArchive, RECOMMENDED_BUFFER_SIZE}; let archive_path = archive_path.as_ref(); let target_dir = target_dir.as_ref(); // Create target directory if it doesn't exist if !target_dir.exists() { std::fs::create_dir_all(target_dir).map_err(|e| { ExtractionError::io_context( e, format!( "Failed to create target directory: {}", target_dir.display() ), ) })?; } let file = std::fs::File::open(archive_path).map_err(|e| { ExtractionError::io_context( e, format!("Failed to open ZIP archive: {}", archive_path.display()), ) })?; let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(file, &mut buffer).map_err(|e| { ExtractionError::zip_context( e, format!("Failed to read ZIP archive: {}", archive_path.display()), ) })?; let mut zip_start_offset = archive.directory_offset(); // Maintain sorted list of compressed data ranges to detect overlaps: // https://www.bamsoftware.com/hacks/zipbomb/ let mut compressed_ranges = Vec::new(); let expected_entries = archive.entries_hint(); let mut entries_processed = 0u64; let mut entries = archive.entries(&mut buffer); loop { let entry = match entries.next_entry() { Ok(Some(entry)) => entry, Ok(None) => break, Err(e) => { // When an error is encountered, if we have processed the // expected number of entries, we treat this as reaching the end // rather than an error. For the vast majority of zips, this is // not necessary but is included for completeness. if entries_processed == expected_entries { break; } else { return Err(ExtractionError::zip_context( e, "Failed to read ZIP entry".to_string(), )); } } }; entries_processed += 1; let raw_path = entry.file_path(); // Avoid zip slips by normalizing the path. Note that it is not required for // zip file paths to be UTF-8 let file_path = match raw_path.try_normalize() { Ok(p) => p, Err(e) => { eprintln!("Skipped suspicious path: {raw_path:?}, reason: {e}"); continue; } }; let out_path = target_dir.join(file_path.as_ref()); let zip_entry = archive.get_entry(entry.wayfinder()).map_err(|e| { ExtractionError::zip_context( e, format!("Failed to get ZIP entry for file: {}", file_path.as_ref()), ) })?; zip_start_offset = entry.local_header_offset().min(zip_start_offset); if entry.is_dir() { std::fs::create_dir_all(&out_path).map_err(|e| { ExtractionError::io_context( e, format!("Failed to create directory: {}", out_path.display()), ) })?; continue; } if let Some(parent) = out_path.parent() { std::fs::create_dir_all(parent).map_err(|e| { ExtractionError::io_context( e, format!("Failed to create parent directory: {}", parent.display()), ) })?; } let reader = zip_entry.reader(); // Check for overlapping compressed data ranges let current_range = zip_entry.compressed_data_range(); let (current_start, current_end) = current_range; // Find insertion point to maintain sorted order by start offset let insert_pos = compressed_ranges .binary_search_by_key(¤t_start, |&(start, _)| start) .unwrap_or_else(|pos| pos); // Check overlap with previous range (if exists) if insert_pos > 0 { let (_, prev_end) = compressed_ranges[insert_pos - 1]; if prev_end > current_start { eprintln!("Skipped file with overlapping compressed data: {file_path:?} (range {}..{} overlaps with previous range ending at {})", current_start, current_end, prev_end); continue; } } // Check overlap with next range (if exists) if insert_pos < compressed_ranges.len() { let (next_start, _) = compressed_ranges[insert_pos]; if current_end > next_start { eprintln!("Skipped file with overlapping compressed data: {file_path:?} (range {}..{} overlaps with next range starting at {})", current_start, current_end, next_start); continue; } } // Insert the range at the correct position to maintain sorted order compressed_ranges.insert(insert_pos, current_range); // "DEFLATE, the compression algorithm most commonly supported by zip // parsers, cannot achieve a compression ratio greater than 1032" // https://www.bamsoftware.com/hacks/zipbomb/ let compressed_size = entry.compressed_size_hint(); let uncompressed_size = entry.uncompressed_size_hint(); if compressed_size > 0 && uncompressed_size / compressed_size > 1032 { eprintln!("Skipped potential zip bomb: compression ratio {:.1}:1 exceeds limit of 1032:1 for file: {file_path:?}", uncompressed_size as f64 / compressed_size as f64); continue; } let mut outfile = std::fs::File::create(&out_path).map_err(|e| { ExtractionError::io_context( e, format!("Failed to create output file: {}", out_path.display()), ) })?; let method = entry.compression_method(); match method { CompressionMethod::Store => { let mut verifier = zip_entry.verifying_reader(reader); std::io::copy(&mut verifier, &mut outfile).map_err(|e| { ExtractionError::io_context( e, format!( "Failed to extract uncompressed file: {}", file_path.as_ref() ), ) })?; } CompressionMethod::Deflate => { let inflater = flate2::read::DeflateDecoder::new(reader); let mut verifier = zip_entry.verifying_reader(inflater); std::io::copy(&mut verifier, &mut outfile).map_err(|e| { ExtractionError::io_context( e, format!("Failed to extract deflated file: {}", file_path.as_ref()), ) })?; } _ => { eprintln!("Unsupported compression method {method:?} for file: {file_path:?}"); continue; } } match entry.last_modified() { rawzip::time::ZipDateTimeKind::Utc(dt) => { let mtime = filetime::FileTime::from_unix_time(dt.to_unix(), dt.nanosecond()); filetime::set_file_mtime(&out_path, mtime).map_err(|e| { ExtractionError::io_context( e, format!( "Failed to set file modification time for: {}", out_path.display() ), ) })?; } rawzip::time::ZipDateTimeKind::Local(dt) if dt.year() > 1980 => { // We only want to write out timestamps that are more recent // than 1980 (which is the start date for the msdos timestamp // format used in zip files). // Convert local time to UTC by treating it as it was UTC. This // is something you may (or may not) want to do too. let utc_time = rawzip::time::UtcDateTime::from_components( dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second(), dt.nanosecond(), ); match utc_time { Some(utc_time) => { let mtime = filetime::FileTime::from_unix_time( utc_time.to_unix(), utc_time.nanosecond(), ); filetime::set_file_mtime(&out_path, mtime).map_err(|e| { ExtractionError::io_context( e, format!( "Failed to set file modification time for: {}", out_path.display() ), ) })?; } None => { eprintln!("Invalid local time for file: {file_path:?}, skipping timestamp setting"); } } } _ => {} }; // Set file attributes based on platform #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mode = entry.mode(); std::fs::set_permissions( &out_path, std::fs::Permissions::from_mode(mode.permissions()), ) .map_err(|e| { ExtractionError::io_context( e, format!("Failed to set file permissions for: {}", out_path.display()), ) })?; } #[cfg(windows)] { // Detect if the file should be marked as readonly if entry.mode().permissions() & 0o200 == 0 { let mut perms = std::fs::metadata(&out_path) .map_err(|e| { ExtractionError::io_context( e, format!("Failed to read file metadata for: {}", out_path.display()), ) })? .permissions(); perms.set_readonly(true); std::fs::set_permissions(&out_path, perms).map_err(|e| { ExtractionError::io_context( e, format!( "Failed to set readonly attribute for: {}", out_path.display() ), ) })?; } } } if zip_start_offset > 0 { println!("ZIP starting offset: {}", zip_start_offset); } Ok(()) } #[derive(Debug)] enum ExtractionError { ZipError { error: rawzip::Error, context: String, }, IoError { error: std::io::Error, context: String, }, } impl std::fmt::Display for ExtractionError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ExtractionError::ZipError { error, context } => { write!(f, "{}: {}", context, error) } ExtractionError::IoError { error, context } => { write!(f, "{}: {}", context, error) } } } } impl std::error::Error for ExtractionError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { ExtractionError::ZipError { error, .. } => Some(error), ExtractionError::IoError { error, .. } => Some(error), } } } impl ExtractionError { fn zip_context(error: rawzip::Error, context: String) -> Self { ExtractionError::ZipError { error, context } } fn io_context(error: std::io::Error, context: String) -> Self { ExtractionError::IoError { error, context } } } rawzip-0.4.4/examples/list.rs000064400000000000000000000075561046102023000143210ustar 00000000000000use rawzip::{ZipArchive, RECOMMENDED_BUFFER_SIZE}; use std::env; use std::fs::File; use std::io::Write; fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); if args.len() != 2 { eprintln!("Usage: {} ", args[0]); eprintln!("List the contents of a ZIP archive"); std::process::exit(1); } let archive_path = &args[1]; let file = File::open(archive_path)?; let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(file, &mut buffer)?; println!("Archive: {}", archive_path); let mut comment_reader = archive.comment(); if comment_reader.remaining() > 0 { print!("Comment: "); std::io::copy(&mut comment_reader, &mut std::io::stdout().lock())?; println!(); } println!(); println!(" Length Date/Time Perms Name"); println!("--------- -------------------- ---------- -------"); let mut total_uncompressed = 0u64; let mut total_compressed = 0u64; let mut file_count = 0u64; let expected_entries = archive.entries_hint(); let mut entries = archive.entries(&mut buffer); loop { let entry = match entries.next_entry() { Ok(Some(entry)) => entry, Ok(None) => break, Err(e) => { if file_count == expected_entries { break; } else { return Err(e.into()); } } }; let uncompressed_size = entry.uncompressed_size_hint(); let compressed_size = entry.compressed_size_hint(); total_uncompressed += uncompressed_size; total_compressed += compressed_size; file_count += 1; // Format permissions let mode = entry.mode(); let permissions_str = format_permissions(mode.value()); // Show uncompressed size, or empty for directories let size_str = if entry.is_dir() { format!("{:9}", "") } else { format!("{:9}", uncompressed_size) }; print!( "{} {:20} {:10} ", size_str, entry.last_modified(), permissions_str ); std::io::stdout().write_all(entry.file_path().as_ref())?; println!(); } println!("--------- -------------------- ---------- -------"); println!( "{:9} {} files", total_uncompressed, file_count ); if total_compressed > 0 && total_uncompressed > 0 { let compression_ratio = (total_compressed as f64 / total_uncompressed as f64) * 100.0; println!( "Compressed size: {} bytes ({:.1}%)", total_compressed, compression_ratio ); } Ok(()) } fn format_permissions(mode: u32) -> String { let file_type = match mode & 0o170000 { 0o040000 => 'd', // Directory 0o120000 => 'l', // Symbolic link 0o100000 => '-', // Regular file 0o060000 => 'b', // Block device 0o020000 => 'c', // Character device 0o010000 => 'p', // FIFO 0o140000 => 's', // Socket _ => '?', // Unknown }; let owner = format!( "{}{}{}", if mode & 0o400 != 0 { 'r' } else { '-' }, if mode & 0o200 != 0 { 'w' } else { '-' }, if mode & 0o100 != 0 { 'x' } else { '-' } ); let group = format!( "{}{}{}", if mode & 0o040 != 0 { 'r' } else { '-' }, if mode & 0o020 != 0 { 'w' } else { '-' }, if mode & 0o010 != 0 { 'x' } else { '-' } ); let other = format!( "{}{}{}", if mode & 0o004 != 0 { 'r' } else { '-' }, if mode & 0o002 != 0 { 'w' } else { '-' }, if mode & 0o001 != 0 { 'x' } else { '-' } ); format!("{}{}{}{}", file_type, owner, group, other) } rawzip-0.4.4/examples/read.rs000064400000000000000000000044071046102023000142510ustar 00000000000000use rawzip::{ZipArchive, RECOMMENDED_BUFFER_SIZE}; use std::env; use std::fs::File; use std::io; fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); if args.len() < 2 { eprintln!("Usage: {} [filename]", args[0]); eprintln!("Extract a file from a ZIP archive to stdout"); eprintln!("If no filename is provided, extracts the last file in the central directory"); std::process::exit(1); } let archive_path = &args[1]; let target_filename = args.get(2); let file = File::open(archive_path)?; let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(file, &mut buffer)?; let mut found_entry = None; let mut entries = archive.entries(&mut buffer); while let Some(entry) = entries.next_entry()? { if entry.is_dir() { continue; } match target_filename { Some(name) if name.as_bytes() == entry.file_path().as_ref() => { found_entry = Some((entry.wayfinder(), entry.compression_method())); break; } None => { found_entry = Some((entry.wayfinder(), entry.compression_method())); } _ => {} } } let Some((wayfinder, compression_method)) = found_entry else { eprintln!("File not found in archive"); std::process::exit(1); }; let zip_entry = archive.get_entry(wayfinder)?; let reader = zip_entry.reader(); let stdout = io::stdout(); let mut stdout_lock = stdout.lock(); match compression_method { rawzip::CompressionMethod::Store => { let mut verifier = zip_entry.verifying_reader(reader); io::copy(&mut verifier, &mut stdout_lock)?; } rawzip::CompressionMethod::Deflate => { let inflater = flate2::read::DeflateDecoder::new(reader); let mut verifier = zip_entry.verifying_reader(inflater); io::copy(&mut verifier, &mut stdout_lock)?; } _ => { eprintln!( "Error: Unsupported compression method: {:?}", compression_method ); std::process::exit(1); } } Ok(()) } rawzip-0.4.4/examples/write.rs000064400000000000000000000132241046102023000144650ustar 00000000000000use rawzip::ZipArchiveWriter; use std::env; use std::fs::{self, File}; use std::io::Write; use std::path::Path; use std::time::UNIX_EPOCH; fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); // Parse flags and arguments let mut use_zstd = false; let mut positional_args = Vec::new(); for arg in &args[1..] { if arg == "--zstd" { use_zstd = true; } else { positional_args.push(arg.as_str()); } } if positional_args.len() < 2 { eprintln!("Usage: {} [--zstd] ...", args[0]); eprintln!("Create a ZIP archive from the specified files and directories"); eprintln!(); eprintln!("Options:"); eprintln!(" --zstd Use zstd compression (level 3) instead of deflate"); std::process::exit(1); } let compression_method = if use_zstd { rawzip::CompressionMethod::Zstd } else { rawzip::CompressionMethod::Deflate }; let output_path = positional_args[0]; let input_paths = &positional_args[1..]; let output_file = File::create(output_path)?; let writer = std::io::BufWriter::new(output_file); let mut archive = ZipArchiveWriter::new(writer); for input_path in input_paths { let path = Path::new(input_path); if path.is_file() { add_file_to_archive( &mut archive, path, path.file_name().unwrap().to_str().unwrap(), compression_method, )?; } else if path.is_dir() { add_directory_to_archive(&mut archive, path, "", compression_method)?; } else { eprintln!( "Warning: '{}' does not exist or is not a regular file/directory", input_path ); } } archive.finish()?; println!("Successfully created '{}'", output_path); Ok(()) } fn get_modification_time( metadata: &fs::Metadata, ) -> Result> { let modified = metadata.modified()?; // Convert system time to UTC DateTime let unix_seconds = modified.duration_since(UNIX_EPOCH)?.as_secs() as i64; Ok(rawzip::time::UtcDateTime::from_unix(unix_seconds)) } fn add_file_to_archive( archive: &mut ZipArchiveWriter, file_path: &Path, archive_path: &str, compression_method: rawzip::CompressionMethod, ) -> Result<(), Box> { let metadata = fs::metadata(file_path)?; let modification_time = get_modification_time(&metadata)?; let mut builder = archive .new_file(archive_path) .compression_method(compression_method) .last_modified(modification_time); if let Some(permissions) = get_unix_permissions(&metadata) { builder = builder.unix_permissions(permissions); } // Read and compress the file content let mut file = fs::File::open(file_path)?; let (mut entry, config) = builder.start()?; match compression_method { rawzip::CompressionMethod::Deflate => { let encoder = flate2::write::DeflateEncoder::new(&mut entry, flate2::Compression::default()); let mut writer = config.wrap(encoder); std::io::copy(&mut file, &mut writer)?; let (encoder, output) = writer.finish()?; encoder.finish()?; entry.finish(output)?; } rawzip::CompressionMethod::Zstd => { let encoder = zstd::Encoder::new(&mut entry, 3)?; let mut writer = config.wrap(encoder); std::io::copy(&mut file, &mut writer)?; let (encoder, output) = writer.finish()?; encoder.finish()?; entry.finish(output)?; } _ => { return Err("Unsupported compression method".into()); } } println!(" adding: {}", archive_path); Ok(()) } fn add_directory_to_archive( archive: &mut ZipArchiveWriter, dir_path: &Path, base_path: &str, compression_method: rawzip::CompressionMethod, ) -> Result<(), Box> { let entries = fs::read_dir(dir_path)?; for entry in entries { let entry = entry?; let path = entry.path(); let name = entry.file_name(); let name_str = name.to_str().unwrap(); let archive_path = if base_path.is_empty() { name_str.to_string() } else { format!("{}/{}", base_path, name_str) }; if path.is_file() { add_file_to_archive(archive, &path, &archive_path, compression_method)?; } else if path.is_dir() { // Add directory entry let metadata = fs::metadata(&path)?; let modification_time = get_modification_time(&metadata)?; let dir_archive_path = format!("{}/", archive_path); let mut builder = archive .new_dir(&dir_archive_path) .last_modified(modification_time); if let Some(permissions) = get_unix_permissions(&metadata) { builder = builder.unix_permissions(permissions); } builder.create()?; println!(" adding: {}", dir_archive_path); // Recursively add directory contents add_directory_to_archive(archive, &path, &archive_path, compression_method)?; } } Ok(()) } #[cfg(unix)] fn get_unix_permissions(metadata: &fs::Metadata) -> Option { use std::os::unix::fs::PermissionsExt; Some(metadata.permissions().mode()) } #[cfg(not(unix))] fn get_unix_permissions(_metadata: &fs::Metadata) -> Option { None } rawzip-0.4.4/src/archive.rs000064400000000000000000002070121046102023000137250ustar 00000000000000use crate::crc::crc32_chunk; use crate::errors::{Error, ErrorKind}; use crate::extra_fields::{ExtraFieldId, ExtraFields}; use crate::mode::{ msdos_mode_to_file_mode, unix_mode_to_file_mode, EntryMode, CREATOR_FAT, CREATOR_MACOS, CREATOR_NTFS, CREATOR_UNIX, CREATOR_VFAT, }; use crate::path::{RawPath, ZipFilePath}; use crate::reader_at::{FileReader, MutexReader, RangeReader, ReaderAt, ReaderAtExt}; use crate::time::{extract_best_timestamp, ZipDateTimeKind}; use crate::utils::{le_u16, le_u32, le_u64}; use crate::{EndOfCentralDirectory, EndOfCentralDirectoryRecordFixed, ZipLocator}; use std::io::{Read, Seek, Write}; pub(crate) const END_OF_CENTRAL_DIR_SIGNATURE64: u32 = 0x06064b50; pub(crate) const END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE: u32 = 0x07064b50; pub(crate) const CENTRAL_HEADER_SIGNATURE: u32 = 0x02014b50; /// The recommended buffer size to use when reading from a zip file. /// /// This buffer size was chosen as it can hold an entire central directory /// record as the spec states (4.4.10): /// /// > the combined length of any directory and these three fields SHOULD NOT /// > generally exceed 65,535 bytes. pub const RECOMMENDED_BUFFER_SIZE: usize = 1 << 16; /// Represents a Zip archive that operates on an in-memory data. /// /// A [`ZipSliceArchive`] is more efficient and easier to use than a [`ZipArchive`], /// as there is no buffer management and memory copying involved. /// /// # Examples /// /// ```rust /// use rawzip::{ZipArchive, ZipSliceArchive, Error}; /// /// fn process_zip_slice(data: &[u8]) -> Result<(), Error> { /// let archive = ZipArchive::from_slice(data)?; /// println!("Found {} entries.", archive.entries_hint()); /// for entry_result in archive.entries() { /// let entry = entry_result?; /// println!("File: {}", entry.file_path().try_normalize()?.as_ref()); /// } /// Ok(()) /// } /// ``` #[derive(Debug, Clone)] pub struct ZipSliceArchive { data: T, eocd: EndOfCentralDirectory, } impl> ZipSliceArchive { pub(crate) fn new(data: T, eocd: EndOfCentralDirectory) -> Self { ZipSliceArchive { data, eocd } } /// Returns an iterator over the entries in the central directory of the archive. pub fn entries(&self) -> ZipSliceEntries<'_> { let data = self.data.as_ref(); let directory_start = self.eocd.directory_offset(); let entry_data = &data[(directory_start as usize)..self.eocd.head_eocd_offset() as usize]; ZipSliceEntries { entry_data, base_offset: self.eocd.base_offset(), current_offset: directory_start, } } /// Returns the byte slice that represents the zip file. /// /// This will include the entire input slice. pub fn as_bytes(&self) -> &[u8] { self.data.as_ref() } /// Returns a hint for the total number of entries in the archive. /// /// This value is read from the End of Central Directory record. pub fn entries_hint(&self) -> u64 { self.eocd.entries() } /// Returns the offset of the End of Central Directory (EOCD) signature. /// /// See [`ZipArchive::eocd_offset()`] for more details. pub fn eocd_offset(&self) -> u64 { self.eocd.tail_eocd_offset() } /// The declared offset of the start of the central directory. /// /// See [`ZipArchive::directory_offset()`] for more details. pub fn directory_offset(&self) -> u64 { self.eocd.directory_offset() } /// Returns the offset where the ZIP archive ends. /// /// See [`ZipArchive::end_offset`] for more details. pub fn end_offset(&self) -> u64 { self.eocd.tail_eocd_offset() + EndOfCentralDirectoryRecordFixed::SIZE as u64 + self.comment().as_bytes().len() as u64 } /// The comment of the zip file. pub fn comment(&self) -> ZipStr<'_> { let data = self.data.as_ref(); let comment_start = self.eocd.tail_eocd_offset() as usize + EndOfCentralDirectoryRecordFixed::SIZE; let comment_len = self.eocd.comment_len(); ZipStr::new(&data[comment_start..comment_start + comment_len]) } /// Converts the [`ZipSliceArchive`] into a general [`ZipArchive`]. /// /// This is useful for unifying code that might handle both slice-based /// and reader-based archives. #[deprecated(note = "Use `ZipSliceArchive::into_zip_archive` instead")] pub fn into_reader(self) -> ZipArchive { ZipArchive { reader: self.data, eocd: self.eocd, } } /// Converts the [`ZipSliceArchive`] into a general [`ZipArchive`]. /// /// This is useful for unifying code that might handle both slice-based and /// reader-based archives. The data is wrapped in a [`std::io::Cursor`] to /// provide the [`ReaderAt`] implementation needed for [`ZipArchive`]. pub fn into_zip_archive(self) -> ZipArchive> { ZipArchive { reader: std::io::Cursor::new(self.data), eocd: self.eocd, } } /// Seeks to the given file entry in the zip archive. /// /// See [`ZipArchive::get_entry`] for more details. The biggest difference /// between the reader and slice APIs is that the slice APIs will eagerly /// validate that the entire compressed data is present. pub fn get_entry(&self, entry: ZipArchiveEntryWayfinder) -> Result, Error> { let data = self.data.as_ref(); let header = &data[(entry.local_header_offset as usize).min(data.len())..]; let file_header = ZipLocalFileHeaderFixed::parse(header)?; let variable_length = file_header.variable_length(); let header_size = (ZipLocalFileHeaderFixed::SIZE + variable_length) as u32; let (total_size, o1) = (u64::from(header_size)).overflowing_add(entry.compressed_size_hint()); if o1 || (header.len() as u64) < total_size { return Err(Error::from(ErrorKind::Eof)); } let (entire_entry, rest) = header.split_at(total_size as usize); let expected_crc = if entry.has_data_descriptor { DataDescriptor::parse(rest)?.crc } else { entry.crc }; Ok(ZipSliceEntry { data: entire_entry, verifier: ZipVerification { crc: expected_crc, uncompressed_size: entry.uncompressed_size_hint(), }, local_header_offset: entry.local_header_offset, data_start_offset: header_size, }) } } /// Represents a single entry (file or directory) within a `ZipSliceArchive`. /// /// It provides access to the raw compressed data of the entry. #[derive(Debug, Clone)] pub struct ZipSliceEntry<'a> { // From local header offset to end of compressed data data: &'a [u8], verifier: ZipVerification, local_header_offset: u64, // self.data[self.data_start_offset] is the start of compressed data data_start_offset: u32, } impl<'a> ZipSliceEntry<'a> { /// Returns the raw, compressed data of the entry as a byte slice. pub fn data(&self) -> &'a [u8] { &self.data[self.data_start_offset as usize..] } /// Returns a verifier for the CRC and uncompressed size of the entry. /// /// Useful when it's more practical to oneshot decompress the data, /// otherwise use [`ZipSliceEntry::verifying_reader`] to stream /// decompression and verification. pub fn claim_verifier(&self) -> ZipVerification { self.verifier } /// Returns a reader that wraps a decompressor and verify the size and CRC /// of the decompressed data once finished. pub fn verifying_reader(&self, reader: D) -> ZipSliceVerifier where D: std::io::Read, { ZipSliceVerifier { reader, verifier: self.verifier, crc: 0, size: 0, } } /// Returns the byte range of the compressed data within the archive. /// /// See [`ZipEntry::compressed_data_range`] for more details. pub fn compressed_data_range(&self) -> (u64, u64) { let compressed_data_start = self.local_header_offset + self.data_start_offset as u64; let compressed_data_end = compressed_data_start + (self.data.len() - self.data_start_offset as usize) as u64; (compressed_data_start, compressed_data_end) } /// Returns an iterator over the extra fields from the local file header. /// /// See [`ZipLocalFileHeader`] for more details. pub fn extra_fields(&self) -> ExtraFields<'_> { let header = ZipLocalFileHeaderFixed::parse(self.data).expect("header has already been parsed"); let file_name_len = header.file_name_len as usize; let extra_field_len = header.extra_field_len as usize; let extra_field_start = ZipLocalFileHeaderFixed::SIZE + file_name_len; let extra_field_end = extra_field_start + extra_field_len; ExtraFields::new(&self.data[extra_field_start..extra_field_end]) } /// Returns the file path from the local file header. /// /// See [`ZipLocalFileHeader`] for more details. pub fn file_path(&self) -> ZipFilePath> { let header = ZipLocalFileHeaderFixed::parse(self.data).expect("header has already been parsed"); let file_name_len = header.file_name_len as usize; let filename_start = ZipLocalFileHeaderFixed::SIZE; let filename_end = filename_start + file_name_len; ZipFilePath::from_bytes(&self.data[filename_start..filename_end]) } } /// Verifies the wrapped reader returns the expected CRC and uncompressed size #[derive(Debug, Clone)] pub struct ZipSliceVerifier { reader: D, crc: u32, size: u64, verifier: ZipVerification, } impl ZipSliceVerifier { /// Consumes the `ZipSliceVerifier`, returning the underlying reader. pub fn into_inner(self) -> D { self.reader } } impl std::io::Read for ZipSliceVerifier where D: std::io::Read, { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read = self.reader.read(buf)?; self.crc = crc32_chunk(&buf[..read], self.crc); self.size += read as u64; if read == 0 || self.size >= self.verifier.size() { self.verifier .valid(ZipVerification { crc: self.crc, uncompressed_size: self.size, }) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; } Ok(read) } } /// An iterator over the central directory file header records. /// /// Created from [`ZipSliceArchive::entries`]. #[derive(Debug, Clone)] pub struct ZipSliceEntries<'data> { entry_data: &'data [u8], base_offset: u64, current_offset: u64, } impl<'data> ZipSliceEntries<'data> { /// Yield the next zip file entry in the central directory if there is any #[inline] pub fn next_entry(&mut self) -> Result>, Error> { if self.entry_data.is_empty() { return Ok(None); } let file_header = ZipFileHeaderFixed::parse(self.entry_data)?; let Some((file_name, extra_field, file_comment, entry_data)) = file_header.parse_variable_length(&self.entry_data[ZipFileHeaderFixed::SIZE..]) else { return Err(Error::from(ErrorKind::Eof)); }; let mut entry = ZipFileHeaderRecord::from_parts( file_header, file_name, extra_field, file_comment, self.current_offset, ); entry.local_header_offset += self.base_offset; self.current_offset += (self.entry_data.len() - entry_data.len()) as u64; self.entry_data = entry_data; Ok(Some(entry)) } } impl<'data> Iterator for ZipSliceEntries<'data> { type Item = Result, Error>; #[inline] fn next(&mut self) -> Option { self.next_entry().transpose() } } /// The main entrypoint for reading a Zip archive. /// /// It can be created from a slice, a file, or any `Read + Seek` source. /// /// # Examples /// /// Creating from a file: /// /// ```rust /// # use rawzip::{ZipArchive, Error, RECOMMENDED_BUFFER_SIZE}; /// # use std::fs::File; /// # use std::io; /// fn example_from_file(file: File) -> Result<(), Error> { /// let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; /// let archive = ZipArchive::from_file(file, &mut buffer)?; /// Ok(()) /// } /// ``` /// /// For more complex use cases, use the [`ZipLocator`] to locate an archive. #[derive(Debug, Clone)] pub struct ZipArchive { reader: R, eocd: EndOfCentralDirectory, } impl ZipArchive<()> { /// Creates a [`ZipLocator`] configured with a maximum search space for the /// End of Central Directory Record (EOCD). pub fn with_max_search_space(max_search_space: u64) -> ZipLocator { ZipLocator::new().max_search_space(max_search_space) } /// Parses an archive from in-memory data. pub fn from_slice>(data: T) -> Result, Error> { ZipLocator::new().locate_in_slice(data).map_err(|(_, e)| e) } /// Parses an archive from a file by reading the End of Central Directory. /// /// A buffer is required to read parts of the file. /// [`RECOMMENDED_BUFFER_SIZE`] can be used to construct this buffer. pub fn from_file( file: std::fs::File, buffer: &mut [u8], ) -> Result, Error> { ZipLocator::new() .locate_in_file(file, buffer) .map_err(|(_, e)| e) } /// Parses an archive from a seekable reader. /// /// Prefer [`ZipArchive::from_file`] and [`ZipArchive::from_slice`] when /// possible, as they are more efficient due to not wrapping the underlying /// reader in a mutex to support positioned io. /// /// ```rust /// # use rawzip::{ZipArchive, Error, RECOMMENDED_BUFFER_SIZE, ZipFileHeaderRecord}; /// # use std::io::Cursor; /// fn example(zip_data: &[u8]) -> Result<(), Error> { /// let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; /// let archive = ZipArchive::from_seekable(Cursor::new(zip_data), &mut buffer)?; /// Ok(()) /// } /// ``` pub fn from_seekable( mut reader: R, buffer: &mut [u8], ) -> Result>, Error> where R: Read + Seek, { let end_offset = reader.seek(std::io::SeekFrom::End(0))?; let reader = MutexReader::new(reader); ZipLocator::new() .locate_in_reader(reader, buffer, end_offset) .map_err(|(_, e)| e) } } impl ZipArchive { pub(crate) fn new(reader: R, eocd: EndOfCentralDirectory) -> Self { ZipArchive { reader, eocd } } /// Returns a reference to the underlying reader. pub fn get_ref(&self) -> &R { &self.reader } /// Consumes this archive and returns the underlying reader. pub fn into_inner(self) -> R { self.reader } /// Returns a lending iterator over the entries in the central directory of /// the archive. /// /// Requires a mutable buffer to read directory entries from the underlying /// reader. /// /// ```rust /// # use rawzip::{ZipArchive, Error, RECOMMENDED_BUFFER_SIZE, ZipFileHeaderRecord}; /// # use std::fs::File; /// fn example(file: File) -> Result<(), Error> { /// let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; /// let archive = ZipArchive::from_file(file, &mut buffer)?; /// let entries_hint = archive.entries_hint(); /// let mut actual_entries = 0; /// let mut entries_iterator = archive.entries(&mut buffer); /// while let Some(_) = entries_iterator.next_entry()? { /// actual_entries += 1; /// } /// println!("Found {} entries (hint: {})", actual_entries, entries_hint); /// Ok(()) /// } /// ``` pub fn entries<'archive, 'buf>( &'archive self, buffer: &'buf mut [u8], ) -> ZipEntries<'archive, 'buf, R> { ZipEntries { buffer, archive: self, pos: 0, end: 0, offset: self.eocd.directory_offset(), base_offset: self.eocd.base_offset(), central_dir_end_pos: self.eocd.head_eocd_offset(), } } /// Returns a hint for the total number of entries in the archive. /// /// This value is read from the End of Central Directory record. pub fn entries_hint(&self) -> u64 { self.eocd.entries() } /// Returns a Read implementation for the comment of the zip archive. /// /// Use [`RangeReader::remaining()`] to get the comment length before /// reading. It is guaranteed to be less than `u16::MAX`. /// /// # Examples /// /// ```rust /// use rawzip::{ZipArchive, ZipStr, RECOMMENDED_BUFFER_SIZE}; /// use std::io::Read; /// use std::fs::File; /// /// let file = File::open("assets/test.zip")?; /// let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; /// let archive = ZipArchive::from_file(file, &mut buffer)?; /// /// let mut comment_reader = archive.comment(); /// let comment_len = comment_reader.remaining() as usize; /// comment_reader.read_exact(&mut buffer[..comment_len])?; /// /// let actual = ZipStr::new(&buffer[..comment_len]); /// let expected = ZipStr::new(b"This is a zipfile comment."); /// assert_eq!(expected, actual); /// # Ok::<(), Box>(()) /// ``` pub fn comment(&self) -> RangeReader<&R> { let comment_start = self.eocd.tail_eocd_offset() + EndOfCentralDirectoryRecordFixed::SIZE as u64; let comment_end = comment_start + self.eocd.comment_len() as u64; RangeReader::new(&self.reader, comment_start..comment_end) } /// Returns the offset of the End of Central Directory (EOCD) signature. /// /// This is the byte position where the EOCD signature (0x06054b50) was found. /// Useful for recovery scenarios when dealing with false EOCD signatures or /// when restarting archive searches from a known position. /// /// # Examples /// /// ```rust /// # use rawzip::{ZipArchive, ZipLocator, RECOMMENDED_BUFFER_SIZE}; /// # use std::fs::File; /// # fn example() -> Result<(), Box> { /// # let file = File::open("assets/test.zip")?; /// # let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; /// let archive = ZipArchive::from_file(file, &mut buffer)?; /// let eocd_position = archive.eocd_offset(); /// /// let locator = ZipLocator::new(); /// let reader = archive.get_ref(); /// let maybe_previous = locator.locate_in_reader(reader, &mut buffer, eocd_position); /// # Ok(()) /// # } /// ``` pub fn eocd_offset(&self) -> u64 { self.eocd.tail_eocd_offset() } /// The declared offset of the start of the central directory. /// /// To verify the validity of this offset, start iterating through the /// central directory via `entries()`. Ensure no errors are returned on the /// first entry. /// /// This value is useful when calculating the amount of prelude data exists /// in the data, as it will serve as the upper bound until each file's /// [`ZipFileHeaderRecord::local_header_offset`] can be examined. pub fn directory_offset(&self) -> u64 { self.eocd.directory_offset() } /// Returns the offset where the ZIP archive ends. /// /// This returns the position immediately after the last byte of the ZIP /// archive, including the End of Central Directory record and any comment. /// This is useful for extracting trailing data. /// /// The calculation does not rely on any self reported values from the /// archive. /// /// This can be used in conjunction with the starting offset calculation /// start offset as shown in [`RangeReader`] to determine the exact byte /// range (and thus size) of the ZIP archive within a context of a larger /// file. pub fn end_offset(&self) -> u64 { self.eocd.tail_eocd_offset() + EndOfCentralDirectoryRecordFixed::SIZE as u64 + self.comment().remaining() } } impl ZipArchive where R: ReaderAt, { /// Seeks to the given file entry in the zip archive. pub fn get_entry(&self, entry: ZipArchiveEntryWayfinder) -> Result, Error> { let mut buffer = [0u8; ZipLocalFileHeaderFixed::SIZE]; self.reader .read_exact_at(&mut buffer, entry.local_header_offset)?; // The central directory is the source of truth so we really only parse // out the local file header to verify the signature and understand the // variable length. Not everyone uses this as the source of truth: // https://labs.redyops.com/index.php/2020/04/30/spending-a-night-reading-the-zip-file-format-specification/ let file_header = ZipLocalFileHeaderFixed::parse(&buffer)?; let (body_offset, o1) = entry .local_header_offset .overflowing_add(ZipLocalFileHeaderFixed::SIZE as u64); let (body_offset, o2) = body_offset.overflowing_add(file_header.variable_length() as u64); let (body_end_offset, o3) = body_offset.overflowing_add(entry.compressed_size); if o1 || o2 || o3 { return Err(Error::from(ErrorKind::Eof)); } Ok(ZipEntry { archive: self, entry, body_offset, body_end_offset, }) } } /// Represents a single entry (file or directory) within a [`ZipArchive`] #[derive(Debug, Clone)] pub struct ZipEntry<'archive, R> { archive: &'archive ZipArchive, body_offset: u64, body_end_offset: u64, entry: ZipArchiveEntryWayfinder, } impl<'archive, R> ZipEntry<'archive, R> where R: ReaderAt, { /// Returns a [`ZipReader`] for reading the compressed data of this entry. pub fn reader(&self) -> ZipReader<&'archive R> { ZipReader { entry: self.entry, range_reader: RangeReader::new( self.archive.get_ref(), self.body_offset..self.body_end_offset, ), } } /// Returns a reader that wraps a decompressor and verify the size and CRC /// of the decompressed data once finished. pub fn verifying_reader(&self, reader: D) -> ZipVerifier where D: std::io::Read, { ZipVerifier { reader, crc: 0, size: 0, archive: self.archive.get_ref(), end_offset: self.body_end_offset, wayfinder: self.entry, } } /// Returns a tuple of start and end byte offsets for the compressed data /// within the underlying reader. /// /// This method uses the information from the local file header in its /// calculations. /// /// # Security Usage /// /// This method is useful for detecting overlapping entries, which are often /// used in zip bombs. By comparing the ranges returned by this method /// across multiple entries, you can identify when entries share compressed /// data: /// /// ```rust /// # use rawzip::{ZipArchive, Error}; /// # fn example(data: &[u8]) -> Result<(), Error> { /// let archive = ZipArchive::from_slice(data)?; /// let mut ranges = Vec::new(); /// /// for entry_result in archive.entries() { /// let entry = entry_result?; /// let wayfinder = entry.wayfinder(); /// if let Ok(zip_entry) = archive.get_entry(wayfinder) { /// ranges.push(zip_entry.compressed_data_range()); /// } /// } /// /// // Check for overlapping ranges /// ranges.sort_by_key(|&(start, _)| start); /// for window in ranges.windows(2) { /// let (_, end1) = window[0]; /// let (start2, _) = window[1]; /// if end1 > start2 { /// panic!("Warning: Overlapping entries detected!"); /// } /// } /// # Ok(()) /// # } /// ``` pub fn compressed_data_range(&self) -> (u64, u64) { (self.body_offset, self.body_end_offset) } /// Returns the local file header information. /// /// This method reads the local file header to which may differ from the /// central directory data. Most ZIP tools use the central directory as /// authoritative, but access to local header data can be useful: /// /// The local header may contain: /// - Additional or different extra fields (richer timestamp data, etc.) /// - Different filename than the central directory (security concern) /// /// The buffer argument must be large enough to hold both the filename and /// extra fields from the local header or a too small error will be /// returned. /// /// # Examples /// /// ```rust /// # use rawzip::{ZipArchive, RECOMMENDED_BUFFER_SIZE, extra_fields::ExtraFieldId}; /// # use std::fs::File; /// # fn example() -> Result<(), Box> { /// // Test with filename mismatch test fixture /// let file = File::open("assets/filename_mismatch_test.zip")?; /// let mut buf = vec![0u8; RECOMMENDED_BUFFER_SIZE]; /// let archive = ZipArchive::from_file(file, &mut buf)?; /// /// let mut entries = archive.entries(&mut buf); /// let entry_header = entries.next_entry()?.unwrap(); /// /// // Central directory shows one filename /// assert_eq!(entry_header.file_path().as_ref(), b"malware.exe"); /// let wayfinder = entry_header.wayfinder(); /// let entry = archive.get_entry(wayfinder)?; /// /// // Read the local header /// let mut local_buffer = vec![0u8; 1024]; /// let local_header = entry.local_header(&mut local_buffer)?; /// /// // Local header shows different filename /// assert_eq!(local_header.file_path().as_ref(), b"safe_file.txt"); /// /// // Access extra fields from local header /// let mut found_fields = 0; /// for (field_id, _data) in local_header.extra_fields() { /// found_fields += 1; /// // Could check for specific extra field types here /// println!("Found extra field: {:04x}", field_id.as_u16()); /// } /// # Ok(()) /// # } /// ``` pub fn local_header<'a>(&self, buffer: &'a mut [u8]) -> Result, Error> { let mut header_buffer = [0u8; ZipLocalFileHeaderFixed::SIZE]; // Read the local file header self.archive .get_ref() .read_exact_at(&mut header_buffer, self.entry.local_header_offset)?; let local_header_fixed = ZipLocalFileHeaderFixed::parse(&header_buffer).expect("header has already been parsed"); let file_name_len = local_header_fixed.file_name_len as usize; let extra_field_len = local_header_fixed.extra_field_len as usize; let total_variable_len = file_name_len + extra_field_len; // Check if buffer is large enough for both filename and extra fields if buffer.len() < total_variable_len { return Err(Error::from(ErrorKind::BufferTooSmall)); } let variable_data = &mut buffer[..total_variable_len]; let variable_data_offset = self.entry.local_header_offset + ZipLocalFileHeaderFixed::SIZE as u64; self.archive .get_ref() .read_exact_at(variable_data, variable_data_offset)?; let (filename_data, extra_field_data) = variable_data.split_at(file_name_len); Ok(ZipLocalFileHeader { file_path: ZipFilePath::from_bytes(filename_data), extra_fields: ExtraFields::new(extra_field_data), }) } } /// Holds the expected CRC32 checksum and uncompressed size for a Zip entry. /// /// This struct is used to verify the integrity of decompressed data. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ZipVerification { pub crc: u32, pub uncompressed_size: u64, } impl ZipVerification { /// Returns the expected CRC32 checksum. pub fn crc(&self) -> u32 { self.crc } /// Returns the expected uncompressed size. pub fn size(&self) -> u64 { self.uncompressed_size } /// Validates the size and CRC of the entry. /// /// This function will return an error if the size or CRC does not match /// the expected values. pub fn valid(&self, rhs: ZipVerification) -> Result<(), Error> { if self.size() != rhs.size() { return Err(Error::from(ErrorKind::InvalidSize { expected: self.size(), actual: rhs.size(), })); } // If the CRC is 0, then it is not verified. if self.crc() != 0 && self.crc() != rhs.crc() { return Err(Error::from(ErrorKind::InvalidChecksum { expected: self.crc(), actual: rhs.crc(), })); } Ok(()) } } /// Verifies the checksum of the decompressed data matches the checksum listed in the zip #[derive(Debug, Clone)] pub struct ZipVerifier { reader: Decompressor, crc: u32, size: u64, archive: ReaderAt, end_offset: u64, wayfinder: ZipArchiveEntryWayfinder, } impl ZipVerifier { /// Consumes the [`ZipVerifier`], returning the underlying decompressor. pub fn into_inner(self) -> Decompressor { self.reader } } impl std::io::Read for ZipVerifier where Decompressor: std::io::Read, Reader: ReaderAt, { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read = self.reader.read(buf)?; self.crc = crc32_chunk(&buf[..read], self.crc); self.size += read as u64; if read == 0 || self.size >= self.wayfinder.uncompressed_size_hint() { let crc = if self.wayfinder.has_data_descriptor { DataDescriptor::read_at(&self.archive, self.end_offset).map(|x| x.crc) } else { Ok(self.wayfinder.crc) }; crc.and_then(|crc| { let expected = ZipVerification { crc, uncompressed_size: self.wayfinder.uncompressed_size_hint(), }; expected.valid(ZipVerification { crc: self.crc, uncompressed_size: self.size, }) }) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; } Ok(read) } } /// A reader for a Zip entry's compressed data. #[derive(Debug, Clone)] pub struct ZipReader { entry: ZipArchiveEntryWayfinder, range_reader: RangeReader, } impl ZipReader where R: ReaderAt, { /// Returns an object that can be used to verify the size and checksum of /// inflated data /// /// Consumes the reader, so this should be called after all data has been read from the entry. /// /// The function will read the data descriptor if one is expected to exist. pub fn claim_verifier(self) -> Result { let expected_size = self.entry.uncompressed_size_hint(); let expected_crc = if self.entry.has_data_descriptor { let end_offset = self.range_reader.end_offset(); let archive = self.range_reader.into_inner(); DataDescriptor::read_at(archive, end_offset).map(|x| x.crc)? } else { self.entry.crc }; Ok(ZipVerification { crc: expected_crc, uncompressed_size: expected_size, }) } } impl Read for ZipReader where R: ReaderAt, { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.range_reader.read(buf) } } /// Local file header information from a ZIP archive entry. /// /// This struct provides access to data stored in the local file header of a ZIP entry, /// which may differ from the information in the central directory. The local header /// contains the filename and extra fields as they appear at the start of each entry's /// data within the ZIP file. /// /// Most ZIP tools use the central directory as authoritative, but access to local /// header data is useful for validation, security analysis, and forensic purposes. #[derive(Debug)] pub struct ZipLocalFileHeader<'a> { file_path: ZipFilePath>, extra_fields: ExtraFields<'a>, } impl<'a> ZipLocalFileHeader<'a> { /// Returns the file path from the local file header. /// /// This may differ from the central directory file path. pub fn file_path(&self) -> ZipFilePath> { self.file_path } /// Returns an iterator over the extra fields from the local file header. /// /// Extra fields in the local header may differ from those in the central directory. /// The local header may contain additional or different metadata compared to the /// central directory entry. pub fn extra_fields(&self) -> ExtraFields<'a> { self.extra_fields } } #[derive(Debug, Clone)] pub(crate) struct DataDescriptor { crc: u32, } impl DataDescriptor { const SIZE: usize = 8; pub const SIGNATURE: u32 = 0x08074b50; fn parse(data: &[u8]) -> Result { if data.len() < Self::SIZE { return Err(Error::from(ErrorKind::Eof)); } let mut pos = 0; let potential_signature = le_u32(&data[0..4]); if potential_signature == Self::SIGNATURE { pos += 4; } // The crc is followed by the compressed_size and then the // uncompressed_size but the spec allows for the sizes to be either 4 // bytes each or 8 bytes in Zip64 mode. (spec 4.3.9.1). They aren't // needed, so we skip them. Ok(DataDescriptor { crc: le_u32(&data[pos..pos + 4]), }) } fn read_at(reader: R, offset: u64) -> Result where R: ReaderAt, { let mut buffer = [0u8; Self::SIZE]; reader.read_exact_at(&mut buffer, offset)?; Self::parse(&buffer) } } /// A lending iterator over file header records in a [`ZipArchive`]. #[derive(Debug)] pub struct ZipEntries<'archive, 'buf, R> { buffer: &'buf mut [u8], archive: &'archive ZipArchive, pos: usize, end: usize, offset: u64, base_offset: u64, central_dir_end_pos: u64, } impl ZipEntries<'_, '_, R> where R: ReaderAt, { /// Yield the next zip file entry in the central directory if there is any /// /// This method reads from the underlying archive reader into the provided /// buffer to parse entry headers. #[inline] pub fn next_entry(&mut self) -> Result>, Error> { if self.pos + ZipFileHeaderFixed::SIZE >= self.end { if self.offset >= self.central_dir_end_pos { return Ok(None); } let remaining = self.end - self.pos; self.buffer.copy_within(self.pos..self.end, 0); let max_read = ((self.central_dir_end_pos - self.offset) as usize) .min(self.buffer.len() - remaining); let read = self.archive.reader.read_at_least_at( &mut self.buffer[remaining..][..max_read], ZipFileHeaderFixed::SIZE, self.offset, )?; self.offset += read as u64; self.pos = 0; self.end = remaining + read; } let central_directory_offset = self.offset - (self.end - self.pos) as u64; let data = &self.buffer[self.pos..self.end]; let file_header = ZipFileHeaderFixed::parse(data)?; self.pos += ZipFileHeaderFixed::SIZE; let variable_length = file_header.variable_length(); if self.pos + variable_length > self.end { // Need to read more data let remaining = self.end - self.pos; self.buffer.copy_within(self.pos..self.end, 0); let max_read = ((self.central_dir_end_pos - self.offset) as usize) .min(self.buffer.len() - remaining); let read = self.archive.reader.read_at_least_at( &mut self.buffer[remaining..][..max_read], variable_length - remaining, self.offset, )?; self.offset += read as u64; self.pos = 0; self.end = remaining + read; } let data = &self.buffer[self.pos..self.end]; let (file_name, extra_field, file_comment, _) = file_header .parse_variable_length(data) .expect("variable length precheck failed"); let mut file_header = ZipFileHeaderRecord::from_parts( file_header, file_name, extra_field, file_comment, central_directory_offset, ); file_header.local_header_offset += self.base_offset; self.pos += variable_length; Ok(Some(file_header)) } } /// 4.4.2 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct VersionMadeBy(u16); #[allow(dead_code)] impl VersionMadeBy { pub fn as_u16(&self) -> u16 { self.0 } /// The (major, minor) ZIP specification version supported by the software /// used to encode the file. /// /// 4.4.2.3: The lower byte, The value / 10 indicates the major version /// number, and the value mod 10 is the minor version number. pub fn version(&self) -> (u8, u8) { let v = (self.0 >> 8) as u8; (v / 10, v % 10) } } #[derive(Debug, Clone)] pub(crate) struct Zip64EndOfCentralDirectory { pub offset: u64, pub central_dir_offset: u64, pub central_dir_size: u64, pub num_entries: u64, } impl Zip64EndOfCentralDirectory { #[inline] pub fn from_parts(offset: u64, record: Zip64EndOfCentralDirectoryRecord) -> Self { Self { offset, central_dir_offset: record.central_dir_offset, central_dir_size: record.central_dir_size, num_entries: record.num_entries, } } } #[derive(Debug, Clone)] pub(crate) struct Zip64EndOfCentralDirectoryRecord { /// zip64 end of central dir signature pub signature: u32, /// size of zip64 end of central directory record #[allow(dead_code)] pub size: u64, /// version made by #[allow(dead_code)] pub version_made_by: VersionMadeBy, /// version needed to extract #[allow(dead_code)] pub version_needed: u16, /// number of this disk #[allow(dead_code)] pub disk_number: u32, /// number of the disk with the start of the central directory #[allow(dead_code)] pub cd_disk: u32, /// total number of entries in the central directory on this disk pub num_entries: u64, /// total number of entries in the central directory #[allow(dead_code)] pub total_entries: u64, /// size of the central directory pub central_dir_size: u64, /// offset of start of central directory with respect to the starting disk number pub central_dir_offset: u64, // zip64 extensible data sector // pub extensible_data: Vec, } impl Zip64EndOfCentralDirectoryRecord { pub(crate) const SIZE: usize = 56; #[inline] pub fn parse(data: &[u8]) -> Result { if data.len() < Self::SIZE { return Err(Error::from(ErrorKind::Eof)); } let result = Zip64EndOfCentralDirectoryRecord { signature: le_u32(&data[0..4]), size: le_u64(&data[4..12]), version_made_by: VersionMadeBy(le_u16(&data[12..14])), version_needed: le_u16(&data[14..16]), disk_number: le_u32(&data[16..20]), cd_disk: le_u32(&data[20..24]), num_entries: le_u64(&data[24..32]), total_entries: le_u64(&data[32..40]), central_dir_size: le_u64(&data[40..48]), central_dir_offset: le_u64(&data[48..56]), }; if result.signature != END_OF_CENTRAL_DIR_SIGNATURE64 { return Err(Error::from(ErrorKind::InvalidSignature { expected: END_OF_CENTRAL_DIR_SIGNATURE64, actual: result.signature, })); } Ok(result) } } /// A numeric identifier for a compression method used in a Zip archive. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CompressionMethodId(u16); impl CompressionMethodId { /// Returns the raw `u16` value of the compression method ID. #[inline] pub fn as_u16(&self) -> u16 { self.0 } /// Converts the numeric ID to a `CompressionMethod` enum. #[inline] pub fn as_method(&self) -> CompressionMethod { match self.0 { 0 => CompressionMethod::Store, 1 => CompressionMethod::Shrunk, 2 => CompressionMethod::Reduce1, 3 => CompressionMethod::Reduce2, 4 => CompressionMethod::Reduce3, 5 => CompressionMethod::Reduce4, 6 => CompressionMethod::Imploded, 7 => CompressionMethod::Tokenizing, 8 => CompressionMethod::Deflate, 9 => CompressionMethod::Deflate64, 10 => CompressionMethod::Terse, 12 => CompressionMethod::Bzip2, 14 => CompressionMethod::Lzma, 18 => CompressionMethod::Lz77, 20 => CompressionMethod::ZstdDeprecated, 93 => CompressionMethod::Zstd, 94 => CompressionMethod::Mp3, 95 => CompressionMethod::Xz, 96 => CompressionMethod::Jpeg, 97 => CompressionMethod::WavPack, 98 => CompressionMethod::Ppmd, 99 => CompressionMethod::Aes, _ => CompressionMethod::Unknown(self.0), } } } /// The compression method used on an individual Zip archive entry /// /// Documented in the spec under: 4.4.5 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u16)] pub enum CompressionMethod { Store = 0, Shrunk = 1, Reduce1 = 2, Reduce2 = 3, Reduce3 = 4, Reduce4 = 5, Imploded = 6, Tokenizing = 7, Deflate = 8, Deflate64 = 9, Terse = 10, Bzip2 = 12, Lzma = 14, Lz77 = 18, ZstdDeprecated = 20, Zstd = 93, Mp3 = 94, Xz = 95, Jpeg = 96, WavPack = 97, Ppmd = 98, Aes = 99, Unknown(u16), } impl CompressionMethod { /// Return the numeric id of this compression method. #[inline] pub fn as_id(&self) -> CompressionMethodId { let value = match self { CompressionMethod::Store => 0, CompressionMethod::Shrunk => 1, CompressionMethod::Reduce1 => 2, CompressionMethod::Reduce2 => 3, CompressionMethod::Reduce3 => 4, CompressionMethod::Reduce4 => 5, CompressionMethod::Imploded => 6, CompressionMethod::Tokenizing => 7, CompressionMethod::Deflate => 8, CompressionMethod::Deflate64 => 9, CompressionMethod::Terse => 10, CompressionMethod::Bzip2 => 12, CompressionMethod::Lzma => 14, CompressionMethod::Lz77 => 18, CompressionMethod::ZstdDeprecated => 20, CompressionMethod::Zstd => 93, CompressionMethod::Mp3 => 94, CompressionMethod::Xz => 95, CompressionMethod::Jpeg => 96, CompressionMethod::WavPack => 97, CompressionMethod::Ppmd => 98, CompressionMethod::Aes => 99, CompressionMethod::Unknown(id) => *id, }; CompressionMethodId(value) } } impl From for CompressionMethod { fn from(id: u16) -> Self { CompressionMethodId(id).as_method() } } /// A borrowed data from a Zip archive, typically for comments or non-path text. /// /// Zip archives may contain text that is not strictly UTF-8. This type /// represents such text as a byte slice. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ZipStr<'a>(&'a [u8]); impl<'a> ZipStr<'a> { /// Creates a new `ZipStr` from a byte slice. #[inline] pub fn new(data: &'a [u8]) -> Self { Self(data) } /// Returns the underlying byte slice. #[inline] pub fn as_bytes(&self) -> &'a [u8] { self.0 } /// Converts the borrowed `ZipStr` into an owned `ZipString` by cloning the /// data. #[inline] pub fn into_owned(&self) -> ZipString { ZipString::new(self.0.to_vec()) } } /// An owned string (`Vec`) from a Zip archive, typically for comments or non-path text. /// /// Similar to `ZipStr`, but owns its data. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ZipString(Vec); impl ZipString { /// Creates a new `ZipString` from a vector of bytes. #[inline] pub fn new(data: Vec) -> Self { Self(data) } /// Returns a borrowed `ZipStr` view of this `ZipString`. #[inline] pub fn as_str(&self) -> ZipStr<'_> { ZipStr::new(self.0.as_slice()) } } /// Represents a record from the Zip archive's central directory for a single /// file /// /// This contains metadata about the file. If interested in navigating to the /// file contents, use `[ZipFileHeaderRecord::wayfinder]`. /// /// Reference 4.3.12 in the zip specification #[derive(Debug, Clone)] #[allow(dead_code)] pub struct ZipFileHeaderRecord<'a> { signature: u32, version_made_by: u16, version_needed: u16, flags: u16, compression_method: CompressionMethodId, last_mod_time: u16, last_mod_date: u16, crc32: u32, compressed_size: u64, uncompressed_size: u64, file_name_len: u16, extra_field_len: u16, file_comment_len: u16, disk_number_start: u32, internal_file_attrs: u16, external_file_attrs: u32, local_header_offset: u64, central_directory_offset: u64, file_name: ZipFilePath>, extra_field: &'a [u8], file_comment: ZipStr<'a>, is_zip64: bool, } impl<'a> ZipFileHeaderRecord<'a> { #[inline] fn from_parts( header: ZipFileHeaderFixed, file_name: &'a [u8], extra_field: &'a [u8], file_comment: &'a [u8], central_directory_offset: u64, ) -> Self { let mut result = Self { signature: header.signature, version_made_by: header.version_made_by, version_needed: header.version_needed, flags: header.flags, compression_method: header.compression_method, last_mod_time: header.last_mod_time, last_mod_date: header.last_mod_date, crc32: header.crc32, compressed_size: u64::from(header.compressed_size), uncompressed_size: u64::from(header.uncompressed_size), file_name_len: header.file_name_len, extra_field_len: header.extra_field_len, file_comment_len: header.file_comment_len, disk_number_start: u32::from(header.disk_number_start), internal_file_attrs: header.internal_file_attrs, external_file_attrs: header.external_file_attrs, local_header_offset: u64::from(header.local_header_offset), central_directory_offset, file_name: ZipFilePath::from_bytes(file_name), extra_field, file_comment: ZipStr::new(file_comment), is_zip64: false, }; if result.uncompressed_size != u64::from(u32::MAX) && result.compressed_size != u64::from(u32::MAX) && result.local_header_offset != u64::from(u32::MAX) && result.disk_number_start != u32::from(u16::MAX) { return result; } let extra_fields = ExtraFields::new(extra_field); for (field_id, field_data) in extra_fields { if field_id != ExtraFieldId::ZIP64 { continue; } let mut field = field_data; result.is_zip64 = true; if header.uncompressed_size == u32::MAX { let Some(uncompressed_size) = field.get(..8).map(le_u64) else { break; }; result.uncompressed_size = uncompressed_size; field = &field[8..]; } if header.compressed_size == u32::MAX { let Some(compressed_size) = field.get(..8).map(le_u64) else { break; }; result.compressed_size = compressed_size; field = &field[8..]; } if header.local_header_offset == u32::MAX { let Some(local_header_offset) = field.get(..8).map(le_u64) else { break; }; result.local_header_offset = local_header_offset; field = &field[8..]; } if header.disk_number_start == u16::MAX { let Some(disk_number_start) = field.get(..4).map(le_u32) else { break; }; result.disk_number_start = disk_number_start; } break; } result } /// Describes if the file is a directory. /// /// See [`ZipFilePath::is_dir`] for more information. #[inline] pub fn is_dir(&self) -> bool { self.file_name.is_dir() } /// Returns true if the entry has a data descriptor that follows its /// compressed data. /// /// From the spec (4.3.9.1): /// /// > This descriptor MUST exist if bit 3 of the general purpose bit flag is /// > set #[inline] pub fn has_data_descriptor(&self) -> bool { self.flags & 0x08 != 0 } /// Describes where the file's data is located within the archive. #[inline] pub fn wayfinder(&self) -> ZipArchiveEntryWayfinder { ZipArchiveEntryWayfinder { uncompressed_size: self.uncompressed_size, compressed_size: self.compressed_size, local_header_offset: self.local_header_offset, has_data_descriptor: self.has_data_descriptor(), crc: self.crc32, } } /// The purported number of bytes of the uncompressed data. /// /// **WARNING**: this number has not yet been validated, so don't trust it /// to make allocation decisions. #[inline] pub fn uncompressed_size_hint(&self) -> u64 { self.uncompressed_size } /// The purported number of bytes of the compressed data. /// /// **WARNING**: this number has not yet been validated, so don't trust it /// to make allocation decisions. #[inline] pub fn compressed_size_hint(&self) -> u64 { self.compressed_size } /// The declared offset to the local file header within the Zip archive. /// /// To verify the validity of this offset, call /// [`ZipSliceArchive::get_entry`] or [`ZipArchive::get_entry`]. /// /// The minimum of all local header offsets (or `directory_offset()` when a /// zip is empty), will be the length of prelude data in a zip archive (data /// that is unrelated to the zip archive). /// /// See [`RangeReader`] for an example. #[inline] pub fn local_header_offset(&self) -> u64 { self.local_header_offset } /// The compression method used to compress the data #[inline] pub fn compression_method(&self) -> CompressionMethod { self.compression_method.as_method() } /// Returns the file path in its raw form. /// /// # Safety /// /// The raw path may contain unsafe components like: /// - Absolute paths (`/etc/passwd`) /// - Directory traversal (`../../../etc/passwd`) /// - Invalid UTF-8 sequences /// /// # Example /// ```rust /// # use rawzip::ZipArchive; /// # fn example() -> Result<(), Box> { /// # let data = include_bytes!("../assets/test.zip"); /// # let archive = ZipArchive::from_slice(data)?; /// # let mut entries = archive.entries(); /// # let entry = entries.next_entry()?.unwrap(); /// // Get raw path (potentially unsafe) /// let raw_path = entry.file_path(); /// /// // Convert to safe path /// let safe_path = raw_path.try_normalize()?; /// println!("Safe path: {}", safe_path.as_ref()); /// /// // Check if it's a directory /// if safe_path.is_dir() { /// println!("This is a directory"); /// } /// # Ok(()) /// # } /// ``` #[inline] pub fn file_path(&self) -> ZipFilePath> { self.file_name } /// Returns the last modification date and time. /// /// This method parses the extra field data to locate more accurate timestamps. #[inline] pub fn last_modified(&self) -> ZipDateTimeKind { extract_best_timestamp(self.extra_fields(), self.last_mod_time, self.last_mod_date) } /// Returns the file mode information extracted from the external file attributes. #[inline] pub fn mode(&self) -> EntryMode { let creator_version = self.version_made_by >> 8; let mut mode = match creator_version { // Unix and macOS CREATOR_UNIX | CREATOR_MACOS => unix_mode_to_file_mode(self.external_file_attrs >> 16), // NTFS, VFAT, FAT CREATOR_NTFS | CREATOR_VFAT | CREATOR_FAT => { msdos_mode_to_file_mode(self.external_file_attrs) } // default to basic permissions _ => 0o644, }; // Check if it's a directory by filename ending with '/' if self.is_dir() { mode |= 0o040000; // S_IFDIR } EntryMode::new(mode) } /// The declared CRC32 checksum of the uncompressed data. /// /// To verify the validity of this value, [`ZipEntry::verifying_reader`] /// will return an error if when the decompressed data does not match this /// checksum. #[inline] pub fn crc32(&self) -> u32 { self.crc32 } /// Returns the offset from the start of reader where this central directory /// record was parsed from. #[inline] pub fn central_directory_offset(&self) -> u64 { self.central_directory_offset } /// Returns an iterator over the extra fields in this file header record. /// /// Extra fields contain additional metadata about files in ZIP archives, /// such as timestamps, alignment information, and platform-specific data. /// /// # Examples /// /// ```rust /// # use rawzip::{ZipArchive, extra_fields::ExtraFieldId}; /// # fn example(data: &[u8]) -> Result<(), Box> { /// let archive = ZipArchive::from_slice(data)?; /// for entry_result in archive.entries() { /// let entry = entry_result?; /// let mut extra_fields = entry.extra_fields(); /// for (field_id, field_data) in extra_fields.by_ref() { /// match field_id { /// ExtraFieldId::JAVA_JAR => { /// println!("Handle jar CAFE field with {} bytes", field_data.len()); /// } /// _ => { /// println!("Found extra field ID: 0x{:04x}", field_id.as_u16()); /// } /// } /// } /// /// // If desired, check for truncated data /// if !extra_fields.remaining_bytes().is_empty() { /// println!("Warning: Some extra field data was truncated"); /// } /// } /// # Ok(()) /// # } /// ``` /// /// Raw access to the entire extra field data is available when /// `remaining_bytes` is called prior to any iteration. #[inline] pub fn extra_fields(&self) -> ExtraFields<'_> { ExtraFields::new(self.extra_field) } } /// Contains directions to where the Zip entry's data is located within the Zip archive. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ZipArchiveEntryWayfinder { uncompressed_size: u64, compressed_size: u64, local_header_offset: u64, crc: u32, has_data_descriptor: bool, } impl ZipArchiveEntryWayfinder { /// Equivalent to [`ZipFileHeaderRecord::compressed_size_hint`] /// /// This is a convenience method to avoid having to deal with lifetime /// issues on a `ZipFileHeaderRecord` #[inline] pub fn uncompressed_size_hint(&self) -> u64 { self.uncompressed_size } /// Equivalent to [`ZipFileHeaderRecord::compressed_size_hint`] /// /// This is a convenience method to avoid having to deal with lifetime /// issues on a `ZipFileHeaderRecord` #[inline] pub fn compressed_size_hint(&self) -> u64 { self.compressed_size } } #[derive(Debug, Clone)] pub(crate) struct ZipLocalFileHeaderFixed { pub(crate) signature: u32, pub(crate) version_needed: u16, pub(crate) flags: u16, pub(crate) compression_method: CompressionMethodId, pub(crate) last_mod_time: u16, pub(crate) last_mod_date: u16, pub(crate) crc32: u32, pub(crate) compressed_size: u32, pub(crate) uncompressed_size: u32, pub(crate) file_name_len: u16, pub(crate) extra_field_len: u16, } impl ZipLocalFileHeaderFixed { const SIZE: usize = 30; pub const SIGNATURE: u32 = 0x04034b50; pub fn parse(data: &[u8]) -> Result { if data.len() < Self::SIZE { return Err(Error::from(ErrorKind::Eof)); } let result = ZipLocalFileHeaderFixed { signature: le_u32(&data[0..4]), version_needed: le_u16(&data[4..6]), flags: le_u16(&data[6..8]), compression_method: CompressionMethodId(le_u16(&data[8..10])), last_mod_time: le_u16(&data[10..12]), last_mod_date: le_u16(&data[12..14]), crc32: le_u32(&data[14..18]), compressed_size: le_u32(&data[18..22]), uncompressed_size: le_u32(&data[22..26]), file_name_len: le_u16(&data[26..28]), extra_field_len: le_u16(&data[28..30]), }; if result.signature != Self::SIGNATURE { return Err(Error::from(ErrorKind::InvalidSignature { expected: Self::SIGNATURE, actual: result.signature, })); } Ok(result) } pub fn variable_length(&self) -> usize { self.file_name_len as usize + self.extra_field_len as usize } pub fn write(&self, mut writer: W) -> Result<(), Error> where W: Write, { // Batch writes with a fixed size buffer. Improved throughput 25% let mut buffer = [0u8; 30]; buffer[..4].copy_from_slice(&self.signature.to_le_bytes()); buffer[4..6].copy_from_slice(&self.version_needed.to_le_bytes()); buffer[6..8].copy_from_slice(&self.flags.to_le_bytes()); buffer[8..10].copy_from_slice(&self.compression_method.0.to_le_bytes()); buffer[10..12].copy_from_slice(&self.last_mod_time.to_le_bytes()); buffer[12..14].copy_from_slice(&self.last_mod_date.to_le_bytes()); buffer[14..18].copy_from_slice(&self.crc32.to_le_bytes()); buffer[18..22].copy_from_slice(&self.compressed_size.to_le_bytes()); buffer[22..26].copy_from_slice(&self.uncompressed_size.to_le_bytes()); buffer[26..28].copy_from_slice(&self.file_name_len.to_le_bytes()); buffer[28..30].copy_from_slice(&self.extra_field_len.to_le_bytes()); writer.write_all(&buffer)?; Ok(()) } } #[derive(Debug, Clone)] pub(crate) struct ZipFileHeaderFixed { pub signature: u32, pub version_made_by: u16, pub version_needed: u16, pub flags: u16, pub compression_method: CompressionMethodId, pub last_mod_time: u16, pub last_mod_date: u16, pub crc32: u32, pub compressed_size: u32, pub uncompressed_size: u32, pub file_name_len: u16, pub extra_field_len: u16, pub file_comment_len: u16, pub disk_number_start: u16, pub internal_file_attrs: u16, pub external_file_attrs: u32, pub local_header_offset: u32, } impl ZipFileHeaderFixed { pub fn variable_length(&self) -> usize { self.file_name_len as usize + self.extra_field_len as usize + self.file_comment_len as usize } } type VariableFields<'a> = ( &'a [u8], // file_name &'a [u8], // extra_field &'a [u8], // file_comment &'a [u8], // rest of the data ); impl ZipFileHeaderFixed { pub(crate) const SIZE: usize = 46; #[inline] pub fn parse(data: &[u8]) -> Result { if data.len() < Self::SIZE { return Err(Error::from(ErrorKind::Eof)); } let result = ZipFileHeaderFixed { signature: le_u32(&data[0..4]), version_made_by: le_u16(&data[4..6]), version_needed: le_u16(&data[6..8]), flags: le_u16(&data[8..10]), compression_method: CompressionMethodId(le_u16(&data[10..12])), last_mod_time: le_u16(&data[12..14]), last_mod_date: le_u16(&data[14..16]), crc32: le_u32(&data[16..20]), compressed_size: le_u32(&data[20..24]), uncompressed_size: le_u32(&data[24..28]), file_name_len: le_u16(&data[28..30]), extra_field_len: le_u16(&data[30..32]), file_comment_len: le_u16(&data[32..34]), disk_number_start: le_u16(&data[34..36]), internal_file_attrs: le_u16(&data[36..38]), external_file_attrs: le_u32(&data[38..42]), local_header_offset: le_u32(&data[42..46]), }; if result.signature != CENTRAL_HEADER_SIGNATURE { return Err(Error::from(ErrorKind::InvalidSignature { expected: CENTRAL_HEADER_SIGNATURE, actual: result.signature, })); } Ok(result) } #[inline] fn parse_variable_length<'a>(&self, data: &'a [u8]) -> Option> { if data.len() < self.file_name_len as usize { return None; } let (file_name, rest) = data.split_at(self.file_name_len as usize); if rest.len() < self.extra_field_len as usize { return None; } let (extra_field, rest) = rest.split_at(self.extra_field_len as usize); if rest.len() < self.file_comment_len as usize { return None; } let (file_comment, rest) = rest.split_at(self.file_comment_len as usize); Some((file_name, extra_field, file_comment, rest)) } pub fn write(&self, mut writer: W) -> Result<(), Error> where W: Write, { // Batch writes with a fixed size buffer. Improved throughput 25% let mut buffer = [0u8; Self::SIZE]; buffer[0..4].copy_from_slice(&self.signature.to_le_bytes()); buffer[4..6].copy_from_slice(&self.version_made_by.to_le_bytes()); buffer[6..8].copy_from_slice(&self.version_needed.to_le_bytes()); buffer[8..10].copy_from_slice(&self.flags.to_le_bytes()); buffer[10..12].copy_from_slice(&self.compression_method.0.to_le_bytes()); buffer[12..14].copy_from_slice(&self.last_mod_time.to_le_bytes()); buffer[14..16].copy_from_slice(&self.last_mod_date.to_le_bytes()); buffer[16..20].copy_from_slice(&self.crc32.to_le_bytes()); buffer[20..24].copy_from_slice(&self.compressed_size.to_le_bytes()); buffer[24..28].copy_from_slice(&self.uncompressed_size.to_le_bytes()); buffer[28..30].copy_from_slice(&self.file_name_len.to_le_bytes()); buffer[30..32].copy_from_slice(&self.extra_field_len.to_le_bytes()); buffer[32..34].copy_from_slice(&self.file_comment_len.to_le_bytes()); buffer[34..36].copy_from_slice(&self.disk_number_start.to_le_bytes()); buffer[36..38].copy_from_slice(&self.internal_file_attrs.to_le_bytes()); buffer[38..42].copy_from_slice(&self.external_file_attrs.to_le_bytes()); buffer[42..46].copy_from_slice(&self.local_header_offset.to_le_bytes()); writer.write_all(&buffer)?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; #[test] pub fn blank_zip_archive() { let data = [80, 75, 5, 6]; let mut buf = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_seekable(Cursor::new(data), &mut buf); assert!(archive.is_err()); } #[test] pub fn trunc_comment_zips() { let data = [ 80, 75, 6, 7, 21, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 10, 0, 59, 59, 80, 75, 5, 6, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 80, 75, 6, 6, 0, 0, 0, 10, ]; let mut buf = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_seekable(Cursor::new(data), &mut buf); assert!(archive.is_err()); let archive = ZipArchive::from_slice(data); assert!(archive.is_err()); } #[test] pub fn trunc_eocd64() { let data = [ 80, 75, 6, 7, 21, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 10, 0, 59, 59, 80, 75, 5, 6, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 80, 75, 6, 6, 0, 0, 6, 0, 0, 250, 255, 255, 255, 255, 251, 0, 0, 0, 0, 80, 5, 6, 0, 0, 0, 0, 56, 0, 0, 0, 0, 10, ]; let archive = ZipArchive::from_slice(data); assert!(archive.is_err()); let mut buf = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_seekable(Cursor::new(data), &mut buf); assert!(archive.is_err()); } #[test] pub fn trunc_eocd_entry() { let data = [ 80, 75, 1, 2, 159, 159, 159, 159, 159, 159, 159, 159, 159, 0, 241, 205, 0, 80, 75, 5, 6, 0, 48, 249, 0, 250, 255, 255, 255, 255, 251, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 0, ]; let archive = ZipArchive::from_slice(data).unwrap(); let mut entries = archive.entries(); assert!(entries.next_entry().is_err()); let mut buf = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_seekable(Cursor::new(data), &mut buf).unwrap(); let mut entries = archive.entries(&mut buf); assert!(entries.next_entry().is_err()); } #[test] fn test_compressed_data_range() { let test_zip = std::fs::read("assets/test.zip").unwrap(); // Test ZipSliceEntry API (from slice) let slice_archive = ZipArchive::from_slice(&test_zip).unwrap(); let slice_header_records: Vec<_> = slice_archive .entries() .collect::, _>>() .unwrap(); assert_eq!(slice_header_records.len(), 2); let entry1_wayfinder = slice_header_records[0].wayfinder(); let slice_entry1 = slice_archive.get_entry(entry1_wayfinder).unwrap(); let slice_range1 = slice_entry1.compressed_data_range(); assert_eq!( slice_range1, (66, 91), "test.txt compressed data should be at bytes 66-91" ); let entry2_wayfinder = slice_header_records[1].wayfinder(); let slice_entry2 = slice_archive.get_entry(entry2_wayfinder).unwrap(); let slice_range2 = slice_entry2.compressed_data_range(); assert_eq!( slice_range2, (169, 954), "gophercolor16x16.png compressed data should be at bytes 169-954" ); // Test ZipEntry API let file = std::fs::File::open("assets/test.zip").unwrap(); let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let reader_archive = ZipArchive::from_file(file, &mut buffer).unwrap(); // Get wayfinders from the slice archive since they should be identical let reader_entry1 = reader_archive.get_entry(entry1_wayfinder).unwrap(); let reader_range1 = reader_entry1.compressed_data_range(); let reader_entry2 = reader_archive.get_entry(entry2_wayfinder).unwrap(); let reader_range2 = reader_entry2.compressed_data_range(); // Verify both APIs return identical ranges assert_eq!(slice_range1, reader_range1); assert_eq!(slice_range2, reader_range2); } } rawzip-0.4.4/src/crc.rs000064400000000000000000000061231046102023000130530ustar 00000000000000const fn gen_crc_table() -> [[u32; 256]; 16] { let mut table: [[u32; 256]; 16] = [[0; 256]; 16]; let poly = 0xEDB88320; // Polynomial used in CRC-32 let mut i = 0; while i < 256 { let mut crc = i as u32; let mut j = 0; while j < 8 { if crc & 1 != 0 { crc = (crc >> 1) ^ poly; } else { crc >>= 1; } j += 1; } table[0][i] = crc; i += 1; } i = 1; while i < 16 { let mut j = 0; while j < 256 { table[i][j] = (table[i - 1][j] >> 8) ^ table[0][(table[i - 1][j] & 0xFF) as usize]; j += 1; } i += 1; } table } // Prefer static over const to cut test times in half // ref: https://github.com/srijs/rust-crc32fast/commit/e61ce6a39bbe9da495198a4037292ec299e8970f static CRC_TABLE: [[u32; 256]; 16] = gen_crc_table(); /// Compute the CRC32 (IEEE) of a byte slice /// /// Typically this function is used only to compute the CRC32 of data that is /// held entirely in memory. When decompressing, a /// [`ZipVerifier`](crate::ZipVerifier) is suitable to streaming computations. /// /// Benchmarks showed that function should be fast enough for all uses, only /// losing to `crc32fast` at the largest payload size and even then eking out a /// single digit performance improvement. pub fn crc32(data: &[u8]) -> u32 { crc32_chunk(data, 0) } #[inline] pub fn crc32_chunk(data: &[u8], prev: u32) -> u32 { let mut chunks = data.chunks_exact(16); let mut crc = chunks.by_ref().fold(!prev, |crc, data| { CRC_TABLE[0x0][data[0xf] as usize] ^ CRC_TABLE[0x1][data[0xe] as usize] ^ CRC_TABLE[0x2][data[0xd] as usize] ^ CRC_TABLE[0x3][data[0xc] as usize] ^ CRC_TABLE[0x4][data[0xb] as usize] ^ CRC_TABLE[0x5][data[0xa] as usize] ^ CRC_TABLE[0x6][data[0x9] as usize] ^ CRC_TABLE[0x7][data[0x8] as usize] ^ CRC_TABLE[0x8][data[0x7] as usize] ^ CRC_TABLE[0x9][data[0x6] as usize] ^ CRC_TABLE[0xa][data[0x5] as usize] ^ CRC_TABLE[0xb][data[0x4] as usize] ^ CRC_TABLE[0xc][data[0x3] as usize ^ ((crc >> 0x18) & 0xFF) as usize] ^ CRC_TABLE[0xd][data[0x2] as usize ^ ((crc >> 0x10) & 0xFF) as usize] ^ CRC_TABLE[0xe][data[0x1] as usize ^ ((crc >> 0x08) & 0xFF) as usize] ^ CRC_TABLE[0xf][data[0x0] as usize ^ (crc & 0xFF) as usize] }); crc = chunks.remainder().iter().fold(crc, |crc, &x| { (crc >> 8) ^ CRC_TABLE[0][(u32::from(x) ^ (crc & 0xFF)) as usize] }); !crc } #[cfg(test)] mod tests { use super::*; #[test] fn test_crc() { let table = gen_crc_table(); assert_eq!(table[0][0], 0x0000_0000); assert_eq!(table[0][1], 0x77073096); assert_eq!(table[0][2], 0xee0e612c); assert_eq!(table[1][1], 0x191B3141); assert_eq!(table[1][2], 0x32366282); let abc = b"EU4txt\nchecksum=\"ced5411e2d4a5ec724595c2c4f1b7347\""; assert_eq!(crc32(abc), 1702863696); } } rawzip-0.4.4/src/errors.rs000064400000000000000000000103711046102023000136200ustar 00000000000000/// An error that occurred while reading or writing a zip file #[derive(Debug)] pub struct Error { inner: Box, } impl Error { /// Returns the offset of the end of central directory (EOCD) signature /// /// Useful for reparsing input that contains a false EOCD signature. pub fn eocd_offset(&self) -> Option { self.inner.eocd_offset } /// Sets the false signature offset on this error pub(crate) fn with_eocd_offset(mut self, offset: u64) -> Self { self.inner.eocd_offset = Some(offset); self } } impl Error { pub(crate) fn io(err: std::io::Error) -> Error { Error::from(ErrorKind::IO(err)) } pub(crate) fn utf8(err: std::str::Utf8Error) -> Error { Error::from(ErrorKind::InvalidUtf8(err)) } pub(crate) fn is_eof(&self) -> bool { matches!(self.inner.kind, ErrorKind::Eof) } /// The kind of error that occurred pub fn kind(&self) -> &ErrorKind { &self.inner.kind } } #[derive(Debug)] struct ErrorInner { kind: ErrorKind, eocd_offset: Option, } /// The kind of error that occurred #[derive(Debug)] #[non_exhaustive] pub enum ErrorKind { /// Missing end of central directory MissingEndOfCentralDirectory, /// Missing zip64 end of central directory MissingZip64EndOfCentralDirectory, /// Buffer size too small BufferTooSmall, /// Invalid end of central directory signature InvalidSignature { expected: u32, actual: u32 }, /// Invalid inflated file crc checksum InvalidChecksum { expected: u32, actual: u32 }, /// An unexpected inflated file size InvalidSize { expected: u64, actual: u64 }, /// Invalid UTF-8 sequence InvalidUtf8(std::str::Utf8Error), /// An invalid input error with associated message InvalidInput { msg: String }, /// Could not construct an archive with the given end of central directory InvalidEndOfCentralDirectory, /// An IO error IO(std::io::Error), /// An unexpected end of file Eof, } impl std::error::Error for Error {} impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.inner.kind)?; Ok(()) } } impl std::fmt::Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { ErrorKind::IO(ref err) => err.fmt(f), ErrorKind::MissingEndOfCentralDirectory => { write!(f, "Missing end of central directory") } ErrorKind::MissingZip64EndOfCentralDirectory => { write!(f, "Missing zip64 end of central directory") } ErrorKind::BufferTooSmall => { write!(f, "Buffer size too small") } ErrorKind::Eof => { write!(f, "Unexpected end of file") } ErrorKind::InvalidSignature { expected, actual } => { write!( f, "Invalid signature: expected 0x{:08x}, got 0x{:08x}", expected, actual ) } ErrorKind::InvalidChecksum { expected, actual } => { write!( f, "Invalid checksum: expected 0x{:08x}, got 0x{:08x}", expected, actual ) } ErrorKind::InvalidSize { expected, actual } => { write!(f, "Invalid size: expected {}, got {}", expected, actual) } ErrorKind::InvalidUtf8(ref err) => { write!(f, "Invalid UTF-8: {}", err) } ErrorKind::InvalidInput { ref msg } => { write!(f, "Invalid input: {}", msg) } ErrorKind::InvalidEndOfCentralDirectory => { write!(f, "Invalid end of central directory") } } } } impl From for Error { fn from(kind: ErrorKind) -> Error { Error { inner: Box::new(ErrorInner { kind, eocd_offset: None, }), } } } impl From for Error { fn from(err: std::io::Error) -> Error { Error::from(ErrorKind::IO(err)) } } rawzip-0.4.4/src/extra_fields.rs000064400000000000000000000340701046102023000147570ustar 00000000000000use crate::{utils::le_u16, Error, ErrorKind, Header}; use std::io::Write; /// A numeric identifier for an extra field in a Zip archive. /// /// Constants defined here correspond to the IDs defined in the Zip specification. /// /// See sections 4.5 and 4.6 of the Zip spec. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtraFieldId(u16); impl ExtraFieldId { pub const ZIP64: Self = Self(0x0001); pub const AV_INFO: Self = Self(0x0007); pub const EXTENDED_LANGUAGE_ENCODING: Self = Self(0x0008); pub const OS2: Self = Self(0x0009); pub const NTFS: Self = Self(0x000a); pub const OPENVMS: Self = Self(0x000c); pub const UNIX: Self = Self(0x000d); pub const FILE_STREAM_AND_FORK_DESCRIPTORS: Self = Self(0x000e); pub const PATCH_DESCRIPTOR: Self = Self(0x000f); pub const PKCS7_STORE: Self = Self(0x0014); pub const X509_CERT_ID_AND_SIG: Self = Self(0x0015); pub const X509_CERT_ID_CENTRAL_DIR: Self = Self(0x0016); pub const STRONG_ENCRYPTION_HEADER: Self = Self(0x0017); pub const RECORD_MANAGEMENT_CONTROLS: Self = Self(0x0018); pub const PKCS7_ENCRYPTION_RECIPIENT_CERT_LIST: Self = Self(0x0019); pub const TIMESTAMP_RECORD: Self = Self(0x0020); pub const POLICY_DECRYPTION_KEY_RECORD: Self = Self(0x0021); pub const SMARTCRYPT_KEY_PROVIDER: Self = Self(0x0022); pub const SMARTCRYPT_POLICY_KEY_DATA: Self = Self(0x0023); pub const IBM_S390_AS400_UNCOMPRESSED: Self = Self(0x0065); pub const IBM_S390_AS400_COMPRESSED: Self = Self(0x0066); pub const POSZIP_4690: Self = Self(0x4690); pub const EXTENDED_TIMESTAMP: Self = Self(0x5455); pub const INFO_ZIP_UNIX_ORIGINAL: Self = Self(0x5855); pub const INFO_ZIP_UNIX: Self = Self(0x7855); pub const INFO_ZIP_UNIX_UID_GID: Self = Self(0x7875); pub const JAVA_JAR: Self = Self(0xCAFE); pub const ANDROID_ZIP_ALIGNMENT: Self = Self(0xD935); pub const MACINTOSH: Self = Self(0x07c8); pub const ACORN_SPARKFS: Self = Self(0x4341); pub const WINDOWS_NT_SECURITY_DESCRIPTOR: Self = Self(0x4653); pub const AOS_VS_ACL: Self = Self(0x5356); pub const INFO_ZIP_UNICODE_COMMENT: Self = Self(0x6375); pub const INFO_ZIP_UNICODE_PATH: Self = Self(0x7075); pub const DATA_STREAM_ALIGNMENT: Self = Self(0xa11e); pub const MICROSOFT_OPEN_PACKAGING_GROWTH_HINT: Self = Self(0xa220); /// Returns the raw `u16` value of the extra field ID. #[inline] pub const fn new(id: u16) -> Self { Self(id) } /// Returns the raw `u16` value of the extra field ID. #[inline] pub const fn as_u16(self) -> u16 { self.0 } } /// An iterator over extra field entries in a Zip archive. /// /// This follows zip spec section 4.5 defines extensible data fields: /// /// - Header ID - 2 bytes /// - Data Size - 2 bytes /// - Data - variable length /// /// If the iterator encounters malformed or truncated data, it will stop /// yielding entries. You can check [`ExtraFields::remaining_bytes()`] after /// iteration to detect if any data was left unparsed. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtraFields<'a> { data: &'a [u8], } impl<'a> ExtraFields<'a> { /// Creates a new iterator over the extra fields in the provided data slice. #[inline] pub fn new(data: &'a [u8]) -> Self { Self { data } } /// Returns the remaining unparsed bytes in the extra field data. #[inline] pub fn remaining_bytes(&self) -> &'a [u8] { self.data } #[inline] fn next_data(&mut self) -> Option<&'a [u8]> { let scratch = self.data; if scratch.len() < 4 { return None; } let size = le_u16(&scratch[2..4]) as usize; let total_field_len = size + 4; if scratch.len() < total_field_len { return None; } let (body, rest) = scratch.split_at(total_field_len); // Only advance once we have the entire entry self.data = rest; Some(body) } } impl<'a> Iterator for ExtraFields<'a> { type Item = (ExtraFieldId, &'a [u8]); #[inline] fn next(&mut self) -> Option { let next_chunk = self.next_data()?; let kind = le_u16(&next_chunk[0..2]); let body = &next_chunk[4..]; Some((ExtraFieldId(kind), body)) } } /// Container for extra fields with a shared data buffer and cached sizes. #[derive(Debug, Clone)] pub(crate) struct ExtraFieldsContainer { entries: StackVec, data_buffer: StackVec, pub(crate) local_size: u16, pub(crate) central_size: u16, } impl ExtraFieldsContainer { pub fn new() -> Self { Self { entries: StackVec::new(Header::new(0)), data_buffer: StackVec::new(0u8), local_size: 0, central_size: 0, } } pub fn add_field( &mut self, id: ExtraFieldId, data: &[u8], location: Header, ) -> Result<(), Error> { let size_delta = 4 + data.len(); let mut current_size = 0; if location.includes_local() { current_size = self.local_size; } if location.includes_central() { current_size = std::cmp::max(self.central_size, current_size); } if size_delta + (current_size as usize) > u16::MAX as usize { return Err(Error::from(ErrorKind::InvalidInput { msg: "extra field data too large".to_string(), })); } let mut buffer = [0u8; 4]; buffer[0..2].copy_from_slice(&id.as_u16().to_le_bytes()); buffer[2..4].copy_from_slice(&(data.len() as u16).to_le_bytes()); self.data_buffer.extend_from_slice(&buffer); self.data_buffer.extend_from_slice(data); if location.includes_local() { self.local_size += size_delta as u16; } if location.includes_central() { self.central_size += size_delta as u16; } self.entries.push(location); Ok(()) } fn write_extra_fields_iter( &self, writer: &mut impl Write, filter: Header, ) -> Result<(), Error> { let fields = self.data_buffer.as_slice(); let mut extra_fields = ExtraFields::new(fields); let entries = self.entries.as_slice(); for entry in entries { let extra_field = extra_fields.next_data().expect("Entry should have data"); let write = entry.intersects(filter); if write { writer.write_all(extra_field)?; } } Ok(()) } #[inline] pub fn write_extra_fields(&self, writer: &mut impl Write, filter: Header) -> Result<(), Error> { if filter == Header::LOCAL && self.local_size == 0 { // No local fields to write Ok(()) } else if filter == Header::CENTRAL && self.central_size == 0 { // No central fields to write Ok(()) } else if self.local_size == 0 || self.central_size == 0 { // If everything is one sided, we can dump everything writer.write_all(self.data_buffer.as_slice())?; Ok(()) } else { self.write_extra_fields_iter(writer, filter) } } } /// A stack-first vector that avoids heap allocation for small amounts of data. /// /// A poor man's `smallvec` as we aren't able to store as many elements inline /// (by one byte), but it's still an extremely effective no dependency, no /// unsafe solution, as benchmarks showed a 33% throughput improvement when /// writing out files with timestamps. #[derive(Debug, Clone)] pub(crate) enum StackVec where T: Copy + Clone, { /// Inline storage for up to N elements Small { data: [T; N], len: u8 }, /// Heap storage for more elements Large(Vec), } impl StackVec where T: Copy + Clone, { pub fn new(default_val: T) -> Self { Self::Small { data: [default_val; N], len: 0, } } pub fn push(&mut self, item: T) { match self { Self::Small { data, len } => { if (*len as usize) < N { // Still fits in small storage data[*len as usize] = item; *len += 1; } else { // Need to promote to large storage let mut vec = Vec::with_capacity(N + 1); vec.extend_from_slice(&data[..N]); vec.push(item); *self = Self::Large(vec); } } Self::Large(vec) => { vec.push(item); } } } pub fn as_slice(&self) -> &[T] { match self { Self::Small { data, len } => &data[..*len as usize], Self::Large(vec) => vec.as_slice(), } } } // Specialized methods for StackVec (byte buffers) impl StackVec { pub fn extend_from_slice(&mut self, slice: &[u8]) { match self { Self::Small { data, len } => { let current_len = *len as usize; let end = current_len + slice.len(); if end <= N { data[current_len..current_len + slice.len()].copy_from_slice(slice); *len += slice.len() as u8; } else { // Need to promote to large buffer let mut vec = Vec::with_capacity(current_len + slice.len()); vec.extend_from_slice(&data[..current_len]); vec.extend_from_slice(slice); *self = Self::Large(vec); } } Self::Large(vec) => { vec.extend_from_slice(slice); } } } } #[derive(Debug)] pub enum StackVecIter<'a, T, const N: usize> where T: Copy + Clone, { Small { data: &'a [T; N], len: u8, index: u8, }, Large(std::slice::Iter<'a, T>), } impl<'a, T, const N: usize> Iterator for StackVecIter<'a, T, N> where T: Copy + Clone, { type Item = &'a T; fn next(&mut self) -> Option { match self { Self::Small { data, len, index } => { if *index < *len { let result = &data[*index as usize]; *index += 1; Some(result) } else { None } } Self::Large(iter) => iter.next(), } } } #[cfg(test)] mod tests { use std::io::Cursor; use super::*; #[test] fn test_partial_parsing_with_remaining_bytes() { let data = [0x55, 0x54, 0x01, 0x00, 0xFF, 0x01, 0x00, 0x05]; let mut iter = ExtraFields::new(&data); assert_eq!(iter.remaining_bytes(), &data); let (id, body) = iter.next().unwrap(); assert_eq!(id, ExtraFieldId::EXTENDED_TIMESTAMP); assert_eq!(body, &[0xFF]); assert_eq!(iter.next(), None); assert_eq!(iter.remaining_bytes(), &[0x01, 0x00, 0x05]); } #[test] fn test_unknown_field_id() { let data = [0xFF, 0xFF, 0x02, 0x00, 0xDE, 0xAD]; let mut iter = ExtraFields::new(&data); let (id, body) = iter.next().unwrap(); assert_eq!(id, ExtraFieldId(0xFFFF)); assert_eq!(body, &[0xDE, 0xAD]); assert_eq!(iter.next(), None); } #[test] fn test_stack_vec_u8_inline_operations() { let mut buf = StackVec::::new(0); assert_eq!(buf.as_slice(), &[]); buf.push(1); assert_eq!(buf.as_slice(), &[1]); buf.extend_from_slice(&[2, 3]); assert_eq!(buf.as_slice(), &[1, 2, 3]); } #[test] fn test_stack_vec_u8_promote_to_heap() { let mut buf = StackVec::::new(0); // Fill inline capacity buf.extend_from_slice(&[1, 2]); assert_eq!(buf.as_slice(), &[1, 2]); // Force promotion to heap buf.extend_from_slice(&[3, 4, 5]); assert_eq!(buf.as_slice(), &[1, 2, 3, 4, 5]); buf.push(6); assert_eq!(buf.as_slice(), &[1, 2, 3, 4, 5, 6]); } #[test] fn test_stack_vec_size_constraints() { // Test that StackVec for bytes is same size as Vec assert!( std::mem::size_of::>() <= 24, "StackVec should not exceed Vec size on 64 bits" ); } #[test] fn test_stack_vec_clone() { let mut buf = StackVec::::new(0); buf.extend_from_slice(&[1, 2, 3]); // Force heap promotion let cloned = buf.clone(); assert_eq!(buf.as_slice(), cloned.as_slice()); } fn round_trip_extra_fields(fields: &[(ExtraFieldId, &[u8], Header)]) { let mut container = ExtraFieldsContainer::new(); for (id, data, location) in fields { container.add_field(*id, data, *location).unwrap(); } for location in [Header::LOCAL, Header::CENTRAL] { let mut cursor = Cursor::new(Vec::new()); container.write_extra_fields(&mut cursor, location).unwrap(); let written_fields = fields .iter() .filter(|&&(_, _, loc)| loc == location) .map(|&(id, data, _)| (id, data)) .collect::>(); let read_fields = ExtraFields::new(cursor.get_ref()).collect::>(); assert_eq!(written_fields, read_fields); } } #[test] fn test_extra_fields() { // Only local extra fields round_trip_extra_fields(&[ (ExtraFieldId::new(0), &[0u8; 16], Header::LOCAL), (ExtraFieldId::new(1), &[1u8; 16], Header::LOCAL), ]); // Only central extra fields round_trip_extra_fields(&[ (ExtraFieldId::new(0), &[0u8; 16], Header::CENTRAL), (ExtraFieldId::new(1), &[1u8; 16], Header::CENTRAL), ]); // Mixed extra fields where the local and central sizes are the same round_trip_extra_fields(&[ (ExtraFieldId::new(0), &[0u8; 16], Header::CENTRAL), (ExtraFieldId::new(1), &[1u8; 16], Header::LOCAL), ]); } } rawzip-0.4.4/src/headers.rs000064400000000000000000000057601046102023000137250ustar 00000000000000/// Specifies which ZIP headers to place data. /// /// The ZIP specification allows for different data in local file headers versus /// central directory headers. This type provides control over where data is /// placed. /// /// The default value is to place data in both header locations. /// /// For usage example, see /// [`ZipFileBuilder::extra_field`](crate::ZipFileBuilder::extra_field) #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Header(u8); impl Header { /// Include data only in the local file header. pub const LOCAL: Self = Self(0b01); /// Include data only in the central directory. pub const CENTRAL: Self = Self(0b10); #[inline] pub(crate) const fn new(value: u8) -> Self { Self(value) } #[inline] pub(crate) const fn includes_local(self) -> bool { self.0 & Self::LOCAL.0 != 0 } #[inline] pub(crate) const fn includes_central(self) -> bool { self.0 & Self::CENTRAL.0 != 0 } #[inline] pub(crate) const fn intersects(self, other: Self) -> bool { (self.0 & other.0) != 0 } } impl Default for Header { fn default() -> Self { Self(Self::LOCAL.0 | Self::CENTRAL.0) } } impl std::ops::BitOr for Header { type Output = Self; #[inline] fn bitor(self, rhs: Self) -> Self::Output { Self(self.0 | rhs.0) } } impl std::ops::BitOrAssign for Header { #[inline] fn bitor_assign(&mut self, rhs: Self) { self.0 |= rhs.0; } } impl std::ops::BitAnd for Header { type Output = Self; #[inline] fn bitand(self, rhs: Self) -> Self::Output { Self(self.0 & rhs.0) } } impl std::ops::BitAndAssign for Header { #[inline] fn bitand_assign(&mut self, rhs: Self) { self.0 &= rhs.0; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_header_bitflags_behavior() { // Test that default equals LOCAL | CENTRAL assert_eq!(Header::LOCAL | Header::CENTRAL, Header::default()); // Test includes methods assert!(Header::LOCAL.includes_local()); assert!(!Header::LOCAL.includes_central()); assert!(!Header::CENTRAL.includes_local()); assert!(Header::CENTRAL.includes_central()); assert!(Header::default().includes_local()); assert!(Header::default().includes_central()); // Test bitwise operations let mut header = Header::LOCAL; header |= Header::CENTRAL; assert_eq!(header, Header::default()); let intersection = Header::default() & Header::LOCAL; assert_eq!(intersection, Header::LOCAL); // Test intersects method assert!(Header::default().intersects(Header::LOCAL)); assert!(Header::default().intersects(Header::CENTRAL)); assert!(Header::LOCAL.intersects(Header::default())); assert!(!Header::LOCAL.intersects(Header::CENTRAL)); } #[test] fn test_header_default() { assert_eq!(Header::default(), Header::LOCAL | Header::CENTRAL); } } rawzip-0.4.4/src/lib.rs000064400000000000000000000007371046102023000130570ustar 00000000000000#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] #![forbid(unsafe_code)] mod archive; mod crc; mod errors; pub mod extra_fields; mod headers; mod locator; mod mode; pub mod path; mod reader_at; pub mod time; mod utils; mod writer; pub use archive::*; pub use crc::crc32; pub use errors::{Error, ErrorKind}; pub use headers::Header; pub use locator::*; pub use mode::EntryMode; pub use reader_at::{FileReader, RangeReader, ReaderAt}; pub use writer::*; rawzip-0.4.4/src/locator.rs000064400000000000000000001023211046102023000137440ustar 00000000000000use crate::errors::{Error, ErrorKind}; use crate::reader_at::{FileReader, ReaderAtExt}; use crate::utils::{le_u16, le_u32, le_u64}; use crate::{ ReaderAt, Zip64EndOfCentralDirectory, Zip64EndOfCentralDirectoryRecord, ZipArchive, ZipFileHeaderFixed, ZipSliceArchive, END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE, }; use std::cell::RefCell; use std::fs::File; use std::io::Seek; use std::num::NonZeroU64; const END_OF_CENTRAL_DIR_SIGNAUTRE: u32 = 0x06054b50; pub(crate) const END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES: [u8; 4] = END_OF_CENTRAL_DIR_SIGNAUTRE.to_le_bytes(); // https://github.com/zlib-ng/minizip-ng/blob/55db144e03027b43263e5ebcb599bf0878ba58de/mz_zip.c#L78 const END_OF_CENTRAL_DIR_MAX_OFFSET: u64 = 1 << 20; /// Locates the End of Central Directory (EOCD) record in a ZIP archive. /// /// The `ZipLocator` is responsible for finding the EOCD record, which is /// crucial for reading the contents of a ZIP file. /// /// In the event, that the comment or tailing data contains the EOCD signature, /// causing the zip locator to fail to parse. One can reparse the data starting /// from the false EOCD offset using the reported offset /// [`Error::eocd_offset()`] #[derive(Debug)] pub struct ZipLocator { max_search_space: u64, } impl Default for ZipLocator { fn default() -> Self { Self::new() } } impl ZipLocator { /// Creates a new `ZipLocator` with a default maximum search space of 1 MiB pub fn new() -> Self { ZipLocator { max_search_space: END_OF_CENTRAL_DIR_MAX_OFFSET, } } /// Sets the maximum number of bytes to search for the EOCD signature. /// /// The search is performed backwards from the end of the data source. /// /// ```rust /// use rawzip::ZipLocator; /// /// let locator = ZipLocator::new().max_search_space(1024 * 64); // 64 KiB /// ``` pub fn max_search_space(mut self, max_search_space: u64) -> Self { self.max_search_space = max_search_space; self } fn locate_in_byte_slice(&self, data: &[u8]) -> Result { let location = find_end_of_central_dir_signature(data, self.max_search_space as usize) .ok_or(ErrorKind::MissingEndOfCentralDirectory)?; let mut eocd = self .locate_in_byte_slice_impl(data, location) .map_err(|e| e.with_eocd_offset(location as u64))?; // Transparently verify that the self reported central directory points // to a valid entry. If it is not a valid entry, we can attempt to // correct offsets when there is undeclared prelude data by testing if // the central directory directly precedes the end of central directory // marker, which should hold true in the vast majority of cases. If both // checks fail, defer returning an error until the user explicitly wants // to iterate through the central directory. let first_entry = data .get(eocd.central_dir_offset as usize..) .filter(|d| ZipFileHeaderFixed::parse(d).is_ok()); match first_entry { None if !eocd.is_zip64() => { let cd_offset = eocd.eocd_offset.saturating_sub(eocd.central_dir_size); let first_entry = data .get(cd_offset as usize..) .filter(|d| ZipFileHeaderFixed::parse(d).is_ok()); if first_entry.is_some() { eocd.base_offset = cd_offset.saturating_sub(eocd.central_dir_offset); eocd.central_dir_offset = cd_offset; } Ok(eocd) } _ => Ok(eocd), } } fn locate_in_byte_slice_impl( &self, data: &[u8], location: usize, ) -> Result { let eocd = EndOfCentralDirectoryRecordFixed::parse(&data[location..])?; let is_zip64 = eocd.is_zip64(); let eocd = EndOfCentralDirectoryRecord::from_parts(location as u64, eocd); // Validate comment is completely present in the slice let comment_start = location + EndOfCentralDirectoryRecordFixed::SIZE; let comment_len = eocd.comment_len as usize; if comment_start + comment_len > data.len() { return Err(Error::from(ErrorKind::Eof)); } if !is_zip64 { return EndOfCentralDirectory::create(eocd); } let zip64l = &data[location.saturating_sub(Zip64EndOfCentralDirectoryLocatorRecord::SIZE)..]; let zip64_locator = Zip64EndOfCentralDirectoryLocatorRecord::parse(zip64l)?; let zip64_eocd = &data[(zip64_locator.directory_offset as usize).min(data.len())..]; let zip64_record = Zip64EndOfCentralDirectoryRecord::parse(zip64_eocd)?; let zip64 = Zip64EndOfCentralDirectory::from_parts(zip64_locator.directory_offset, zip64_record); EndOfCentralDirectory::create_zip64(eocd, zip64) } /// Locates the EOCD record within a byte slice. /// /// On success, returns a `ZipSliceArchive` which allows reading the archive /// directly from the slice. On failure, returns the original slice and an `Error`. /// /// # Examples /// /// ```rust /// use rawzip::ZipLocator; /// use std::fs; /// use std::io::Read; /// /// # fn main() -> Result<(), Box> { /// let mut file = fs::File::open("assets/readme.zip")?; /// let mut data = Vec::new(); /// file.read_to_end(&mut data)?; /// /// let locator = ZipLocator::new(); /// match locator.locate_in_slice(&data) { /// Ok(archive) => { /// println!("Found EOCD in slice, archive has {} files.", archive.entries_hint()); /// } /// Err((_data, e)) => { /// eprintln!("Failed to locate EOCD in slice: {:?}", e); /// } /// } /// # Ok(()) /// # } /// ``` pub fn locate_in_slice>( &self, data: T, ) -> Result, (T, Error)> { match self.locate_in_byte_slice(data.as_ref()) { Ok(eocd) => Ok(ZipSliceArchive::new(data, eocd)), Err(e) => Err((data, e)), } } /// Locates the EOCD record within a file. /// /// A mutable byte slice to use for reading data from the file. The buffer /// should be large enough to hold the EOCD record and potentially parts of /// the ZIP64 EOCD locator if present. A common size might be a few /// kilobytes. /// /// On failure, returns the original file and an `Error`. /// /// # Examples /// /// ```rust /// use rawzip::ZipLocator; /// use std::fs::File; /// /// # fn main() -> Result<(), Box> { /// let file = File::open("assets/readme.zip")?; /// let mut buffer = vec![0; rawzip::RECOMMENDED_BUFFER_SIZE]; /// let locator = ZipLocator::new(); /// /// match locator.locate_in_file(file, &mut buffer) { /// Ok(archive) => { /// println!("Found EOCD in file, archive has {} files.", archive.entries_hint()); /// } /// Err((_file, e)) => { /// eprintln!("Failed to locate EOCD in file: {:?}", e); /// } /// } /// # Ok(()) /// # } /// ``` pub fn locate_in_file( &self, file: std::fs::File, buffer: &mut [u8], ) -> Result, (File, Error)> { let mut reader = FileReader::from(file); let end_offset = match reader.seek(std::io::SeekFrom::End(0)) { Ok(offset) => offset, Err(e) => return Err((reader.into_inner(), Error::from(e))), }; self.locate_in_reader(reader, buffer, end_offset) .map_err(|(fr, e)| (fr.into_inner(), e)) } /// Locates the EOCD record in a reader, treating the specified end offset /// as the starting point when searching backwards. /// /// This method is useful for several scenarios: /// /// - Zip archive is nowhere near the end of the reader /// - Zip archives are concatenated /// /// For seekable readers, you can determine the end_offset by seeking to the /// end of the stream. /// /// Note that the zip locator may request data passed the end offset in /// order to read the entire end of the central directory record + comment. /// /// # Examples /// /// ```rust /// use rawzip::{ZipLocator, FileReader}; /// use std::fs::File; /// use std::io::Seek; /// /// # fn main() -> Result<(), rawzip::Error> { /// let file = File::open("assets/test.zip").unwrap(); /// let mut reader = FileReader::from(file); /// let mut buffer = vec![0; rawzip::RECOMMENDED_BUFFER_SIZE]; /// let locator = ZipLocator::new(); /// /// // An example of determining the end offset when you don't /// // the length but have a seekable reader. /// let end_offset = reader.seek(std::io::SeekFrom::End(0)).unwrap(); /// let archive = locator.locate_in_reader(reader, &mut buffer, end_offset) /// .map_err(|(_, e)| e)?; /// /// // Maybe there is another zip archive to be found. /// // To find where the current archive starts, we need the minimum local header /// // offset. Below we are being conservative and iterating through the entire central /// // directory for the start offset, but in reality out of order central directories /// // are an edge case. /// let zip_start = { /// let mut min_offset = u64::MAX; /// let mut entries = archive.entries(&mut buffer); /// while let Ok(Some(entry)) = entries.next_entry() { /// min_offset = min_offset.min(entry.local_header_offset()); /// } /// if min_offset == u64::MAX { 0 } else { min_offset } /// }; /// match locator.locate_in_reader(archive.get_ref(), &mut buffer, zip_start) { /// Ok(previous_archive) => { /// println!("Found previous ZIP archive!"); /// } /// Err((_, _)) => println!("No previous ZIP archive found"), /// } /// # Ok(()) /// # } /// ``` pub fn locate_in_reader( &self, mut reader: R, buffer: &mut [u8], end_offset: u64, ) -> Result, (R, Error)> where R: ReaderAt, { let location_result = find_end_of_central_dir(&mut reader, buffer, self.max_search_space, end_offset); let (eocd_offset, buffer_pos, buffer_valid_len) = match location_result { Ok(Some(location_tuple)) => location_tuple, Ok(None) => { return Err((reader, Error::from(ErrorKind::MissingEndOfCentralDirectory))); } Err(error) => { return Err((reader, Error::io(error))); } }; let (reader, mut eocd) = self .locate_in_reader_impl(reader, buffer, eocd_offset, buffer_pos, buffer_valid_len) .map_err(|(reader, e)| (reader, e.with_eocd_offset(eocd_offset)))?; // Check first entry in central directory, see // `ZipLocator::locate_in_byte_slice` for more info let first_entry = reader .read_exact_at( &mut buffer[..ZipFileHeaderFixed::SIZE], eocd.central_dir_offset, ) .ok() .filter(|_| ZipFileHeaderFixed::parse(buffer).is_ok()); match first_entry { None if !eocd.is_zip64() => { let cd_offset = eocd.eocd_offset.saturating_sub(eocd.central_dir_size); let first_entry = reader .read_exact_at(&mut buffer[..ZipFileHeaderFixed::SIZE], cd_offset) .ok() .filter(|_| ZipFileHeaderFixed::parse(buffer).is_ok()); if first_entry.is_some() { eocd.base_offset = cd_offset.saturating_sub(eocd.central_dir_offset); eocd.central_dir_offset = cd_offset; } Ok(ZipArchive::new(reader, eocd)) } _ => Ok(ZipArchive::new(reader, eocd)), } } fn locate_in_reader_impl( &self, reader: R, buffer: &mut [u8], eocd_offset: u64, buffer_pos: usize, buffer_valid_len: usize, ) -> Result<(R, EndOfCentralDirectory), (R, Error)> where R: ReaderAt, { // Most likely the single read to find the end of the central directory // will fill the buffer with entire end of the central directory (and // optionally zip64 end of central directory). So let's try and reuse // the the data already in memory as much as possible. let reader = Marker::new(reader); let mut end_of_central_directory = &buffer[buffer_pos..buffer_valid_len]; let eocd = loop { match EndOfCentralDirectoryRecordFixed::parse(end_of_central_directory) { Ok(record) => break record, Err(e) if e.is_eof() => { // Unhappy path: the end of central directory crossed over read boundaries let read = reader.read_at_least_at( buffer, EndOfCentralDirectoryRecordFixed::SIZE, eocd_offset, ); let read = match read { Ok(read) => read, Err(e) => return Err((reader.inner, e)), }; end_of_central_directory = &buffer[..read]; } Err(e) => return Err((reader.inner, e)), } }; let is_zip64 = eocd.is_zip64(); end_of_central_directory = &end_of_central_directory[EndOfCentralDirectoryRecordFixed::SIZE..]; let comment_len = eocd.comment_len as usize; // Check if the rest of the buffer doesn't completely contain the comment. if end_of_central_directory.len() < comment_len { let pos = end_of_central_directory.len(); let comment_offset = eocd_offset + EndOfCentralDirectoryRecordFixed::SIZE as u64 + pos as u64; let remaining_comment_len = comment_len - pos; // Try to read a single byte to validate the rest of the comment is accessible let mut temp_buf = [0u8; 1]; let end_comment_offset = comment_offset + remaining_comment_len as u64 - 1; if let Err(e) = reader.read_exact_at(&mut temp_buf, end_comment_offset) { return Err((reader.inner, Error::io(e))); } } let eocd = EndOfCentralDirectoryRecord::from_parts(eocd_offset, eocd); if !is_zip64 { return match EndOfCentralDirectory::create(eocd) { Ok(eocd) => Ok((reader.inner, eocd)), Err(e) => Err((reader.inner, e)), }; } let eocd64l_size = Zip64EndOfCentralDirectoryLocatorRecord::SIZE; // Unhappy path: if we needed to issue any reads since the original // eocd or don't have enough data in the buffer let eocd64l_pos = if reader.is_marked() || eocd64l_size > buffer_pos { if (eocd64l_size as u64) > eocd_offset { return Err(( reader.inner, Error::from(ErrorKind::MissingZip64EndOfCentralDirectory), )); } let read = reader.read_exact_at( &mut buffer[..eocd64l_size], eocd_offset - eocd64l_size as u64, ); match read { Ok(_) => 0, Err(e) => return Err((reader.inner, Error::io(e))), } } else { buffer_pos - eocd64l_size }; let zip64l_eocd = &buffer[eocd64l_pos..eocd64l_pos + eocd64l_size]; let zip64_locator = match Zip64EndOfCentralDirectoryLocatorRecord::parse(zip64l_eocd) { Ok(locator) => locator, Err(e) => return Err((reader.inner, e)), }; let zip64_eocd_fixed_size = Zip64EndOfCentralDirectoryRecord::SIZE; // Unhappy path: zip64 eocd is not in the original buffer let (eocd64_start, eocd64_end) = if reader.is_marked() || zip64_locator.directory_offset > eocd_offset || eocd_offset - zip64_locator.directory_offset > buffer_pos as u64 { let read = reader.try_read_at_least_at( buffer, zip64_eocd_fixed_size, zip64_locator.directory_offset, ); match read { Ok(read) => (0, read), Err(e) => { return Err((reader.inner, Error::io(e))); } } } else { ( buffer_pos - (eocd_offset - zip64_locator.directory_offset) as usize, buffer_valid_len, ) }; let zip64_eocd = &buffer[eocd64_start..eocd64_end]; let zip64_record = match Zip64EndOfCentralDirectoryRecord::parse(zip64_eocd) { Ok(record) => record, Err(e) => return Err((reader.inner, e)), }; // todo: zip64 extensible data sector let zip_eocd = Zip64EndOfCentralDirectory::from_parts(zip64_locator.directory_offset, zip64_record); match EndOfCentralDirectory::create_zip64(eocd, zip_eocd) { Ok(eocd) => Ok((reader.inner, eocd)), Err(e) => Err((reader.inner, e)), } } } #[derive(Debug, Clone)] pub(crate) struct EndOfCentralDirectory { eocd_offset: u64, zip64_eocd_offset: Option, central_dir_size: u64, central_dir_offset: u64, num_entries: u64, comment_len: u16, base_offset: u64, } impl EndOfCentralDirectory { pub(crate) fn create(eocd: EndOfCentralDirectoryRecord) -> Result { let result = EndOfCentralDirectory { eocd_offset: eocd.offset, zip64_eocd_offset: None, central_dir_size: u64::from(eocd.central_dir_size), central_dir_offset: u64::from(eocd.central_dir_offset), num_entries: u64::from(eocd.num_entries), comment_len: eocd.comment_len, base_offset: 0, }; result.validate()?; Ok(result) } pub(crate) fn create_zip64( eocd: EndOfCentralDirectoryRecord, zip64: Zip64EndOfCentralDirectory, ) -> Result { let result = EndOfCentralDirectory { eocd_offset: eocd.offset, zip64_eocd_offset: NonZeroU64::new(zip64.offset), central_dir_size: zip64.central_dir_size, central_dir_offset: zip64.central_dir_offset, num_entries: zip64.num_entries, comment_len: eocd.comment_len, base_offset: 0, }; result.validate()?; Ok(result) } fn validate(&self) -> Result<(), Error> { // It doesn't make sense if the start of the central directory is after // the end. if self.directory_offset() > self.head_eocd_offset() { return Err(Error::from(ErrorKind::InvalidEndOfCentralDirectory)); } Ok(()) } #[inline] pub(crate) fn is_zip64(&self) -> bool { self.zip64_eocd_offset.is_some() } pub(crate) fn base_offset(&self) -> u64 { self.base_offset } /// The first end of the central directory signature offsets. /// /// This is offset where no new central directory records are expected. /// /// Will be equivalent to [`Self::tail_eocd_offset`] eocd for non-zip64 files #[inline] pub(crate) fn head_eocd_offset(&self) -> u64 { self.zip64_eocd_offset .map(|x| x.get()) .unwrap_or(self.eocd_offset) } /// The last end of the central directory signature offsets. /// /// This will always be the byte offset of 0x06054b50 #[inline] pub(crate) fn tail_eocd_offset(&self) -> u64 { self.eocd_offset } /// offset of the start of the central directory #[inline] pub(crate) fn directory_offset(&self) -> u64 { self.central_dir_offset } #[inline] pub(crate) fn entries(&self) -> u64 { self.num_entries } #[inline] pub(crate) fn comment_len(&self) -> usize { self.comment_len as usize } } struct Marker { inner: T, marked: RefCell, } impl Marker { fn new(inner: T) -> Self { Self { inner, marked: RefCell::new(false), } } fn is_marked(&self) -> bool { *self.marked.borrow() } } impl ReaderAt for Marker where T: ReaderAt, { fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { match self.inner.read_at(buf, offset) { Ok(n) if n > 0 => { *self.marked.borrow_mut() = true; Ok(n) } x => x, } } } impl std::io::Seek for Marker where T: std::io::Seek, { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.inner.seek(pos) } } /// A non-zip64 end of central directory #[derive(Debug, Clone)] pub(crate) struct EndOfCentralDirectoryRecord { pub(crate) offset: u64, pub(crate) central_dir_size: u32, pub(crate) central_dir_offset: u32, pub(crate) num_entries: u16, pub(crate) comment_len: u16, } impl EndOfCentralDirectoryRecord { #[inline] pub fn from_parts(offset: u64, eocd: EndOfCentralDirectoryRecordFixed) -> Self { Self { offset, central_dir_size: eocd.central_dir_size, central_dir_offset: eocd.central_dir_offset, num_entries: eocd.total_entries, comment_len: eocd.comment_len, } } } #[derive(Debug, Clone)] pub(crate) struct EndOfCentralDirectoryRecordFixed { pub(crate) signature: u32, #[allow(dead_code)] pub(crate) disk_number: u16, #[allow(dead_code)] pub(crate) eocd_disk: u16, pub(crate) num_entries: u16, pub(crate) total_entries: u16, pub(crate) central_dir_size: u32, pub(crate) central_dir_offset: u32, pub(crate) comment_len: u16, } impl EndOfCentralDirectoryRecordFixed { pub(crate) const SIZE: usize = 22; pub fn parse(data: &[u8]) -> Result { if data.len() < Self::SIZE { return Err(Error::from(ErrorKind::Eof)); } let result = EndOfCentralDirectoryRecordFixed { signature: le_u32(&data[0..4]), disk_number: le_u16(&data[4..6]), eocd_disk: le_u16(&data[6..8]), num_entries: le_u16(&data[8..10]), total_entries: le_u16(&data[10..12]), central_dir_size: le_u32(&data[12..16]), central_dir_offset: le_u32(&data[16..20]), comment_len: le_u16(&data[20..22]), }; if result.signature != END_OF_CENTRAL_DIR_SIGNAUTRE { return Err(Error::from(ErrorKind::InvalidSignature { expected: END_OF_CENTRAL_DIR_SIGNAUTRE, actual: result.signature, })); } Ok(result) } pub fn is_zip64(&self) -> bool { // https://github.com/zlib-ng/minizip-ng/blob/55db144e03027b43263e5ebcb599bf0878ba58de/mz_zip.c#L1011 self.num_entries == u16::MAX || // 4.4.22 self.central_dir_offset == u32::MAX // 4.4.24 } } /// /// /// 4.3.15 #[derive(Debug)] #[allow(dead_code)] struct Zip64EndOfCentralDirectoryLocatorRecord { /// zip64 end of central dir locator signature pub signature: u32, /// number of the disk with the start of the zip64 end of central directory pub eocd_disk: u32, /// relative offset of the zip64 end of central directory record pub directory_offset: u64, /// total number of disks pub total_disks: u32, } impl Zip64EndOfCentralDirectoryLocatorRecord { const SIZE: usize = 20; pub fn parse(data: &[u8]) -> Result { if data.len() < Self::SIZE { return Err(Error::from(ErrorKind::Eof)); } let result = Zip64EndOfCentralDirectoryLocatorRecord { signature: le_u32(&data[0..4]), eocd_disk: le_u32(&data[4..8]), directory_offset: le_u64(&data[8..16]), total_disks: le_u32(&data[16..20]), }; if result.signature != END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE { return Err(Error::from(ErrorKind::InvalidSignature { expected: END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE, actual: result.signature, })); } Ok(result) } } pub(crate) fn find_end_of_central_dir_signature( data: &[u8], max_search_space: usize, ) -> Option { let start_search = data.len().saturating_sub(max_search_space); backwards_find( &data[start_search..], &END_OF_CENTRAL_DIR_SIGNAUTRE.to_le_bytes(), ) .map(|pos| pos + start_search) } pub(crate) fn find_end_of_central_dir( reader: T, buffer: &mut [u8], max_search_space: u64, end_offset: u64, ) -> std::io::Result> where T: ReaderAt, { if buffer.len() < END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES.len() { debug_assert!(false, "buffer not big enough to hold signature"); return Ok(None); } let max_back = end_offset.saturating_sub(max_search_space); let mut offset = end_offset; // The amount of data the remains in the stream let mut remaining = end_offset - max_back; // The number of bytes that were translated from the front to the back let mut carry_over = 0; loop { // We either want to read into the entire buffer (sans the bytes that // were carried over from the last read). Or we want to read the remainder let read_size = (buffer.len() - carry_over).min(remaining as usize); // Need to jump back to the start of the previous read and then how much // we want to read offset -= read_size as u64; // reader.seek_relative(-offset)?; reader.read_exact_at(&mut buffer[..read_size], offset)?; remaining -= read_size as u64; let haystack = &buffer[..read_size + carry_over]; if let Some(i) = backwards_find(haystack, &END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES) { let eocd_offset = (max_back + remaining) + (i as u64); return Ok(Some((eocd_offset, i, read_size + carry_over))); } if remaining == 0 { return Ok(None); } // Since the signature may be across read boundaries, match how much the // end of the signature matches the start of the buffer carry_over = match buffer { [b0, b1, b2, ..] if [*b0, *b1, *b2] == END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES[1..4] => 3, [b0, b1, ..] if [*b0, *b1] == END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES[2..4] => 2, [b0, ..] if *b0 == END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES[3] => 1, _ => 0, }; if carry_over > 0 { // place the carry over bytes at the end of the buffer for the next read let dest = (buffer.len() - carry_over).min(remaining as usize); buffer.copy_within(..carry_over, dest); } } } fn backwards_find(haystack: &[u8], needle: &[u8]) -> Option { haystack .windows(needle.len()) .rposition(|window| window == needle) } #[cfg(test)] mod tests { use super::*; use quickcheck_macros::quickcheck; use rstest::rstest; use std::io::Cursor; #[quickcheck] fn test_find_end_of_central_dir_signature(mut data: Vec, offset: usize, chunk_size: u16) { if data.len() < 4 { return; } let max_search_space = END_OF_CENTRAL_DIR_MAX_OFFSET; let pos = (offset % data.len()).saturating_sub(END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES.len()); data[pos..pos + 4].copy_from_slice(&END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES); let result = find_end_of_central_dir_signature(&data, max_search_space as usize).unwrap(); let mut buffer = vec![0u8; chunk_size.max(4) as usize]; let reader = std::io::Cursor::new(&data); let (index, buffer_index, buffer_valid_len) = find_end_of_central_dir(reader, &mut buffer, max_search_space, data.len() as u64) .unwrap() .unwrap(); assert_eq!(index, result as u64); assert!(buffer_valid_len > 0, "buffer_valid_len should be positive"); assert!( buffer_valid_len <= buffer.len(), "buffer_valid_len should not exceed buffer capacity" ); assert!( buffer_index < buffer_valid_len, "buffer_index should be within buffer_valid_len" ); assert!( buffer_index + END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES.len() <= buffer_valid_len, "signature should be within valid part of buffer" ); assert_eq!( buffer[buffer_index..buffer_index + 4], END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES ); } #[quickcheck] fn test_find_end_of_central_dir_signature_random( data: Vec, chunk_size: u16, max_search_space: u64, ) { let mem = find_end_of_central_dir_signature(&data, max_search_space as usize); let mut buffer = vec![0u8; chunk_size.max(4) as usize]; let reader = std::io::Cursor::new(&data); let curse = find_end_of_central_dir(reader, &mut buffer, max_search_space, data.len() as u64) .unwrap(); let mem_result = mem.map(|x| x as u64); let curse_result = curse.map(|(a, _, _)| a); assert_eq!(mem_result, curse_result); if let Some((_, buffer_index, buffer_valid_len)) = curse { assert!(buffer_valid_len > 0, "buffer_valid_len should be positive"); assert!( buffer_valid_len <= buffer.len(), "buffer_valid_len should not exceed buffer capacity" ); assert!( buffer_index < buffer_valid_len, "buffer_index should be within buffer_valid_len" ); assert!( buffer_index + END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES.len() <= buffer_valid_len, "signature should be within valid part of buffer" ); } } #[rstest] #[case(&[], 4, 1000, None)] #[case(&[6], 4, 1000, None)] #[case(&[5, 6], 4, 1000, None)] #[case(&[b'K', 5, 6], 4, 1000, None)] #[case(&[0, 6, 0, 0, 0], 4, 1000, None)] #[case(&[b'P', b'K', 5, 6], 4, 1000, Some(0))] #[case(&[b'P', b'K', 5, 6], 5, 1000, Some(0))] #[case(&[b'P', b'K', 5, 6, 5, 6], 5, 1000, Some(0))] #[case(&[b'P', b'K', 5, 6, 6, 0, 0, 0], 4, 1000, Some(0))] #[case(&[b'P', b'K', 5, 6, 0, 0, 0, 0], 4, 1000, Some(0))] #[case(&[b'P', b'K', 5, 6, 0, 0, 0], 4, 1000, Some(0))] #[case(&[b'P', b'K', 5, 6, 0], 4, 1000, Some(0))] #[case(&[5, 6, b'P', b'K', 5, 6], 4, 1000, Some(2))] #[case(&[5, 6, b'P', b'K', 5, 6], 5, 1000, Some(2))] #[case(&[5, 6, b'P', b'K', 5, 6, 5, 6], 4, 1000, Some(2))] #[case(&[5, 6, b'P', b'K', 5, 6, 5, 6], 5, 1000, Some(2))] #[case(&[b'P', b'K', 5, 6, b'P', b'K', 5, 6, 5, 6], 5, 1000, Some(4))] #[case(&[b'P', b'K', 5, 6, b'P', b'K', 5, 6, 5, 6], 32, 1000, Some(4))] #[case(&[b'P', b'K', 5, 6], 5, 4, Some(0))] // start of max search space tests #[case(&[b'P', b'K', 5, 6, 5, 6], 5, 5, None)] #[case(&[b'P', b'K', 5, 6, 6, 0, 0, 0], 4, 8, Some(0))] #[case(&[b'P', b'K', 5, 6, 0, 0, 0], 4, 8, Some(0))] #[case(&[b'P', b'K', 5, 6, 0], 4, 4, None)] #[case(&[5, 6, b'P', b'K', 5, 6], 4, 4, Some(2))] #[case(&[5, 6, b'P', b'K', 5, 6], 5, 4, Some(2))] #[case(&[5, 6, b'P', b'K', 5, 6, 5, 6], 4, 4, None)] #[case(&[5, 6, b'P', b'K', 5, 6, 5, 6], 5, 4, None)] #[case(&[b'P', b'K', 5, 6, b'P', b'K', 5, 6, 5, 6], 5, 6, Some(4))] #[case(&[b'P', b'K', 5, 6, b'P', b'K', 5, 6, 5, 6], 32, 10, Some(4))] #[test] fn test_find_end_of_central_dir_signature_cases( #[case] input: &[u8], #[case] buffer_size: usize, #[case] max_search_space: u64, #[case] expected: Option, ) { let result = find_end_of_central_dir_signature(input, max_search_space as usize); assert_eq!(result.map(|x| x as u64), expected); let cursor = Cursor::new(&input); let mut buffer = vec![0u8; buffer_size]; let found = find_end_of_central_dir(cursor, &mut buffer, max_search_space, input.len() as u64) .unwrap(); let found_result = found.map(|(a, _, _)| a); assert_eq!(found_result, expected); if expected.is_some() { let (_, buffer_pos, buffer_valid_len) = found.unwrap(); assert!(buffer_valid_len > 0, "buffer_valid_len should be positive"); assert!( buffer_valid_len <= buffer_size, "buffer_valid_len should not exceed buffer capacity" ); assert!( buffer_pos < buffer_valid_len, "buffer_index should be within buffer_valid_len" ); assert!( buffer_pos + END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES.len() <= buffer_valid_len, "signature should be within valid part of buffer" ); assert_eq!( buffer[buffer_pos..buffer_pos + 4], END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES ); } } } rawzip-0.4.4/src/mode.rs000064400000000000000000000052041046102023000132270ustar 00000000000000/// ZIP creator system constants used in version_made_by field pub(crate) const CREATOR_UNIX: u16 = 3; pub(crate) const CREATOR_MACOS: u16 = 19; pub(crate) const CREATOR_NTFS: u16 = 11; pub(crate) const CREATOR_VFAT: u16 = 14; pub(crate) const CREATOR_FAT: u16 = 0; /// File mode information for a given zip file entry. /// /// This represents Unix-style file permissions and type information. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct EntryMode(u32); impl EntryMode { /// Creates a new Mode from a raw mode value. #[must_use] pub(crate) const fn new(value: u32) -> Self { Self(value) } /// Returns the raw mode value #[must_use] pub const fn value(&self) -> u32 { self.0 } /// Returns true if this is a symbolic link. #[must_use] pub const fn is_symlink(&self) -> bool { self.0 & S_IFMT == S_IFLNK } /// Returns the Unix permission bits (e.g., 0o755). #[must_use] pub const fn permissions(&self) -> u32 { self.0 & 0o777 } } /// Unix file type and permission constants const S_IFMT: u32 = 0o170000; // File type mask const S_IFSOCK: u32 = 0o140000; // Socket const S_IFLNK: u32 = 0o120000; // Symbolic link const S_IFREG: u32 = 0o100000; // Regular file const S_IFBLK: u32 = 0o060000; // Block device const S_IFDIR: u32 = 0o040000; // Directory const S_IFCHR: u32 = 0o020000; // Character device const S_IFIFO: u32 = 0o010000; // FIFO const S_ISUID: u32 = 0o004000; // Set user ID const S_ISGID: u32 = 0o002000; // Set group ID const S_ISVTX: u32 = 0o001000; // Sticky bit /// MSDOS file attribute constants const MSDOS_DIR: u32 = 0x10; const MSDOS_READONLY: u32 = 0x01; /// Converts Unix mode to file mode pub(crate) fn unix_mode_to_file_mode(m: u32) -> u32 { let mut mode = m & 0o777; // Basic permissions // Set file type bits based on Unix mode match m & S_IFMT { S_IFBLK => mode |= S_IFBLK, S_IFCHR => mode |= S_IFCHR, S_IFDIR => mode |= S_IFDIR, S_IFIFO => mode |= S_IFIFO, S_IFLNK => mode |= S_IFLNK, S_IFSOCK => mode |= S_IFSOCK, _ => mode |= S_IFREG, // Default to regular file } // Set special permission bits if m & S_ISGID != 0 { mode |= S_ISGID; } if m & S_ISUID != 0 { mode |= S_ISUID; } if m & S_ISVTX != 0 { mode |= S_ISVTX; } mode } /// Converts MSDOS attributes to file mode, following Go's zip reader logic pub(crate) fn msdos_mode_to_file_mode(m: u32) -> u32 { if m & MSDOS_DIR != 0 { S_IFDIR | 0o777 } else if m & MSDOS_READONLY != 0 { S_IFREG | 0o444 } else { S_IFREG | 0o666 } } rawzip-0.4.4/src/path.rs000064400000000000000000000352731046102023000132500ustar 00000000000000//! Path handling for ZIP archives with type-safe raw and normalized paths. //! //! This module provides a comprehensive system for handling file paths from ZIP //! archives with strong safety guarantees against path traversal attacks (zip //! slip vulnerabilities). //! //! ## Path Types //! //! The main type is [`ZipFilePath`], which is generic over three possible path //! types with different safety levels: //! //! - [`RawPath`]: Direct bytes from ZIP archive (⚠️ may contain malicious //! paths) //! - [`NormalizedPath`]: Validated and sanitized path //! - [`NormalizedPathBuf`]: Owned version of normalized path //! //! ## Raw Paths //! //! Raw paths provide direct access to the original bytes from the ZIP file //! without any validation. //! //! May contain the following: //! //! - Directory traversal: `../`, `..\\`, `..` sequences //! - Absolute paths: `/etc/passwd`, `C:\\Windows\\system32` //! - Invalid UTF-8: Arbitrary byte sequences that aren't valid text //! //! ## Normalized Paths //! //! Normalized paths have been validated and sanitized according to these rules: //! //! - Assumed to be UTF-8 ([zip file names aren't always //! UTF-8](https://fasterthanli.me/articles/the-case-for-sans-io#character-encoding-differences)) //! - Path separators: All backslashes (`\`) converted to forward slashes (`/`) //! - Redundant slashes: Multiple consecutive slashes (`//`) reduced to single //! slash //! - Relative components: Current directory (`.`) and parent directory (`..`) //! resolved //! - Leading separators: Absolute paths made relative (`/foo` → `foo`) //! - Drive letters: Windows drive prefixes removed (`C:\\foo` → `foo`) //! - Escape prevention: Paths cannot escape the archive root directory //! //! ## Usage Examples //! //! ```rust //! use rawzip::path::ZipFilePath; //! //! // From raw bytes //! let raw_path = ZipFilePath::from_bytes(b"../../../etc/passwd"); //! let safe_path = raw_path.try_normalize()?; // Returns error if invalid UTF-8 //! assert_eq!(safe_path.as_str(), "etc/passwd"); //! //! // From string //! let normalized_path = ZipFilePath::from_str("dir\\file.txt"); //! assert_eq!(normalized_path.as_str(), "dir/file.txt"); //! assert_eq!(String::from(normalized_path), "dir/file.txt"); //! //! // Backslashes to forward slashes //! let path = ZipFilePath::from_str("dir\\subdir\\file.txt"); //! assert_eq!(path.as_str(), "dir/subdir/file.txt"); //! //! // Remove redundant slashes //! let path = ZipFilePath::from_str("dir//subdir///file.txt"); //! assert_eq!(path.as_str(), "dir/subdir/file.txt"); //! //! // Resolve relative components //! let path = ZipFilePath::from_str("dir/../file.txt"); //! assert_eq!(path.as_str(), "file.txt"); //! //! // Remove leading slashes (absolute → relative) //! let path = ZipFilePath::from_str("/etc/passwd"); //! assert_eq!(path.as_str(), "etc/passwd"); //! //! // Prevent directory traversal //! let path = ZipFilePath::from_str("../../../etc/passwd"); //! assert_eq!(path.as_str(), "etc/passwd"); //! //! // Get string from normalized path //! let path = ZipFilePath::from_str("dir/file.txt"); //! let my_str = String::from(path.into_owned()); //! assert_eq!(my_str, String::from("dir/file.txt")); //! //! # Ok::<(), Box>(()) //! ``` //! //! ## UTF-8 Encoding Detection //! //! The library automatically detects when paths contain characters that require //! UTF-8 encoding in ZIP files (beyond the default CP-437 encoding). This //! information is used internally when creating ZIP archives. use crate::{Error, ZipStr}; use std::borrow::Cow; /// Raw path data directly from a ZIP archive. /// /// **Warning**: Contains unvalidated bytes that may include malicious path components. /// Use [`ZipFilePath::try_normalize()`] to create a safe path. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct RawPath<'a>(ZipStr<'a>); impl AsRef<[u8]> for RawPath<'_> { #[inline] fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } /// A normalized and sanitized path from a ZIP archive. /// /// This path has been validated and sanitized according to the normalization /// rules described in the module documentation. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct NormalizedPath<'a>(Cow<'a, str>); impl AsRef<[u8]> for NormalizedPath<'_> { #[inline] fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } impl AsRef for NormalizedPath<'_> { #[inline] fn as_ref(&self) -> &str { self.0.as_ref() } } /// An owned, normalized path from a ZIP archive. /// /// Owned version of [`NormalizedPath`] with the same safety guarantees. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct NormalizedPathBuf(String); impl AsRef<[u8]> for NormalizedPathBuf { #[inline] fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } impl AsRef for NormalizedPathBuf { #[inline] fn as_ref(&self) -> &str { &self.0 } } /// Type-safe wrapper for ZIP archive file paths. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ZipFilePath { data: R, } impl ZipFilePath<()> { /// Creates a raw path from bytes. /// /// **Warning**: The resulting path is unvalidated. Use [`ZipFilePath::try_normalize()`] /// to create a safe path. #[inline] pub fn from_bytes(data: &[u8]) -> ZipFilePath> { ZipFilePath { data: RawPath(ZipStr::new(data)), } } /// Creates a normalized path from a UTF-8 string. /// /// The path is automatically normalized according to the rules described in the module /// documentation. When possible, the original string reference is preserved to avoid allocation. #[inline] #[allow(clippy::should_implement_trait)] // Can't implement FromStr due to lifetime issues pub fn from_str(mut name: &str) -> ZipFilePath> { let mut last = 0; for &c in name.as_bytes() { if matches!( (c, last), (b'\\', _) | (b'/', b'/') | (b'.', b'.') | (b'.', b'/') | (b':', _) ) { // slow path: intrusive string manipulations required return ZipFilePath { data: NormalizedPath(Cow::Owned(Self::normalize_alloc(name))), }; } last = c; } loop { // Fast path: before we trim, do a quick check if they are even necessary. name = match name.as_bytes() { [b'.', b'.', b'/', ..] => name.trim_start_matches("../"), [b'.', b'/', ..] => name.trim_start_matches("./"), [b'/', ..] => name.trim_start_matches('/'), _ => { return ZipFilePath { data: NormalizedPath(Cow::Borrowed(name)), } } } } } fn normalize_alloc(s: &str) -> String { // 4.4.17.1 All slashes MUST be forward slashes '/' let s = s.replace('\\', "/"); // 4.4.17.1 MUST NOT contain a drive or device letter let s = s.split(':').next_back().unwrap_or_default(); // resolve path components let splits = s.split('/'); let mut result = String::new(); for split in splits { if split.is_empty() || split == "." { continue; } if split == ".." { let last = result.rfind('/'); result.truncate(last.unwrap_or(0)); continue; } if !result.is_empty() { result.push('/'); } result.push_str(split); } result } } impl ZipFilePath where R: AsRef<[u8]>, { /// Returns true if the file path represents a directory. /// /// Determined by the path ending with a forward slash (`/`). #[inline] pub fn is_dir(&self) -> bool { self.data.as_ref().last() == Some(&b'/') } /// Returns the length of the path in bytes. #[inline] pub fn len(&self) -> usize { self.data.as_ref().len() } /// Returns true if the path is empty. #[inline] pub fn is_empty(&self) -> bool { self.data.as_ref().is_empty() } } impl ZipFilePath where R: AsRef, { /// Determines if the path requires UTF-8 encoding based on CP-437 compatibility. /// /// Returns `true` if the path contains characters that cannot be represented in CP-437 /// (the default ZIP encoding), requiring the UTF-8 flag to be set in the ZIP file. pub(crate) fn needs_utf8_encoding(&self) -> bool { for ch in self.data.as_ref().chars() { let code_point = ch as u32; // Forbid 0x7e (~) and 0x5c (\) since EUC-KR and Shift-JIS replace those // characters with localized currency and overline characters. // Also forbid control characters (< 0x20) and characters above 0x7d. if !(0x20..=0x7d).contains(&code_point) || code_point == 0x5c { return true; } } false } } impl<'a> ZipFilePath> { /// Returns the raw bytes of the zip file path. #[inline] pub fn as_bytes(&self) -> &'a [u8] { self.data.0.as_bytes() } /// Attempts to normalize this raw path into a safe, validated path. /// /// Validates the raw bytes as UTF-8 and applies normalization rules. /// /// # Errors /// /// Returns an error if the file path contains invalid UTF-8 sequences. #[inline] pub fn try_normalize(self) -> Result>, Error> { let raw_data = self.data.0; let name = std::str::from_utf8(raw_data.as_bytes()).map_err(Error::utf8)?; Ok(ZipFilePath::from_str(name)) } } impl AsRef<[u8]> for ZipFilePath> { #[inline] fn as_ref(&self) -> &[u8] { self.data.0.as_bytes() } } impl AsRef for ZipFilePath> { #[inline] fn as_ref(&self) -> &str { self.data.0.as_ref() } } impl AsRef for ZipFilePath { #[inline] fn as_ref(&self) -> &str { self.data.0.as_ref() } } impl From> for String { #[inline] fn from(path: ZipFilePath) -> Self { path.data.0 } } impl From>> for String { #[inline] fn from(path: ZipFilePath>) -> Self { path.data.0.into_owned() } } impl ZipFilePath> { /// Returns the normalized string slice. #[inline] pub fn as_str(&self) -> &str { self.data.0.as_ref() } /// Converts this borrowed path into an owned path. /// /// Similar to [`Cow::into_owned`] #[inline] pub fn into_owned(self) -> ZipFilePath { ZipFilePath { data: NormalizedPathBuf(self.data.0.into_owned()), } } } impl ZipFilePath { /// Returns the normalized string slice. #[inline] pub fn as_str(&self) -> &str { self.data.0.as_ref() } } #[cfg(test)] mod tests { use super::*; use rstest::rstest; #[rstest] #[case(b"test.txt", "test.txt")] #[case(b"dir/test.txt", "dir/test.txt")] #[case(b"dir\\test.txt", "dir/test.txt")] #[case(b"dir//test.txt", "dir/test.txt")] #[case(b"/test.txt", "test.txt")] #[case(b"../test.txt", "test.txt")] #[case(b"dir/../test.txt", "test.txt")] #[case(b"./test.txt", "test.txt")] #[case(b"dir/./test.txt", "dir/test.txt")] #[case(b"dir/./../test.txt", "test.txt")] #[case(b"dir/sub/../test.txt", "dir/test.txt")] #[case(b"dir/../../test.txt", "test.txt")] #[case(b"../../../test.txt", "test.txt")] #[case(b"a/b/../../test.txt", "test.txt")] #[case(b"a/b/c/../../../test.txt", "test.txt")] #[case(b"a/b/c/d/../../test.txt", "a/b/test.txt")] #[case(b"C:\\hello\\test.txt", "hello/test.txt")] #[case(b"C:/hello\\test.txt", "hello/test.txt")] #[case(b"C:/hello/test.txt", "hello/test.txt")] fn test_zip_path_normalized(#[case] input: &[u8], #[case] expected: &str) { assert_eq!( ZipFilePath::from_bytes(input) .try_normalize() .unwrap() .as_ref(), expected ); } #[rstest] #[case(&[0xFF])] #[case(&[b't', b'e', b's', b't', 0xFF])] fn test_zip_path_normalized_invalid_utf8(#[case] input: &[u8]) { assert!(ZipFilePath::from_bytes(input).try_normalize().is_err()); } #[rstest] #[case("test.txt", false)] #[case("hello_world", false)] #[case("file.name.ext", false)] #[case("hello!", false)] #[case("hello{world}", false)] #[case("hello|world", false)] #[case("hello`world", false)] #[case("hello\"world", false)] #[case("hello", false)] #[case("hello;world", false)] #[case("hello:world", false)] #[case("hello^world", false)] #[case("hello\u{00A0}world", true)] #[case("hello\u{0080}world", true)] #[case("hello\u{00FF}world", true)] #[case("hello\u{0100}world", true)] #[case("hello\u{03B1}world", true)] #[case("hello\u{4E00}world", true)] #[case("hello\u{1F600}world", true)] #[case(r"hello\world", false)] // Backslash gets normalized to forward slash #[case("hello~world", true)] #[case("hello\u{007F}world", true)] #[case("hello\u{001F}world", true)] #[case("hello\u{0000}world", true)] #[case("hello\u{0001}world", true)] #[case("hello\u{000A}world", true)] #[case("hello\u{000D}world", true)] #[case("hello\u{0009}world", true)] #[case("", false)] #[case(" ", false)] #[case("hello\u{007E}world", true)] #[case("hello\u{007D}world", false)] fn test_needs_utf8_encoding(#[case] input: &str, #[case] expected: bool) { let path = ZipFilePath::from_str(input); assert_eq!( path.needs_utf8_encoding(), expected, "Failed for input: {}", input ); } #[test] fn test_path_lifetime_test() { let normalized_path = ZipFilePath::from_bytes(b"test.txt") .try_normalize() .unwrap(); assert_eq!(normalized_path.as_ref(), "test.txt"); assert_eq!(normalized_path.len(), 8); } #[test] fn test_raw_path_lifetime_preservation() { use std::str::Utf8Error; // See https://github.com/nickbabcock/rawzip/issues/101 fn file_path_utf8<'a>(path: ZipFilePath>) -> Result<&'a str, Utf8Error> { std::str::from_utf8(path.as_bytes()) } let raw_path = ZipFilePath::from_bytes(b"test/file.txt"); let result = file_path_utf8(raw_path).unwrap(); assert_eq!(result, "test/file.txt"); } } rawzip-0.4.4/src/reader_at.rs000064400000000000000000000373041046102023000142370ustar 00000000000000use crate::errors::{Error, ErrorKind}; use std::io::Read; use std::ops::Range; #[cfg(unix)] use std::os::unix::fs::FileExt; #[cfg(windows)] use std::os::windows::fs::FileExt; use std::{rc::Rc, sync::Arc}; /// Provides reading bytes at a specific offset /// /// This trait is similar to [`std::io::Read`] but with an additional offset /// parameter that signals where the read should begin offset from the start of /// the data. This allows methods to not require a mutable reference to the /// reader, which is critical for zip files to easily offer decompression of /// multiple files simultaneously without needing to store them in memory. /// /// This trait is modelled after Go's /// [`io.ReaderAt`](https://pkg.go.dev/io#ReaderAt) interface, which is used by /// their own [Zip implementation](https://pkg.go.dev/archive/zip#NewReader). pub trait ReaderAt { /// Read bytes from the reader at a specific offset fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result; /// Sibling to [`read_exact`](std::io::Read::read_exact), but at an offset fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result<()> { let mut read = 0; while read < buf.len() { let latest = self.read_at(&mut buf[read..], offset + (read as u64))?; if latest == 0 { return Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "failed to fill whole buffer", )); } read += latest; } Ok(()) } } pub(crate) trait ReaderAtExt { fn try_read_at_least_at( &self, buffer: &mut [u8], size: usize, offset: u64, ) -> std::io::Result; fn read_at_least_at(&self, buffer: &mut [u8], size: usize, offset: u64) -> Result; } impl ReaderAtExt for T { fn try_read_at_least_at( &self, buffer: &mut [u8], mut size: usize, offset: u64, ) -> std::io::Result { size = size.min(buffer.len()); let mut pos = 0; while pos < size { let read = self.read_at(&mut buffer[pos..], offset + pos as u64)?; if read == 0 { return Ok(pos); } pos += read; } Ok(pos) } fn read_at_least_at( &self, buffer: &mut [u8], size: usize, offset: u64, ) -> Result { if buffer.len() < size { return Err(Error::from(ErrorKind::BufferTooSmall)); } let read = self.try_read_at_least_at(buffer, size, offset)?; if read < size { return Err(Error::from(ErrorKind::Eof)); } Ok(read) } } #[cfg(not(any(unix, windows)))] #[derive(Debug)] pub struct FileReader(MutexReader); /// A file wrapper that implements [`ReaderAt`] across platforms. #[cfg(any(unix, windows))] #[derive(Debug)] pub struct FileReader(std::fs::File); impl FileReader { pub fn into_inner(self) -> std::fs::File { #[cfg(not(any(unix, windows)))] return self.0.into_inner(); #[cfg(any(unix, windows))] return self.0; } } impl ReaderAt for FileReader { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { #[cfg(unix)] return self.0.read_at(buf, offset); #[cfg(windows)] return self.0.seek_read(buf, offset); #[cfg(not(any(unix, windows)))] return self.0.read_at(buf, offset); } } impl std::io::Seek for FileReader { #[inline] fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.0.seek(pos) } } impl From for FileReader { #[cfg(not(any(unix, windows)))] fn from(file: std::fs::File) -> Self { Self(MutexReader(std::sync::Mutex::new(file))) } #[cfg(any(unix, windows))] fn from(file: std::fs::File) -> Self { Self(file) } } /// A reader that is wrapped in a mutex to allow for concurrent reads. #[derive(Debug)] pub struct MutexReader(std::sync::Mutex); impl MutexReader { pub fn new(inner: R) -> Self { Self(std::sync::Mutex::new(inner)) } pub fn into_inner(self) -> R { self.0.into_inner().unwrap() } } impl ReaderAt for MutexReader where R: std::io::Read + std::io::Seek, { /// For seekable implementations, we can emulate the read_at method by /// seeking to the offset, reading the data, and then seeking back to the /// original position within a mutex. /// /// This is how Go implements the `io.ReaderAt` interface for filed on /// Windows: /// https://github.com/golang/go/blob/70b603f4d295573197b43ad090d7cad21895144e/src/internal/poll/fd_windows.go#L525 fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { let mut lock = self.0.lock().unwrap(); let original_position = lock.stream_position()?; lock.seek(std::io::SeekFrom::Start(offset))?; let result = lock.read(buf); lock.seek(std::io::SeekFrom::Start(original_position))?; result } } impl std::io::Read for MutexReader where R: std::io::Read, { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.0.lock().unwrap().read(buf) } } impl std::io::Seek for MutexReader where R: std::io::Seek, { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.0.lock().unwrap().seek(pos) } } impl ReaderAt for &'_ T { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { (*self).read_at(buf, offset) } } impl ReaderAt for &'_ mut T { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { (**self).read_at(buf, offset) } } impl ReaderAt for &[u8] { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { let skip = self.len().min(offset as usize); let data = &self[skip..]; let len = data.len().min(buf.len()); buf[..len].copy_from_slice(&data[..len]); Ok(len) } } impl ReaderAt for std::io::Cursor where R: AsRef<[u8]>, { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { let data = self.get_ref().as_ref(); data.read_at(buf, offset) } } impl ReaderAt for Vec { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { self.as_slice().read_at(buf, offset) } } impl ReaderAt for Arc { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { (**self).read_at(buf, offset) } } impl ReaderAt for Rc { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { (**self).read_at(buf, offset) } } impl ReaderAt for Box { #[inline] fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result { (**self).read_at(buf, offset) } } /// A reader that reads a specific range of data from a [`ReaderAt`] source. /// /// `RangeReader` implements [`std::io::Read`] and provides bounded reading /// within a specified range of offsets. It maintains its current position and /// ensures reads don't exceed the defined end boundary. /// /// Useful when working with APIs that operate on [`std::io::Read`] instead of /// [`ReaderAt`]. For instance, incrementally reading large prelude and trailing /// data of a ZIP file. /// /// # Examples /// /// Reading prelude data from a zip file: /// /// ``` /// use std::io::Read; /// use rawzip::{ZipArchive, RangeReader, RECOMMENDED_BUFFER_SIZE}; /// use std::fs::File; /// /// let file = File::open("assets/test-prefix.zip")?; /// let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; /// let archive = ZipArchive::from_file(file, &mut buffer)?; /// /// // Typically you only need the first entry to find where the zip data starts /// // but this is the longer form that examines every entry in case they are /// // out of order /// let mut zip_start_offset = archive.directory_offset(); /// let mut entries = archive.entries(&mut buffer); /// while let Some(entry) = entries.next_entry()? { /// zip_start_offset = zip_start_offset.min(entry.local_header_offset()); /// } /// /// // For example purposes, just slurp up all the prelude data /// let mut prelude_reader = RangeReader::new(archive.get_ref(), 0..zip_start_offset); /// prelude_reader.read_exact(&mut buffer[..zip_start_offset as usize])?; /// assert_eq!( /// &buffer[..zip_start_offset as usize], /// b"prefix that could be an executable jar file" /// ); /// # Ok::<(), Box>(()) /// ``` #[derive(Debug, Clone)] pub struct RangeReader { archive: R, offset: u64, end_offset: u64, } impl RangeReader { /// Creates a new `RangeReader` that will read data from the specified range. #[inline] pub fn new(archive: R, range: Range) -> Self { Self { archive, offset: range.start, end_offset: range.end, } } /// Returns the current read position within the range. #[inline] pub fn position(&self) -> u64 { self.offset } /// Returns the remaining bytes that are expected to be read from the /// current position. /// /// When a range reader is constructed with a range that exceeds the /// underlying reader, remaining will be non-zero when `read()` returns zero /// signalling the end of the stream. #[inline] pub fn remaining(&self) -> u64 { self.end_offset - self.offset } /// Returns the end offset of the range. #[inline] pub fn end_offset(&self) -> u64 { self.end_offset } /// Returns a reference to the underlying reader. #[inline] pub fn get_ref(&self) -> &R { &self.archive } /// Consumes the self and returns the underlying reader. #[inline] pub fn into_inner(self) -> R { self.archive } } impl Read for RangeReader where R: ReaderAt, { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read_size = buf.len().min(self.remaining() as usize); let read = self.archive.read_at(&mut buf[..read_size], self.offset)?; self.offset += read as u64; Ok(read) } } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; const TEST_DATA: &[u8] = b"Hello, World! This is test data for ReaderAt implementations."; fn test_reader_at_impl(reader: R, data_len: usize) { let mut buf = [0u8; 5]; // Test reading from start assert_eq!(reader.read_at(&mut buf, 0).unwrap(), 5); assert_eq!(&buf, b"Hello"); // Test reading from offset buf.fill(0); assert_eq!(reader.read_at(&mut buf, 7).unwrap(), 5); assert_eq!(&buf, b"World"); // Test read beyond data length buf.fill(0); let bytes_read = reader.read_at(&mut buf, data_len as u64).unwrap(); assert_eq!(bytes_read, 0); // Test partial read at end of data buf.fill(0); let bytes_read = reader.read_at(&mut buf, (data_len - 3) as u64).unwrap(); assert_eq!(bytes_read, 3); assert_eq!(&buf[..3], &TEST_DATA[data_len - 3..]); } #[test] fn test_smart_pointer_implementations() { let data = TEST_DATA.to_vec(); // Test Arc> let arc_reader = Arc::new(data.clone()); test_reader_at_impl(&*arc_reader, data.len()); test_reader_at_impl(arc_reader, data.len()); // Test Rc> let rc_reader = Rc::new(data.clone()); test_reader_at_impl(&*rc_reader, data.len()); test_reader_at_impl(rc_reader, data.len()); // Test Box> let box_reader = Box::new(data.clone()); test_reader_at_impl(&*box_reader, data.len()); test_reader_at_impl(box_reader, data.len()); } #[test] fn test_reference_implementations() { let mut data = TEST_DATA.to_vec(); let data_len = data.len(); test_reader_at_impl(&data, data_len); test_reader_at_impl(&mut data, data_len); } #[test] fn test_byte_slice_implementation() { let data = TEST_DATA; test_reader_at_impl(data, data.len()); } #[test] fn test_cursor_implementation() { let data = TEST_DATA.to_vec(); let cursor = Cursor::new(data.clone()); test_reader_at_impl(&cursor, data.len()); } #[test] fn test_vec_implementation() { let data = TEST_DATA.to_vec(); test_reader_at_impl(&data, data.len()); } #[test] fn test_range_reader_basic() { let data = b"Hello, World! This is test data."; let mut range_reader = RangeReader::new(data.as_slice(), 7..13); let mut buffer = [0u8; 10]; let bytes_read = range_reader.read(&mut buffer).unwrap(); assert_eq!(bytes_read, 6); assert_eq!(&buffer[..bytes_read], b"World!"); } #[test] fn test_range_reader_multiple_reads() { let data = b"0123456789"; let mut range_reader = RangeReader::new(data.as_slice(), 2..8); let mut buffer = [0u8; 3]; let bytes_read1 = range_reader.read(&mut buffer).unwrap(); assert_eq!(bytes_read1, 3); assert_eq!(&buffer[..bytes_read1], b"234"); assert_eq!(range_reader.position(), 5); let bytes_read2 = range_reader.read(&mut buffer).unwrap(); assert_eq!(bytes_read2, 3); assert_eq!(&buffer[..bytes_read2], b"567"); assert_eq!(range_reader.position(), 8); // Should return 0 when at end let bytes_read3 = range_reader.read(&mut buffer).unwrap(); assert_eq!(bytes_read3, 0); } #[test] fn test_range_reader_empty_range() { let data = b"Hello, World!"; let mut range_reader = RangeReader::new(data.as_slice(), 5..5); let mut buffer = [0u8; 10]; let bytes_read = range_reader.read(&mut buffer).unwrap(); assert_eq!(bytes_read, 0); assert_eq!(range_reader.remaining(), 0); } #[test] fn test_range_reader_get_ref_and_into_inner() { let data = b"Hello, World!"; let range_reader = RangeReader::new(data.as_slice(), 0..5); assert_eq!(range_reader.get_ref(), &data.as_slice()); let inner = range_reader.into_inner(); assert_eq!(inner, data.as_slice()); } #[test] fn test_range_reader_clone() { let data = b"Hello, World!"; let range_reader = RangeReader::new(data.as_slice(), 0..5); let cloned = range_reader.clone(); assert_eq!(range_reader.position(), cloned.position()); assert_eq!(range_reader.remaining(), cloned.remaining()); } #[test] fn test_range_reader_range_exceeds_data() { let data = b"Hello"; // Test range that starts within data but extends beyond let mut reader1 = RangeReader::new(data.as_slice(), 3..10); let mut buf1 = [0u8; 10]; let read1 = reader1.read(&mut buf1).unwrap(); assert_eq!(read1, 2); // Only reads "lo" assert_eq!(&buf1[..read1], b"lo"); // Test range that starts at end of data let mut reader2 = RangeReader::new(data.as_slice(), 5..10); let mut buf2 = [0u8; 10]; let read2 = reader2.read(&mut buf2).unwrap(); assert_eq!(read2, 0); // No data to read // Test range that starts beyond data let mut reader3 = RangeReader::new(data.as_slice(), 10..20); let mut buf3 = [0u8; 10]; let read3 = reader3.read(&mut buf3).unwrap(); assert_eq!(read3, 0); // No data to read } } rawzip-0.4.4/src/time.rs000064400000000000000000001242411046102023000132440ustar 00000000000000//! ZIP file timestamp handling //! //! Datetimes for ZIP files come in two flavors: UTC and local time. It is not //! possible for the local time zone to be encoded in the ZIP format, so //! converting between the two requires assuming that UTC is the local time. //! //! When reading a ZIP file, [`ZipDateTimeKind`] will provide information about //! the timestamp's original time zone (UTC and local time) //! //! However, when writing a ZIP file, only a [`UtcDateTime`] is supported. //! //! # Example: Copying Modification Times //! //! This example shows how to read a ZIP file and create a new one while //! preserving modification times: //! //! ``` //! use rawzip::{ZipArchive, ZipArchiveWriter, ZipDataWriter}; //! use rawzip::time::{ZipDateTimeKind, UtcDateTime}; //! use std::io::Write; //! //! // Read a test ZIP file with timestamps //! let input_data = include_bytes!("../assets/time-go.zip"); //! let input_archive = ZipArchive::from_slice(input_data).unwrap(); //! //! // Create output archive //! let mut output_data = Vec::new(); //! let mut output_archive = ZipArchiveWriter::new(&mut output_data); //! //! // Copy each entry with its modification time //! let mut entries = input_archive.entries(); //! while let Ok(Some(entry)) = entries.next_entry() { //! let name = entry.file_path().try_normalize().unwrap().as_ref().to_string(); //! let modification_time = entry.last_modified(); //! //! let utc_time = match modification_time { //! ZipDateTimeKind::Utc(utc_time) => utc_time, //! ZipDateTimeKind::Local(local_time) => { //! // Convert local time to UTC by reinterpreting the components //! // This treats the local time as if it were UTC //! UtcDateTime::from_components( //! local_time.year(), //! local_time.month(), //! local_time.day(), //! local_time.hour(), //! local_time.minute(), //! local_time.second(), //! local_time.nanosecond() //! ).unwrap() //! } //! }; //! //! if !entry.is_dir() { //! // Copy file with preserved modification time //! let (mut entry, config) = output_archive.new_file(&name) //! .last_modified(utc_time) //! .start() //! .unwrap(); //! let mut writer = config.wrap(&mut entry); //! writer.write_all(b"example data").unwrap(); //! let (_, descriptor) = writer.finish().unwrap(); //! entry.finish(descriptor).unwrap(); //! } else { //! // Copy directory with preserved modification time //! output_archive.new_dir(&name) //! .last_modified(utc_time) //! .create() //! .unwrap(); //! } //! } //! //! output_archive.finish().unwrap(); //! //! // Verify the output archive preserves timestamps //! let output_archive = ZipArchive::from_slice(&output_data).unwrap(); //! //! assert!(output_archive.entries_hint() > 0, "Output should contain entries"); //! //! // Verify at least one entry has a UTC timestamp //! let mut output_entries = output_archive.entries(); //! let mut has_utc_timestamp = false; //! while let Ok(Some(entry)) = output_entries.next_entry() { //! if matches!(entry.last_modified(), ZipDateTimeKind::Utc(_)) { //! has_utc_timestamp = true; //! break; //! } //! } //! assert!(has_utc_timestamp, "Output should contain UTC timestamps"); //! ``` use crate::{ extra_fields::{ExtraFieldId, ExtraFields}, utils::{le_u16, le_u32, le_u64}, }; /// Represents the time zone of a timestamp. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TimeZone { /// UTC (Coordinated Universal Time) Utc, /// Local time (timezone unknown) Local, } /// Marker type for UTC timezone #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Utc; /// Marker type for Local timezone #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Local; /// Trait for timezone markers pub trait TimeZoneMarker { fn timezone() -> TimeZone; } impl TimeZoneMarker for Utc { fn timezone() -> TimeZone { TimeZone::Utc } } impl TimeZoneMarker for Local { fn timezone() -> TimeZone { TimeZone::Local } } /// Represents a timestamp found in a ZIP file #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ZipDateTime { year: u16, month: u8, // 1-12 day: u8, // 1-31 hour: u8, // 0-23 minute: u8, // 0-59 second: u8, // 0-59 nanosecond: u32, // 0-999,999,999 _timezone: std::marker::PhantomData, } /// Type alias for UTC timestamps pub type UtcDateTime = ZipDateTime; /// Type alias for Local timestamps pub type LocalDateTime = ZipDateTime; /// Enum for timestamp parsing results that can be either UTC or Local #[derive(Debug, Clone, PartialEq, Eq)] pub enum ZipDateTimeKind { Utc(UtcDateTime), Local(LocalDateTime), } impl ZipDateTimeKind { /// Returns the timezone of this timestamp #[must_use] pub const fn timezone(&self) -> TimeZone { match self { ZipDateTimeKind::Utc(_) => TimeZone::Utc, ZipDateTimeKind::Local(_) => TimeZone::Local, } } /// Returns the year component of the timestamp #[must_use] pub fn year(&self) -> u16 { match self { ZipDateTimeKind::Utc(dt) => dt.year(), ZipDateTimeKind::Local(dt) => dt.year(), } } /// Returns the month component (1-12) of the timestamp #[must_use] pub fn month(&self) -> u8 { match self { ZipDateTimeKind::Utc(dt) => dt.month(), ZipDateTimeKind::Local(dt) => dt.month(), } } /// Returns the day component (1-31) of the timestamp #[must_use] pub fn day(&self) -> u8 { match self { ZipDateTimeKind::Utc(dt) => dt.day(), ZipDateTimeKind::Local(dt) => dt.day(), } } /// Returns the hour component (0-23) of the timestamp #[must_use] pub fn hour(&self) -> u8 { match self { ZipDateTimeKind::Utc(dt) => dt.hour(), ZipDateTimeKind::Local(dt) => dt.hour(), } } /// Returns the minute component (0-59) of the timestamp #[must_use] pub fn minute(&self) -> u8 { match self { ZipDateTimeKind::Utc(dt) => dt.minute(), ZipDateTimeKind::Local(dt) => dt.minute(), } } /// Returns the second component (0-59) of the timestamp #[must_use] pub fn second(&self) -> u8 { match self { ZipDateTimeKind::Utc(dt) => dt.second(), ZipDateTimeKind::Local(dt) => dt.second(), } } /// Returns the nanosecond component (0-999,999,999) of the timestamp #[must_use] pub fn nanosecond(&self) -> u32 { match self { ZipDateTimeKind::Utc(dt) => dt.nanosecond(), ZipDateTimeKind::Local(dt) => dt.nanosecond(), } } } impl std::fmt::Display for ZipDateTimeKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ZipDateTimeKind::Utc(dt) => dt.fmt(f), ZipDateTimeKind::Local(dt) => dt.fmt(f), } } } impl std::fmt::Display for ZipDateTime { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Write out the date and time in ISO 8601 format. RFC 3339 requires a // time zone, which we won't have for local times. write!( f, "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", self.year, self.month, self.day, self.hour, self.minute, self.second )?; if self.nanosecond != 0 { write!(f, ".{:09}", self.nanosecond)?; } match TZ::timezone() { TimeZone::Utc => write!(f, "Z"), TimeZone::Local => Ok(()), } } } impl ZipDateTime { /// Creates a ZipDateTime from date/time components with validation. /// /// # Arguments /// /// * `year` - Year (1-65535) /// * `month` - Month (1-12) /// * `day` - Day of month (1-31, validated against month) /// * `hour` - Hour (0-23) /// * `minute` - Minute (0-59) /// * `second` - Second (0-59) /// * `nanosecond` - Nanosecond (0-999,999,999), defaults to 0 /// /// # Errors /// /// Returns `None` if any component is invalid or the date doesn't exist /// (e.g. February 30th, April 31st). /// /// # Examples /// /// ``` /// # use rawzip::time::{UtcDateTime, LocalDateTime}; /// let utc_datetime = UtcDateTime::from_components( /// 2023, 6, 15, 14, 30, 45, 500_000_000 /// ).unwrap(); /// assert_eq!(utc_datetime.year(), 2023); /// assert_eq!(utc_datetime.nanosecond(), 500_000_000); /// /// // Invalid date returns None /// assert!(UtcDateTime::from_components(2023, 2, 30, 0, 0, 0, 0).is_none()); /// ``` pub fn from_components( year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8, nanosecond: u32, ) -> Option { // Validate components if year == 0 || month == 0 || month > 12 || day == 0 || hour > 23 || minute > 59 || second > 59 || nanosecond > 999_999_999 { return None; } let max_day = last_day_of_month(year, month); if day > max_day { return None; } Some(Self { year, month, day, hour, minute, second, nanosecond, _timezone: std::marker::PhantomData, }) } /// Returns the year component of the timestamp. #[must_use] pub const fn year(&self) -> u16 { self.year } /// Returns the month component (1-12) of the timestamp. #[must_use] pub const fn month(&self) -> u8 { self.month } /// Returns the day component (1-31) of the timestamp. #[must_use] pub const fn day(&self) -> u8 { self.day } /// Returns the hour component (0-23) of the timestamp. #[must_use] pub const fn hour(&self) -> u8 { self.hour } /// Returns the minute component (0-59) of the timestamp. #[must_use] pub const fn minute(&self) -> u8 { self.minute } /// Returns the second component (0-59) of the timestamp. #[must_use] pub const fn second(&self) -> u8 { self.second } /// Returns the nanosecond component (0-999,999,999) of the timestamp. /// For timestamps that don't support nanosecond precision, this returns 0. #[must_use] pub const fn nanosecond(&self) -> u32 { self.nanosecond } /// Returns the timezone of this timestamp. #[must_use] pub fn timezone(&self) -> TimeZone { TZ::timezone() } /// Calculate days since Unix epoch (1970-01-01) for this date. /// /// Based on Howard Hinnant's `days_from_civil` algorithm: /// /// /// Negative values indicate dates prior to 1970-01-01. const fn days_from_civil(&self) -> i32 { let (y, m) = if self.month <= 2 { (self.year as i32 - 1, self.month as i32 + 9) } else { (self.year as i32, self.month as i32 - 3) }; // Calculate era (400-year cycles) let era = y / 400; let yoe = y - era * 400; // year of era [0, 399] // Calculate day of year let doy = (153 * m + 2) / 5 + self.day as i32 - 1; // day of year [0, 365] // Calculate day of era let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // day of era [0, 146096] // Calculate days since epoch (era 0 starts at year 0, not 1970) era * 146097 + doe - 719468 } } impl ZipDateTime { /// Creates a ZipDateTime from a Unix timestamp (seconds since epoch) pub fn from_unix(seconds: i64) -> UtcDateTime { let (year, month, day, hour, minute, second) = unix_timestamp_to_components(seconds); ZipDateTime { year, month, day, hour, minute, second, nanosecond: 0, _timezone: std::marker::PhantomData, } } /// Creates a ZipDateTime from an NTFS timestamp (100ns ticks since 1601) pub(crate) fn from_ntfs(ticks: u64) -> UtcDateTime { let unix_seconds = (ticks / 10_000_000).saturating_sub(NTFS_EPOCH_OFFSET) as i64; let (year, month, day, hour, minute, second) = unix_timestamp_to_components(unix_seconds); let nanosecond = ((ticks % 10_000_000) * 100) as u32; ZipDateTime { year, month, day, hour, minute, second, nanosecond, _timezone: std::marker::PhantomData, } } /// Convert to Unix timestamp (seconds since epoch). /// /// Returns the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). /// Negative values represent dates before 1970. #[must_use] pub fn to_unix(&self) -> i64 { let days_since_epoch = self.days_from_civil(); (i64::from(days_since_epoch)) * 86400 + (i64::from(self.hour)) * 3600 + (i64::from(self.minute)) * 60 + (i64::from(self.second)) } } impl ZipDateTime { /// Creates a ZipDateTime from a DosDateTime pub(crate) fn from_dos(dos: DosDateTime) -> LocalDateTime { // Note: DOS timestamps with month=0 and day=0 are a gray area. Some // seem to normalize to 1980-01-01 while others normalize to 1979-11-30. ZipDateTime { year: dos.year(), month: dos.month(), day: dos.day(), hour: dos.hour(), minute: dos.minute(), second: dos.second(), nanosecond: 0, _timezone: std::marker::PhantomData, } } } /// Represents an MS-DOS timestamp with 2-second precision. /// /// MS-DOS timestamps are stored as packed 16-bit values for date and time, /// with a limited range from 1980 to 2107 and 2-second precision for seconds. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DosDateTime { time: u16, date: u16, } impl DosDateTime { /// Creates a new MS-DOS datetime from packed date and time values. #[must_use] pub(crate) const fn new(time: u16, date: u16) -> Self { Self { time, date } } /// Returns the year (1980-2107). #[must_use] pub fn year(&self) -> u16 { ((self.date >> 9) & 0x7f) + 1980 } /// Returns the month (1-12). #[must_use] pub fn month(&self) -> u8 { let raw_month = ((self.date >> 5) & 0x0f) as u8; raw_month.clamp(1, 12) } /// Returns the day of the month (1-31). #[must_use] pub fn day(&self) -> u8 { let raw_day = (self.date & 0x1f) as u8; raw_day.clamp(1, last_day_of_month(self.year(), self.month())) } /// Returns the hour (0-23). #[must_use] pub fn hour(&self) -> u8 { let raw_hour = ((self.time >> 11) & 0x1f) as u8; raw_hour.min(23) } /// Returns the minute (0-59). #[must_use] pub fn minute(&self) -> u8 { let raw_minute = ((self.time >> 5) & 0x3f) as u8; raw_minute.min(59) } /// Returns the second (0-58, always even due to 2-second precision). #[must_use] pub fn second(&self) -> u8 { let raw_second = ((self.time & 0x1f) * 2) as u8; raw_second.min(58) } /// Returns the packed time and date components as (time, date). #[must_use] pub(crate) const fn into_parts(self) -> (u16, u16) { (self.time, self.date) } } impl From<&ZipDateTime> for DosDateTime { fn from(zip_dt: &ZipDateTime) -> Self { // Saturate year to DOS range (1980-2107) let dos_year = zip_dt.year.clamp(1980, 2107); // Pack the date: bits 15-9: year-1980, bits 8-5: month, bits 4-0: day let packed_date = ((dos_year - 1980) << 9) | ((zip_dt.month as u16) << 5) | (zip_dt.day as u16); // Pack the time: bits 15-11: hour, bits 10-5: minute, bits 4-0: second/2 let packed_time = ((zip_dt.hour as u16) << 11) | ((zip_dt.minute as u16) << 5) | ((zip_dt.second as u16) / 2); Self { time: packed_time, date: packed_date, } } } /// Extracts timestamp from the extra field using "last wins" strategy. /// Returns the last valid timestamp found, or falls back to MS-DOS if none found. /// This matches Go's zip reader behavior. pub(crate) fn extract_best_timestamp( extra_fields: ExtraFields<'_>, dos_time: u16, dos_date: u16, ) -> ZipDateTimeKind { let mut last_timestamp = None; for (field_id, field_data) in extra_fields { match field_id { ExtraFieldId::NTFS => { if let Some(timestamp) = parse_ntfs_timestamp(field_data) { last_timestamp = Some(ZipDateTimeKind::Utc(timestamp)); } } ExtraFieldId::EXTENDED_TIMESTAMP => { if let Some(timestamp) = parse_extended_timestamp(field_data) { last_timestamp = Some(ZipDateTimeKind::Utc(timestamp)); } } ExtraFieldId::INFO_ZIP_UNIX_ORIGINAL => { if let Some(timestamp) = parse_unix_timestamp(field_data) { last_timestamp = Some(ZipDateTimeKind::Utc(timestamp)); } } _ => {} } } // Return the last timestamp found, or fall back to MS-DOS last_timestamp.unwrap_or_else(|| { ZipDateTimeKind::Local(LocalDateTime::from_dos(DosDateTime::new( dos_time, dos_date, ))) }) } /// Parses NTFS timestamp extra field (0x000a) fn parse_ntfs_timestamp(data: &[u8]) -> Option { if data.len() < 32 { return None; } // NTFS extra field format: // 4 bytes: reserved (usually 0) // 2 bytes: attribute tag (0x0001 for timestamps) // 2 bytes: attribute size (24 bytes for 3 timestamps) // 8 bytes: modification time // 8 bytes: access time // 8 bytes: creation time let tag = le_u16(&data[4..6]); if tag != 0x0001 { return None; } let size = le_u16(&data[6..8]) as usize; if size < 24 || data.len() < 8 + size { return None; } // Extract modification time (first 8 bytes of timestamp data) let mtime_ticks = le_u64(&data[8..16]); Some(UtcDateTime::from_ntfs(mtime_ticks)) } /// Parses Extended Timestamp extra field (0x5455) fn parse_extended_timestamp(data: &[u8]) -> Option { if data.len() < 5 { return None; } let flags = data[0]; let pos = 1; // Check if modification time is present (bit 0) if flags & 0x01 != 0 && pos + 4 <= data.len() { let mtime_seconds = le_u32(&data[pos..pos + 4]); return Some(UtcDateTime::from_unix(i64::from(mtime_seconds))); } None } /// Parses Unix timestamp extra field (0x5855) - obsolete format fn parse_unix_timestamp(data: &[u8]) -> Option { if data.len() < 8 { return None; } // Unix format has access time first, then modification time let mtime_seconds = le_u32(&data[4..8]); Some(UtcDateTime::from_unix(i64::from(mtime_seconds))) } /// Convert Unix timestamp to broken down date/time components /// /// Based on Howard Hinnant's date library algorithm `civil_from_days`: /// /// fn unix_timestamp_to_components(timestamp: i64) -> (u16, u8, u8, u8, u8, u8) { const SECONDS_PER_DAY: i64 = 86400; // Break timestamp into days and seconds within day let total_days = timestamp / SECONDS_PER_DAY; let mut seconds_in_day = timestamp % SECONDS_PER_DAY; // Handle negative remainder for negative timestamps if seconds_in_day < 0 { seconds_in_day += SECONDS_PER_DAY; } // Convert seconds within day to H:M:S let hour = (seconds_in_day / 3600) as u8; let minute = ((seconds_in_day % 3600) / 60) as u8; let second = (seconds_in_day % 60) as u8; let days_since_epoch = total_days; // Shift epoch from 1970-01-01 to 0000-03-01 for easier leap year handling // This makes March 1st, year 0 our epoch (which aligns with leap year cycle) let days_since_shifted_epoch = days_since_epoch + 719468; // Days from 0000-03-01 to 1970-01-01 // Calculate the era (400-year period) let era = days_since_shifted_epoch / 146097; let days_of_era = days_since_shifted_epoch % 146097; // Calculate year within the era (0-399) let year_of_era = (days_of_era - days_of_era / 1460 + days_of_era / 36524 - days_of_era / 146096) / 365; // Calculate the actual year let year = era * 400 + year_of_era; // Calculate day of year let days_before_year = year_of_era * 365 + year_of_era / 4 - year_of_era / 100; let day_of_year = days_of_era - days_before_year; // Calculate month and day // Months are shifted: Mar=0, Apr=1, ..., Dec=9, Jan=10, Feb=11 let month_shifted = (5 * day_of_year + 2) / 153; let day_of_month = day_of_year - (153 * month_shifted + 2) / 5 + 1; // Convert back to normal calendar let (final_year, final_month) = if month_shifted < 10 { (year, month_shifted + 3) } else { (year + 1, month_shifted - 9) }; ( final_year as u16, final_month as u8, day_of_month as u8, hour, minute, second, ) } // NTFS timestamp is 100-nanosecond intervals since 1601-01-01 00:00:00 UTC const NTFS_EPOCH_OFFSET: u64 = 11644473600; // Seconds between 1601-01-01 and 1970-01-01 /// Returns true if the given year is a leap year. const fn is_leap(year: u16) -> bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } /// Returns the last valid day of the given month in the given year. const fn last_day_of_month(year: u16, month: u8) -> u8 { if month != 2 || !is_leap(year) { last_day_of_month_common_year(month as usize) } else { 29 } } const fn last_day_of_month_common_year(m: usize) -> u8 { [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1] } #[cfg(test)] mod tests { use super::*; fn utc_from_components( year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8, nanosecond: u32, ) -> UtcDateTime { UtcDateTime::from_components(year, month, day, hour, minute, second, nanosecond).unwrap() } fn local_from_components( year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8, nanosecond: u32, ) -> LocalDateTime { LocalDateTime::from_components(year, month, day, hour, minute, second, nanosecond).unwrap() } #[test] fn test_zip_to_dos_conversion() { // Test normal conversion let zip_dt = utc_from_components(2023, 6, 15, 14, 30, 45, 0); let dos_dt: DosDateTime = (&zip_dt).into(); let (dos_time, dos_date) = dos_dt.into_parts(); let dos_dt_check = DosDateTime::new(dos_time, dos_date); assert_eq!(dos_dt_check.year(), 2023); assert_eq!(dos_dt_check.month(), 6); assert_eq!(dos_dt_check.day(), 15); assert_eq!(dos_dt_check.hour(), 14); assert_eq!(dos_dt_check.minute(), 30); assert_eq!(dos_dt_check.second(), 44); // Rounded down to even second } #[test] fn test_zip_to_dos_year_saturation() { // Test year before DOS range (should saturate to 1980) let zip_dt_before = utc_from_components(1979, 6, 15, 14, 30, 45, 0); let dos_dt: DosDateTime = (&zip_dt_before).into(); let (dos_time, dos_date) = dos_dt.into_parts(); let dos_dt_check = DosDateTime::new(dos_time, dos_date); assert_eq!(dos_dt_check.year(), 1980); // Saturated to minimum assert_eq!(dos_dt_check.month(), 6); assert_eq!(dos_dt_check.day(), 15); // Test year way before DOS range let zip_dt_way_before = utc_from_components(1800, 1, 1, 0, 0, 0, 0); let dos_dt2: DosDateTime = (&zip_dt_way_before).into(); let (dos_time2, dos_date2) = dos_dt2.into_parts(); let dos_dt2_check = DosDateTime::new(dos_time2, dos_date2); assert_eq!(dos_dt2_check.year(), 1980); // Saturated to minimum // Test year after DOS range (should saturate to 2107) let zip_dt_after = utc_from_components(2108, 6, 15, 14, 30, 45, 0); let dos_dt3: DosDateTime = (&zip_dt_after).into(); let (dos_time3, dos_date3) = dos_dt3.into_parts(); let dos_dt3_check = DosDateTime::new(dos_time3, dos_date3); assert_eq!(dos_dt3_check.year(), 2107); // Saturated to maximum assert_eq!(dos_dt3_check.month(), 6); assert_eq!(dos_dt3_check.day(), 15); // Test year way after DOS range let zip_dt_way_after = utc_from_components(3000, 12, 31, 23, 59, 59, 0); let dos_dt4: DosDateTime = (&zip_dt_way_after).into(); let (dos_time4, dos_date4) = dos_dt4.into_parts(); let dos_dt4_check = DosDateTime::new(dos_time4, dos_date4); assert_eq!(dos_dt4_check.year(), 2107); // Saturated to maximum } #[test] fn test_dos_datetime() { // Test using the From trait let zip_dt = utc_from_components(2023, 6, 15, 14, 30, 45, 0); let dos_dt: DosDateTime = (&zip_dt).into(); assert_eq!(dos_dt.year(), 2023); assert_eq!(dos_dt.month(), 6); assert_eq!(dos_dt.day(), 15); assert_eq!(dos_dt.hour(), 14); assert_eq!(dos_dt.minute(), 30); assert_eq!(dos_dt.second(), 44); // Rounded down to even second } #[test] fn test_dos_datetime_odd_seconds() { // Test that odd seconds are rounded down using the From trait let zip_dt_odd = utc_from_components(2020, 1, 1, 12, 30, 45, 0); let dos_dt_odd: DosDateTime = (&zip_dt_odd).into(); assert_eq!(dos_dt_odd.second(), 44); // 45 rounded down to 44 let zip_dt_even = utc_from_components(2020, 1, 1, 12, 30, 46, 0); let dos_dt_even: DosDateTime = (&zip_dt_even).into(); assert_eq!(dos_dt_even.second(), 46); // 46 stays 46 } #[test] fn test_dos_datetime_edge_cases() { // Test minimum date using From trait let zip_dt_min = utc_from_components(1980, 1, 1, 0, 0, 0, 0); let dos_dt_min: DosDateTime = (&zip_dt_min).into(); assert_eq!(dos_dt_min.year(), 1980); assert_eq!(dos_dt_min.month(), 1); assert_eq!(dos_dt_min.day(), 1); // Test maximum date using From trait let zip_dt_max = utc_from_components(2107, 12, 31, 23, 59, 58, 0); let dos_dt_max: DosDateTime = (&zip_dt_max).into(); assert_eq!(dos_dt_max.year(), 2107); assert_eq!(dos_dt_max.month(), 12); assert_eq!(dos_dt_max.day(), 31); assert_eq!(dos_dt_max.hour(), 23); assert_eq!(dos_dt_max.minute(), 59); assert_eq!(dos_dt_max.second(), 58); } #[test] fn test_dos_datetime_zero_normalization() { // Test that zero DOS timestamp (0x0000 0x0000) is normalized to 1980-01-01 00:00:00 let datetime = DosDateTime::new(0x0000, 0x0000); assert_eq!(datetime.year(), 1980); assert_eq!(datetime.month(), 1); // month 0 normalized to 1 assert_eq!(datetime.day(), 1); // day 0 normalized to 1 assert_eq!(datetime.hour(), 0); assert_eq!(datetime.minute(), 0); assert_eq!(datetime.second(), 0); // Test partial zero normalization - only month is zero let datetime = DosDateTime::new(0x0000, 0x0001); // day=1, month=0, year=1980 assert_eq!(datetime.year(), 1980); assert_eq!(datetime.month(), 1); // month 0 normalized to 1 assert_eq!(datetime.day(), 1); assert_eq!(datetime.hour(), 0); assert_eq!(datetime.minute(), 0); assert_eq!(datetime.second(), 0); // Test partial zero normalization - only day is zero let datetime = DosDateTime::new(0x0000, 0x0020); // day=0, month=1, year=1980 assert_eq!(datetime.year(), 1980); assert_eq!(datetime.month(), 1); assert_eq!(datetime.day(), 1); // day 0 normalized to 1 assert_eq!(datetime.hour(), 0); assert_eq!(datetime.minute(), 0); assert_eq!(datetime.second(), 0); } #[test] fn test_zip_datetime_dos() { let datetime = local_from_components(2020, 6, 15, 14, 30, 44, 0); assert_eq!(datetime.year(), 2020); assert_eq!(datetime.month(), 6); assert_eq!(datetime.day(), 15); assert_eq!(datetime.hour(), 14); assert_eq!(datetime.minute(), 30); assert_eq!(datetime.second(), 44); assert_eq!(datetime.nanosecond(), 0); assert_eq!(datetime.timezone(), TimeZone::Local); } #[test] fn test_zip_datetime_unix() { // Unix timestamp for 2010-09-05 02:12:01 UTC let datetime = utc_from_components(2010, 9, 5, 2, 12, 1, 0); assert_eq!(datetime.year(), 2010); assert_eq!(datetime.month(), 9); assert_eq!(datetime.day(), 5); assert_eq!(datetime.hour(), 2); assert_eq!(datetime.minute(), 12); assert_eq!(datetime.second(), 1); assert_eq!(datetime.nanosecond(), 0); assert_eq!(datetime.timezone(), TimeZone::Utc); } #[test] fn test_zip_datetime_ntfs() { // NTFS timestamp for roughly 2010-09-05 02:12:01 UTC with 500ms precision let datetime = utc_from_components(2010, 9, 5, 2, 12, 1, 500000000); assert_eq!(datetime.year(), 2010); assert_eq!(datetime.month(), 9); assert_eq!(datetime.day(), 5); assert_eq!(datetime.hour(), 2); assert_eq!(datetime.minute(), 12); assert_eq!(datetime.second(), 1); assert_eq!(datetime.nanosecond(), 500000000); assert_eq!(datetime.timezone(), TimeZone::Utc); } #[test] fn test_to_unix_comprehensive() { // Test comprehensive cases including edge cases and leap years // Test first day of each month in a leap year (2020) let jan_1_2020 = utc_from_components(2020, 1, 1, 0, 0, 0, 0); assert_eq!(jan_1_2020.to_unix(), 1577836800); let feb_29_2020 = utc_from_components(2020, 2, 29, 0, 0, 0, 0); assert_eq!(feb_29_2020.to_unix(), 1582934400); let mar_1_2020 = utc_from_components(2020, 3, 1, 0, 0, 0, 0); assert_eq!(mar_1_2020.to_unix(), 1583020800); // Test non-leap year (2021) let feb_28_2021 = utc_from_components(2021, 2, 28, 0, 0, 0, 0); assert_eq!(feb_28_2021.to_unix(), 1614470400); // Test century boundary (non-leap year despite being divisible by 4) let mar_1_1900 = utc_from_components(1900, 3, 1, 0, 0, 0, 0); // This is before Unix epoch, so returns negative value let result = mar_1_1900.to_unix(); assert!(result < 0); // Dates before epoch return negative values // Test year 2038 boundary (close to u32::MAX seconds) let early_2038 = utc_from_components(2038, 1, 1, 0, 0, 0, 0); let timestamp_2038 = early_2038.to_unix(); assert!(timestamp_2038 > 0); // Should have a valid positive timestamp // Test far future dates (beyond u32 range but handled by i64) let far_future = utc_from_components(2200, 1, 1, 0, 0, 0, 0); let result = far_future.to_unix(); // Should return a valid i64 timestamp for far future dates assert!(result > u32::MAX as i64); // Should exceed u32 range } #[test] fn test_to_unix_accuracy() { // Test known dates against their Unix timestamps (verified with Python datetime) // Unix epoch: 1970-01-01 00:00:00 UTC = 0 let epoch = utc_from_components(1970, 1, 1, 0, 0, 0, 0); assert_eq!(epoch.to_unix(), 0); // 2000-01-01 00:00:00 UTC = 946684800 let y2k = utc_from_components(2000, 1, 1, 0, 0, 0, 0); assert_eq!(y2k.to_unix(), 946684800); // 2023-06-15 14:30:45 UTC = 1686839445 let test_date = utc_from_components(2023, 6, 15, 14, 30, 45, 0); assert_eq!(test_date.to_unix(), 1686839445); // Leap year test: 2020-02-29 12:00:00 UTC = 1582977600 let leap_day = utc_from_components(2020, 2, 29, 12, 0, 0, 0); assert_eq!(leap_day.to_unix(), 1582977600); // Test dates before Unix epoch return negative values let before_epoch = utc_from_components(1969, 12, 31, 23, 59, 59, 0); let result = before_epoch.to_unix(); // One second before epoch should be -1 assert_eq!(result, -1); } #[test] fn test_negative_unix_timestamps() { // Test that negative timestamps (before 1970) work correctly let negative_timestamp = -86400; // One day before epoch (1969-12-31) let datetime = UtcDateTime::from_unix(negative_timestamp); assert_eq!(datetime.year(), 1969); assert_eq!(datetime.month(), 12); assert_eq!(datetime.day(), 31); assert_eq!(datetime.hour(), 0); assert_eq!(datetime.minute(), 0); assert_eq!(datetime.second(), 0); // Round trip test assert_eq!(datetime.to_unix(), negative_timestamp); } #[test] fn test_days_from_civil() { // Test Unix epoch let epoch = utc_from_components(1970, 1, 1, 0, 0, 0, 0); assert_eq!(epoch.days_from_civil(), 0); // Test Y2K (verified with Python) let y2k = utc_from_components(2000, 1, 1, 0, 0, 0, 0); assert_eq!(y2k.days_from_civil(), 10957); // Test leap year boundary (verified with Python) let leap_day = utc_from_components(2020, 2, 29, 0, 0, 0, 0); assert_eq!(leap_day.days_from_civil(), 18321); // Test before epoch (negative value) let before_epoch = utc_from_components(1969, 12, 31, 0, 0, 0, 0); assert_eq!(before_epoch.days_from_civil(), -1); } #[test] fn test_zip_datetime_display() { // Test with zero nanoseconds - should omit the nanosecond part let datetime_no_nanos = utc_from_components(2023, 6, 15, 14, 30, 42, 0); assert_eq!(format!("{}", datetime_no_nanos), "2023-06-15T14:30:42Z"); // Test with non-zero nanoseconds - should include the nanosecond part let datetime_with_nanos = utc_from_components(2023, 6, 15, 14, 30, 42, 500000000); assert_eq!( format!("{}", datetime_with_nanos), "2023-06-15T14:30:42.500000000Z" ); // Test local time with zero nanoseconds let datetime_local = local_from_components(2023, 6, 15, 14, 30, 42, 0); assert_eq!(format!("{}", datetime_local), "2023-06-15T14:30:42"); // Test local time with nanoseconds let datetime_local_nanos = local_from_components(2023, 6, 15, 14, 30, 42, 123456789); assert_eq!( format!("{}", datetime_local_nanos), "2023-06-15T14:30:42.123456789" ); } #[test] fn test_parse_extended_timestamp() { // Extended timestamp with modification time flag and Unix timestamp let mut data = vec![0x01]; // Flags: modification time present data.extend_from_slice(&1283652721u32.to_le_bytes()); // Unix timestamp let result = parse_extended_timestamp(&data).unwrap(); // Check that it's a Unix timestamp with the right components assert_eq!(result.year(), 2010); assert_eq!(result.month(), 9); assert_eq!(result.day(), 5); assert_eq!(result.hour(), 2); assert_eq!(result.minute(), 12); assert_eq!(result.second(), 1); assert_eq!(result.timezone(), TimeZone::Utc); } #[test] fn test_parse_unix_timestamp() { // Unix timestamp format: access time (4 bytes) + modification time (4 bytes) let mut data = vec![]; data.extend_from_slice(&0u32.to_le_bytes()); // Access time (ignored) data.extend_from_slice(&1283652721u32.to_le_bytes()); // Modification time let result = parse_unix_timestamp(&data).unwrap(); // Check that it's a Unix timestamp with the right components assert_eq!(result.year(), 2010); assert_eq!(result.month(), 9); assert_eq!(result.day(), 5); assert_eq!(result.hour(), 2); assert_eq!(result.minute(), 12); assert_eq!(result.second(), 1); assert_eq!(result.timezone(), TimeZone::Utc); } #[test] fn test_parse_ntfs_timestamp() { // NTFS timestamp format let mut data = vec![0; 4]; // Reserved data.extend_from_slice(&0x0001u16.to_le_bytes()); // Tag data.extend_from_slice(&24u16.to_le_bytes()); // Size // NTFS timestamp (100-nanosecond ticks since 1601-01-01) let ticks = (1283652721 + NTFS_EPOCH_OFFSET) * 10_000_000; data.extend_from_slice(&ticks.to_le_bytes()); // Modification time data.extend_from_slice(&0u64.to_le_bytes()); // Access time data.extend_from_slice(&0u64.to_le_bytes()); // Creation time let result = parse_ntfs_timestamp(&data).unwrap(); // Check that it's an NTFS timestamp with the right components assert_eq!(result.year(), 2010); assert_eq!(result.month(), 9); assert_eq!(result.day(), 5); assert_eq!(result.hour(), 2); assert_eq!(result.minute(), 12); assert_eq!(result.second(), 1); assert_eq!(result.timezone(), TimeZone::Utc); } #[test] fn test_zip_datetime_ordering() { let dt1 = UtcDateTime::from_components(2020, 1, 1, 0, 0, 0, 0).unwrap(); let dt2 = UtcDateTime::from_components(2020, 1, 1, 0, 0, 0, 500_000_000).unwrap(); // Same time, more nanoseconds let dt3 = UtcDateTime::from_components(2020, 1, 1, 0, 0, 1, 0).unwrap(); // One second later let dt4 = UtcDateTime::from_components(2020, 1, 1, 0, 1, 0, 0).unwrap(); // One minute later let dt5 = UtcDateTime::from_components(2020, 1, 1, 1, 0, 0, 0).unwrap(); // One hour later let dt6 = UtcDateTime::from_components(2020, 1, 2, 0, 0, 0, 0).unwrap(); // One day later let dt7 = UtcDateTime::from_components(2020, 2, 1, 0, 0, 0, 0).unwrap(); // One month later let dt8 = UtcDateTime::from_components(2021, 1, 1, 0, 0, 0, 0).unwrap(); // One year later let mut timestamps = vec![dt8, dt3, dt1, dt6, dt4, dt2, dt7, dt5]; timestamps.sort_unstable(); let expected = vec![dt1, dt2, dt3, dt4, dt5, dt6, dt7, dt8]; assert_eq!( timestamps, expected, "sorting should produce chronological order" ); } } #[cfg(test)] mod property_tests { //! Property-based tests to verify timestamp conversion accuracy against jiff. use super::*; use quickcheck_macros::quickcheck; #[quickcheck] fn prop_unix_timestamp_conversion(unix_seconds: u32) { let zip_datetime = UtcDateTime::from_unix(i64::from(unix_seconds)); let Ok(timestamp) = jiff::Timestamp::from_second(unix_seconds as i64) else { return; }; let dt = timestamp.to_zoned(jiff::tz::TimeZone::UTC); assert_eq!(zip_datetime.year(), dt.year() as u16, "year"); assert_eq!(zip_datetime.month(), dt.month() as u8, "month"); assert_eq!(zip_datetime.day(), dt.day() as u8, "day"); assert_eq!(zip_datetime.hour(), dt.hour() as u8, "hour"); assert_eq!(zip_datetime.minute(), dt.minute() as u8, "minute"); assert_eq!(zip_datetime.second(), dt.second() as u8, "second"); assert_eq!(zip_datetime.timezone(), TimeZone::Utc); assert_eq!(zip_datetime.nanosecond(), 0, "nanosecond"); assert_eq!( zip_datetime.to_unix(), i64::from(unix_seconds), "to_unix should match input" ); } /// Property test: NTFS timestamp conversion should match jiff's conversion #[quickcheck] fn prop_ntfs_timestamp_conversion(ntfs_ticks: u64) { let zip_datetime = UtcDateTime::from_ntfs(ntfs_ticks); // Convert NTFS ticks to Unix timestamp for jiff // NTFS ticks are 100-nanosecond intervals since 1601-01-01 let unix_seconds = (ntfs_ticks / 10_000_000).saturating_sub(NTFS_EPOCH_OFFSET); let nanoseconds = ((ntfs_ticks % 10_000_000) * 100) as u32; if unix_seconds > u32::MAX as u64 { return; } let Ok(jiff_timestamp) = jiff::Timestamp::new(unix_seconds as i64, nanoseconds as i32) else { return; }; let dt = jiff_timestamp.to_zoned(jiff::tz::TimeZone::UTC); assert_eq!(zip_datetime.year(), dt.year() as u16, "year"); assert_eq!(zip_datetime.month(), dt.month() as u8, "month"); assert_eq!(zip_datetime.day(), dt.day() as u8, "day"); assert_eq!(zip_datetime.hour(), dt.hour() as u8, "hour"); assert_eq!(zip_datetime.minute(), dt.minute() as u8, "minute"); assert_eq!(zip_datetime.second(), dt.second() as u8, "second"); assert_eq!(zip_datetime.timezone(), TimeZone::Utc); assert_eq!(zip_datetime.nanosecond(), nanoseconds, "nanosecond"); } /// Property test: DOS timestamp conversion should always produce valid jiff datetimes #[quickcheck] fn prop_dos_timestamp_always_valid(dos_time: u16, dos_date: u16) { let dos_datetime = DosDateTime::new(dos_time, dos_date); let zip_datetime = LocalDateTime::from_dos(dos_datetime); // Create jiff datetime - this should never fail with our normalization let dt = jiff::civil::DateTime::new( zip_datetime.year() as i16, zip_datetime.month() as i8, zip_datetime.day() as i8, zip_datetime.hour() as i8, zip_datetime.minute() as i8, zip_datetime.second() as i8, 0, // nanosecond ) .unwrap(); // Verify the components match what we expect assert_eq!(zip_datetime.year(), dt.year() as u16, "year"); assert_eq!(zip_datetime.month(), dt.month() as u8, "month"); assert_eq!(zip_datetime.day(), dt.day() as u8, "day"); assert_eq!(zip_datetime.hour(), dt.hour() as u8, "hour"); assert_eq!(zip_datetime.minute(), dt.minute() as u8, "minute"); assert_eq!(zip_datetime.second(), dt.second() as u8, "second"); } } rawzip-0.4.4/src/utils.rs000064400000000000000000000005251046102023000134440ustar 00000000000000#[inline(always)] pub(crate) fn le_u64(d: &[u8]) -> u64 { u64::from_le_bytes([d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]]) } #[inline(always)] pub(crate) fn le_u32(d: &[u8]) -> u32 { u32::from_le_bytes([d[0], d[1], d[2], d[3]]) } #[inline(always)] pub(crate) fn le_u16(d: &[u8]) -> u16 { u16::from_le_bytes([d[0], d[1]]) } rawzip-0.4.4/src/writer.rs000064400000000000000000001440431046102023000136240ustar 00000000000000use crate::{ crc, errors::ErrorKind, extra_fields::{ExtraFieldId, ExtraFieldsContainer}, mode::CREATOR_UNIX, path::{NormalizedPath, ZipFilePath}, time::{DosDateTime, UtcDateTime}, CompressionMethod, DataDescriptor, Error, Header, ZipFileHeaderFixed, ZipLocalFileHeaderFixed, CENTRAL_HEADER_SIGNATURE, END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE, END_OF_CENTRAL_DIR_SIGNATURE64, END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES, }; use std::io::{self, Write}; // ZIP64 constants const ZIP64_VERSION_NEEDED: u16 = 45; // 4.5 const ZIP64_EOCD_SIZE: usize = 56; // General purpose bit flags const FLAG_DATA_DESCRIPTOR: u16 = 0x08; // bit 3: data descriptor present const FLAG_UTF8_ENCODING: u16 = 0x800; // bit 11: UTF-8 encoding flag (EFS) // ZIP64 thresholds - when to switch to ZIP64 format const ZIP64_THRESHOLD_FILE_SIZE: u64 = u32::MAX as u64; const ZIP64_THRESHOLD_OFFSET: u64 = u32::MAX as u64; const ZIP64_THRESHOLD_ENTRIES: usize = u16::MAX as usize; #[derive(Debug)] struct CountWriter { writer: W, count: u64, } impl CountWriter { fn new(writer: W, count: u64) -> Self { CountWriter { writer, count } } fn count(&self) -> u64 { self.count } } impl Write for CountWriter { fn write(&mut self, buf: &[u8]) -> io::Result { let bytes_written = self.writer.write(buf)?; self.count += bytes_written as u64; Ok(bytes_written) } fn flush(&mut self) -> io::Result<()> { self.writer.flush() } } /// Builds a `ZipArchiveWriter`. #[derive(Debug, Default)] pub struct ZipArchiveWriterBuilder { count: u64, capacity: usize, } impl ZipArchiveWriterBuilder { /// Creates a new `ZipArchiveWriterBuilder`. pub fn new() -> Self { Self::default() } /// Sets the anticipated number of files to optimize memory allocation. pub fn with_capacity(mut self, capacity: usize) -> Self { self.capacity = capacity; self } /// Sets the starting offset for writing. Useful when there is prelude data /// prior to the zip archive. /// /// When there is prelude data, setting the offset may not technically be /// required, but it is recommended. For standard zip files, many zip /// readers can self correct when the prelude data isn't properly declared. /// However for zip64 archives, setting the correct offset is required. /// /// # Example: Appending ZIP to existing data /// ```rust /// use std::io::{Cursor, Write, Seek, SeekFrom}; /// /// // Create a file with some prefix data /// let mut output = Cursor::new(Vec::new()); /// output.write_all(b"This is a custom header or prefix data\n").unwrap(); /// let zip_start_offset = output.position(); /// /// // Create ZIP archive starting after the prefix data /// let mut archive = rawzip::ZipArchiveWriter::builder() /// .with_offset(zip_start_offset) // Tell the archive where it starts /// .build(&mut output); /// /// // Add files normally /// let mut file = archive.new_file("data.txt").create().unwrap(); /// let mut writer = rawzip::ZipDataWriter::new(&mut file); /// writer.write_all(b"File content").unwrap(); /// let (_, desc) = writer.finish().unwrap(); /// file.finish(desc).unwrap(); /// archive.finish().unwrap(); /// /// // The resulting file contains both prefix data and the ZIP archive /// let final_data = output.into_inner(); /// assert!(final_data.starts_with(b"This is a custom header")); /// ``` pub fn with_offset(mut self, offset: u64) -> Self { self.count = offset; self } /// Builds a `ZipArchiveWriter` that writes to `writer`. pub fn build(&self, writer: W) -> ZipArchiveWriter { ZipArchiveWriter { writer: CountWriter::new(writer, self.count), files: Vec::with_capacity(self.capacity), file_names: Vec::new(), } } } /// Create a new Zip archive. /// /// Basic usage: /// ```rust /// use std::io::Write; /// /// let mut output = std::io::Cursor::new(Vec::new()); /// let mut archive = rawzip::ZipArchiveWriter::new(&mut output); /// let (mut entry, config) = archive.new_file("file.txt").start().unwrap(); /// let mut writer = config.wrap(&mut entry); /// writer.write_all(b"Hello, world!").unwrap(); /// let (_, output) = writer.finish().unwrap(); /// entry.finish(output).unwrap(); /// archive.finish().unwrap(); /// ``` /// /// Use the builder for customization: /// ```rust /// use std::io::Write; /// /// let mut output = std::io::Cursor::new(Vec::::new()); /// let mut _archive = rawzip::ZipArchiveWriter::builder() /// .with_capacity(1000) // Optimize for 1000 anticipated files /// .build(&mut output); /// // ... add files as usual /// ``` #[derive(Debug)] pub struct ZipArchiveWriter { files: Vec, file_names: Vec, writer: CountWriter, } impl ZipArchiveWriter<()> { /// Creates a `ZipArchiveWriterBuilder` for configuring the writer. pub fn builder() -> ZipArchiveWriterBuilder { ZipArchiveWriterBuilder::new() } } impl ZipArchiveWriter { /// Creates a new `ZipArchiveWriter` that writes to `writer`. pub fn new(writer: W) -> Self { ZipArchiveWriterBuilder::new().build(writer) } /// Returns the current offset in the output stream. /// /// Analagous to [`std::io::Cursor::position`]. /// /// This can be used to determine various offsets during ZIP archive /// creation: /// /// - Local header offset /// - Start of compressed data offset /// - End of compressed data offset /// - End of data descriptor offset / next file's local header offset /// /// # Example /// /// ```rust /// use std::io::Write; /// /// let mut output = std::io::Cursor::new(Vec::new()); /// let mut archive = rawzip::ZipArchiveWriter::new(&mut output); /// /// // 1. Get local header offset /// let local_header_offset = archive.stream_offset(); /// let mut file = archive.new_file("test.txt").create().unwrap(); /// /// // 2. Get start of data offset /// let data_start_offset = file.stream_offset(); /// /// // Write some data /// let mut writer = rawzip::ZipDataWriter::new(&mut file); /// writer.write_all(b"Hello World").unwrap(); /// let (_, desc) = writer.finish().unwrap(); /// /// // 3. Get end of compressed data offset /// let end_data_offset = file.stream_offset(); /// /// let compressed_bytes = file.finish(desc).unwrap(); /// /// // 4. Get end of data descriptor offset (next file's local header offset) /// let end_descriptor_offset = archive.stream_offset(); /// /// archive.finish().unwrap(); /// /// assert_eq!(local_header_offset, 0); /// assert!(data_start_offset > local_header_offset); /// assert_eq!(end_data_offset, data_start_offset + b"Hello World".len() as u64); /// assert_eq!(end_descriptor_offset, end_data_offset + 16); // 16 bytes for data descriptor /// assert_eq!(compressed_bytes, end_data_offset - data_start_offset); /// ``` pub fn stream_offset(&self) -> u64 { self.writer.count() } } /// Options for CRC32 calculation in ZIP files. #[derive(Debug, Clone, Copy, Default)] pub enum Crc32Option { /// Calculate CRC32 automatically from the data. #[default] Calculate, /// Use a custom CRC32 value and skip calculation. Custom(u32), /// Skip CRC32 calculation entirely (sets CRC32 to 0). Skip, } impl Crc32Option { /// Returns the initial CRC32 value for this option. #[inline] pub fn initial_value(&self) -> u32 { match self { Crc32Option::Calculate => 0, Crc32Option::Custom(value) => *value, Crc32Option::Skip => 0, } } } /// A builder for creating a new file entry in a ZIP archive. #[derive(Debug)] pub struct ZipFileBuilder<'archive, 'name, W> { archive: &'archive mut ZipArchiveWriter, name: &'name str, compression_method: CompressionMethod, modification_time: Option, unix_permissions: Option, extra_fields: ExtraFieldsContainer, crc32_option: Crc32Option, } impl<'archive, W> ZipFileBuilder<'archive, '_, W> where W: Write, { /// Sets the compression method for the file entry. #[must_use] #[inline] pub fn compression_method(mut self, compression_method: CompressionMethod) -> Self { self.compression_method = compression_method; self } /// Sets the modification time for the file entry. /// /// Only accepts UTC timestamps to ensure Extended Timestamp fields are written correctly. #[must_use] #[inline] pub fn last_modified(mut self, modification_time: UtcDateTime) -> Self { self.modification_time = Some(modification_time); self } /// Sets the Unix permissions for the file entry. /// /// Accepts either: /// - Basic permission bits (e.g., 0o644 for rw-r--r--, 0o755 for rwxr-xr-x) /// - Full Unix mode including file type (e.g., 0o100644 for regular file, 0o040755 for directory) /// - Special permission bits are preserved (SUID: 0o4000, SGID: 0o2000, sticky: 0o1000) /// /// When set, the archive will be created with Unix-compatible "version made by" field /// to ensure proper interpretation of the permissions by zip readers. #[must_use] #[inline] pub fn unix_permissions(mut self, permissions: u32) -> Self { self.unix_permissions = Some(permissions); self } /// Adds an extra field to this file entry. /// /// Extra fields contain additional metadata about files in ZIP archives, /// such as timestamps, alignment information, and platform-specific data. /// /// No deduplication is performed - duplicate field IDs will result in /// multiple entries /// /// Will return an error if the total size exceeds 65,535 bytes for the /// specified headers. /// /// Rawzip will automatically add extra fields: /// /// - `EXTENDED_TIMESTAMP` when `last_modified()` is set /// - `ZIP64` when 32-bit thresholds are met /// /// # Examples /// /// Create files with different extra field headers and verify the /// behavior. Only the central directory is checked. To check the local /// extra fields, see /// [`ZipEntry::local_header`](crate::ZipEntry::local_header) /// /// ```rust /// # use std::io::{Cursor, Write}; /// # use rawzip::{ZipArchive, ZipArchiveWriter, ZipDataWriter, extra_fields::ExtraFieldId, Header}; /// let mut output = Cursor::new(Vec::new()); /// let mut archive = ZipArchiveWriter::new(&mut output); /// /// let my_custom_field = ExtraFieldId::new(0x6666); /// /// // File with extra fields only in the local file header /// let mut local_file = archive.new_file("video.mp4") /// .extra_field(my_custom_field, b"field1", Header::LOCAL)? /// .create()?; /// let mut writer = ZipDataWriter::new(&mut local_file); /// writer.write_all(b"video data")?; /// let (_, desc) = writer.finish()?; /// local_file.finish(desc)?; /// /// // File with extra fields only in the central directory /// let mut central_file = archive.new_file("document.pdf") /// .extra_field(my_custom_field, b"field2", Header::CENTRAL)? /// .create()?; /// let mut writer = ZipDataWriter::new(&mut central_file); /// writer.write_all(b"PDF content")?; /// let (_, desc) = writer.finish()?; /// central_file.finish(desc)?; /// /// // File with extra fields in both headers for maximum compatibility /// assert_eq!(Header::default(), Header::LOCAL | Header::CENTRAL); /// let mut both_file = archive.new_file("important.dat") /// .extra_field(my_custom_field, b"field3", Header::default())? /// .create()?; /// let mut writer = ZipDataWriter::new(&mut both_file); /// writer.write_all(b"important data")?; /// let (_, desc) = writer.finish()?; /// both_file.finish(desc)?; /// /// archive.finish()?; /// /// // Verify the behavior when reading back the central directory /// let zip_data = output.into_inner(); /// let archive = ZipArchive::from_slice(&zip_data)?; /// /// for entry_result in archive.entries() { /// let entry = entry_result?; /// /// // Find our custom field in the central directory /// let custom_field_data = entry.extra_fields() /// .find(|(id, _)| *id == my_custom_field) /// .map(|(_, data)| data); /// /// match entry.file_path().as_ref() { /// b"video.mp4" => { /// // local only field should not be in central directory /// assert_eq!(custom_field_data, None); /// } /// b"document.pdf" => { /// // central only field should be in central directory /// assert_eq!(custom_field_data, Some(b"field2".as_slice())); /// } /// b"important.dat" => { /// // both location field should be in central directory /// assert_eq!(custom_field_data, Some(b"field3".as_slice())); /// } /// _ => {} /// } /// } /// # Ok::<(), Box>(()) /// ``` pub fn extra_field( mut self, id: ExtraFieldId, data: &[u8], location: Header, ) -> Result { self.extra_fields.add_field(id, data, location)?; Ok(self) } /// Sets the CRC32 calculation option for the file entry. /// /// By default, CRC32 is calculated automatically from the data. Use this /// method to: /// /// - Skip CRC32 calculation entirely (for performance or when verification /// isn't desired) /// - Provide a pre-calculated CRC32 value #[must_use] #[inline] pub fn crc32(mut self, crc32_option: Crc32Option) -> Self { self.crc32_option = crc32_option; self } /// Creates the file entry and returns a writer for the file's content. #[deprecated( since = "0.4.0", note = "Use `start()` method instead as it allows for more flexibility (ie: CRC configuration)" )] pub fn create(self) -> Result, Error> { let (entry_writer, _) = self.start()?; Ok(entry_writer) } /// Mark the start of file data /// /// Returns a tuple: /// /// - `entry` handles the ZIP format and writes compressed data to the archive /// - `config` constructs data writers that handle uncompressed data and CRC32 calculation /// /// # Examples /// /// For stored (uncompressed) files: /// ``` /// # use std::io::Write; /// # let mut output = std::io::Cursor::new(Vec::new()); /// # let mut archive = rawzip::ZipArchiveWriter::new(&mut output); /// let (mut entry, config) = archive.new_file("file.txt").start().unwrap(); /// let mut writer = config.wrap(&mut entry); /// writer.write_all(b"Hello").unwrap(); /// let (_, output) = writer.finish().unwrap(); /// entry.finish(output).unwrap(); /// # archive.finish().unwrap(); /// ``` /// /// For deflate compression: /// ``` /// # use std::io::Write; /// # let mut output = std::io::Cursor::new(Vec::new()); /// # let mut archive = rawzip::ZipArchiveWriter::new(&mut output); /// let (mut entry, config) = archive.new_file("file.txt").start().unwrap(); /// let encoder = flate2::write::DeflateEncoder::new(&mut entry, flate2::Compression::default()); /// let mut writer = config.wrap(encoder); /// writer.write_all(b"Hello").unwrap(); /// let (encoder, output) = writer.finish().unwrap(); /// encoder.finish().unwrap(); /// entry.finish(output).unwrap(); /// # archive.finish().unwrap(); /// ``` pub fn start(self) -> Result<(ZipEntryWriter<'archive, W>, ZipDataWriterConfig), Error> { let crc32_option = self.crc32_option; let options = ZipEntryOptions { compression_method: self.compression_method, modification_time: self.modification_time, unix_permissions: self.unix_permissions, extra_fields: self.extra_fields, }; let entry_writer = self.archive.new_file_with_options(self.name, options)?; let data_writer_config = ZipDataWriterConfig { crc32_option }; Ok((entry_writer, data_writer_config)) } } /// A builder for creating a new directory entry in a ZIP archive. #[derive(Debug)] pub struct ZipDirBuilder<'a, W> { archive: &'a mut ZipArchiveWriter, name: &'a str, modification_time: Option, unix_permissions: Option, extra_fields: ExtraFieldsContainer, } impl ZipDirBuilder<'_, W> where W: Write, { /// Sets the modification time for the directory entry. /// /// See [`ZipFileBuilder::last_modified`] for details. #[must_use] #[inline] pub fn last_modified(mut self, modification_time: UtcDateTime) -> Self { self.modification_time = Some(modification_time); self } /// Sets the Unix permissions for the directory entry. /// /// See [`ZipFileBuilder::unix_permissions`] for details. #[must_use] #[inline] pub fn unix_permissions(mut self, permissions: u32) -> Self { self.unix_permissions = Some(permissions); self } /// Adds an extra field to this directory entry. /// /// See [`ZipFileBuilder::extra_field`] for details and examples. /// The same behavior notes apply: append-only, no deduplication, and automatic fields. pub fn extra_field( mut self, id: ExtraFieldId, data: &[u8], location: Header, ) -> Result { self.extra_fields.add_field(id, data, location)?; Ok(self) } /// Creates the directory entry. pub fn create(self) -> Result<(), Error> { let options = ZipEntryOptions { compression_method: CompressionMethod::Store, // Directories always use Store modification_time: self.modification_time, unix_permissions: self.unix_permissions, extra_fields: self.extra_fields, }; self.archive.new_dir_with_options(self.name, options) } } impl ZipArchiveWriter where W: Write, { /// Writes a local file header with filtered extra fields. fn write_local_header( &mut self, file_path: &ZipFilePath, flags: u16, compression_method: CompressionMethod, options: &mut ZipEntryOptions, ) -> Result<(), Error> { // Get DOS timestamp from options or use 0 as default let (dos_time, dos_date) = options .modification_time .as_ref() .map(|dt| DosDateTime::from(dt).into_parts()) .unwrap_or((0, 0)); if let Some(datetime) = options.modification_time.as_ref() { let unix_time = datetime.to_unix().max(0) as u32; let mut data = [0u8; 5]; data[0] = 1; // Flags: modification time present data[1..].copy_from_slice(&unix_time.to_le_bytes()); options.extra_fields.add_field( ExtraFieldId::EXTENDED_TIMESTAMP, &data, Header::CENTRAL, )?; } let header = ZipLocalFileHeaderFixed { signature: ZipLocalFileHeaderFixed::SIGNATURE, version_needed: 20, flags, compression_method: compression_method.as_id(), last_mod_time: dos_time, last_mod_date: dos_date, crc32: 0, // must be zero if data descriptor is used (4.4.4) compressed_size: 0, uncompressed_size: 0, file_name_len: file_path.len() as u16, extra_field_len: options.extra_fields.local_size, }; header.write(&mut self.writer)?; self.writer.write_all(file_path.as_ref().as_bytes())?; options .extra_fields .write_extra_fields(&mut self.writer, Header::LOCAL)?; Ok(()) } /// Creates a builder for adding a new directory to the archive. /// /// The name of the directory must end with a `/`. /// /// # Example /// /// ```rust /// # use std::io::Cursor; /// # let mut output = Cursor::new(Vec::new()); /// # let mut archive = rawzip::ZipArchiveWriter::new(&mut output); /// archive.new_dir("my-dir/") /// .unix_permissions(0o755) /// .create()?; /// # Ok::<(), Box>(()) /// ``` #[must_use] pub fn new_dir<'a>(&'a mut self, name: &'a str) -> ZipDirBuilder<'a, W> { ZipDirBuilder { archive: self, name, modification_time: None, unix_permissions: None, extra_fields: ExtraFieldsContainer::new(), } } /// Adds a new directory to the archive with options (internal method). /// /// The name of the directory must end with a `/`. fn new_dir_with_options( &mut self, name: &str, mut options: ZipEntryOptions, ) -> Result<(), Error> { let file_path = ZipFilePath::from_str(name); if !file_path.is_dir() { return Err(Error::from(ErrorKind::InvalidInput { msg: "not a directory".to_string(), })); } if file_path.len() > u16::MAX as usize { return Err(Error::from(ErrorKind::InvalidInput { msg: "directory name too long".to_string(), })); } let local_header_offset = self.writer.count(); let mut flags = 0u16; if file_path.needs_utf8_encoding() { flags |= FLAG_UTF8_ENCODING; } else { flags &= !FLAG_UTF8_ENCODING; } // Store the name bytes in the central buffer let name_bytes = file_path.as_ref().as_bytes(); let name_len = name_bytes.len() as u16; self.file_names.extend_from_slice(name_bytes); self.write_local_header(&file_path, flags, CompressionMethod::Store, &mut options)?; let file_header = FileHeader { name_len, compression_method: CompressionMethod::Store, local_header_offset, compressed_size: 0, uncompressed_size: 0, crc: 0, flags, modification_time: options.modification_time, unix_permissions: options.unix_permissions, extra_fields: options.extra_fields, }; self.files.push(file_header); Ok(()) } /// Creates a builder for adding a new file to the archive. /// /// # Example /// /// ```rust /// # use std::io::{Cursor, Write}; /// # let mut output = Cursor::new(Vec::new()); /// # let mut archive = rawzip::ZipArchiveWriter::new(&mut output); /// let (mut entry, config) = archive.new_file("my-file") /// .compression_method(rawzip::CompressionMethod::Deflate) /// .unix_permissions(0o644) /// .start()?; /// let mut writer = config.wrap(&mut entry); /// writer.write_all(b"Hello, world!")?; /// let (_, output) = writer.finish()?; /// entry.finish(output)?; /// # Ok::<(), Box>(()) /// ``` #[must_use] pub fn new_file<'name>(&mut self, name: &'name str) -> ZipFileBuilder<'_, 'name, W> { ZipFileBuilder { archive: self, name, compression_method: CompressionMethod::Store, modification_time: None, unix_permissions: None, extra_fields: ExtraFieldsContainer::new(), crc32_option: Crc32Option::default(), } } /// Adds a new file to the archive with options (internal method). fn new_file_with_options( &mut self, name: &str, mut options: ZipEntryOptions, ) -> Result, Error> { let file_path = ZipFilePath::from_str(name.trim_end_matches('/')); if file_path.len() > u16::MAX as usize { return Err(Error::from(ErrorKind::InvalidInput { msg: "file name too long".to_string(), })); } let local_header_offset = self.writer.count(); let mut flags = FLAG_DATA_DESCRIPTOR; if file_path.needs_utf8_encoding() { flags |= FLAG_UTF8_ENCODING; } else { flags &= !FLAG_UTF8_ENCODING; } // Store the name bytes in the central buffer let name_bytes = file_path.as_ref().as_bytes(); let name_len = name_bytes.len() as u16; self.file_names.extend_from_slice(name_bytes); self.write_local_header(&file_path, flags, options.compression_method, &mut options)?; Ok(ZipEntryWriter { inner: self, compressed_bytes: 0, name_len, local_header_offset, compression_method: options.compression_method, flags, modification_time: options.modification_time, unix_permissions: options.unix_permissions, extra_fields: options.extra_fields, }) } /// Finishes writing the archive and returns the underlying writer. /// /// This writes the central directory and the end of central directory /// record. ZIP64 format is used automatically when thresholds are exceeded. pub fn finish(mut self) -> Result where W: Write, { let central_directory_offset = self.writer.count(); let total_entries = self.files.len(); // Determine if we need ZIP64 format let needs_zip64 = total_entries >= ZIP64_THRESHOLD_ENTRIES || central_directory_offset >= ZIP64_THRESHOLD_OFFSET || self.files.iter().any(|f| f.needs_zip64()); let mut name_offset = 0; // Write central directory entries for file in &self.files { // Version made by and version needed to extract let version_needed = if file.needs_zip64() { ZIP64_VERSION_NEEDED } else { 20 }; // Set version_made_by to indicate Unix when Unix permissions are present let version_made_by_hi = file.unix_permissions.map(|_| CREATOR_UNIX).unwrap_or(0); let version_made_by = (version_made_by_hi << 8) | version_needed; let (dos_time, dos_date) = file .modification_time .as_ref() .map(|dt| DosDateTime::from(dt).into_parts()) .unwrap_or((0, 0)); let header = ZipFileHeaderFixed { signature: CENTRAL_HEADER_SIGNATURE, version_made_by, version_needed, flags: file.flags, compression_method: file.compression_method.as_id(), last_mod_time: dos_time, last_mod_date: dos_date, crc32: file.crc, compressed_size: file.compressed_size.min(ZIP64_THRESHOLD_FILE_SIZE) as u32, uncompressed_size: file.uncompressed_size.min(ZIP64_THRESHOLD_FILE_SIZE) as u32, file_name_len: file.name_len, extra_field_len: file.extra_fields.central_size, file_comment_len: 0, disk_number_start: 0, internal_file_attrs: 0, external_file_attrs: file.unix_permissions.map(|x| x << 16).unwrap_or(0), local_header_offset: file.local_header_offset.min(ZIP64_THRESHOLD_OFFSET) as u32, }; header.write(&mut self.writer)?; // File name let new_name_offset = name_offset + file.name_len as usize; self.writer .write_all(&self.file_names[name_offset..new_name_offset])?; name_offset = new_name_offset; // Extra fields file.extra_fields .write_extra_fields(&mut self.writer, Header::CENTRAL)?; } let central_directory_end = self.writer.count(); let central_directory_size = central_directory_end - central_directory_offset; // Write ZIP64 structures if needed if needs_zip64 { let zip64_eocd_offset = self.writer.count(); // Write ZIP64 End of Central Directory Record write_zip64_eocd( &mut self.writer, total_entries as u64, central_directory_size, central_directory_offset, )?; // Write ZIP64 End of Central Directory Locator write_zip64_eocd_locator(&mut self.writer, zip64_eocd_offset)?; } // Write regular End of Central Directory Record self.writer.write_all(&END_OF_CENTRAL_DIR_SIGNAUTRE_BYTES)?; // Disk numbers self.writer.write_all(&[0u8; 4])?; // Number of entries - use 0xFFFF if ZIP64 let entries_count = total_entries.min(ZIP64_THRESHOLD_ENTRIES) as u16; self.writer.write_all(&entries_count.to_le_bytes())?; self.writer.write_all(&entries_count.to_le_bytes())?; // Central directory size - use 0xFFFFFFFF if ZIP64 let cd_size = central_directory_size.min(ZIP64_THRESHOLD_OFFSET) as u32; self.writer.write_all(&cd_size.to_le_bytes())?; // Central directory offset - use 0xFFFFFFFF if ZIP64 let cd_offset = central_directory_offset.min(ZIP64_THRESHOLD_OFFSET) as u32; self.writer.write_all(&cd_offset.to_le_bytes())?; // Comment length self.writer.write_all(&0u16.to_le_bytes())?; self.writer.flush()?; Ok(self.writer.writer) } } /// A writer for a file in a ZIP archive. /// /// This writer is created by `ZipArchiveWriter::new_file`. /// Data written to this writer is compressed and written to the underlying archive. /// /// After writing all data, call `finish` to complete the entry. #[derive(Debug)] pub struct ZipEntryWriter<'a, W> { inner: &'a mut ZipArchiveWriter, compressed_bytes: u64, name_len: u16, local_header_offset: u64, compression_method: CompressionMethod, flags: u16, modification_time: Option, unix_permissions: Option, extra_fields: ExtraFieldsContainer, } /// Configuration for creating data writers that handle uncompressed data and CRC32 calculation. #[derive(Debug)] pub struct ZipDataWriterConfig { crc32_option: Crc32Option, } impl ZipDataWriterConfig { /// Wraps an encoder with a data writer configured with this builder's options. pub fn wrap(self, encoder: E) -> ZipDataWriter { ZipDataWriter::with_crc32(encoder, self.crc32_option) } } impl<'a, W> ZipEntryWriter<'a, W> { /// Returns the total number of bytes successfully written (bytes out). pub fn compressed_bytes(&self) -> u64 { self.compressed_bytes } /// Returns the current offset in the output stream. /// /// See [`ZipArchiveWriter::stream_offset`] for more information. pub fn stream_offset(&self) -> u64 { self.inner.stream_offset() } /// Finishes writing the file entry. /// /// This writes the data descriptor if necessary and adds the file entry to the central directory. pub fn finish(self, mut output: DataDescriptorOutput) -> Result where W: Write, { output.compressed_size = self.compressed_bytes; let mut buffer = [0u8; 24]; buffer[0..4].copy_from_slice(&DataDescriptor::SIGNATURE.to_le_bytes()); buffer[4..8].copy_from_slice(&output.crc.to_le_bytes()); let out_data = if output.compressed_size >= ZIP64_THRESHOLD_FILE_SIZE || output.uncompressed_size >= ZIP64_THRESHOLD_FILE_SIZE { // Use 64-bit sizes for ZIP64 buffer[8..16].copy_from_slice(&output.compressed_size.to_le_bytes()); buffer[16..24].copy_from_slice(&output.uncompressed_size.to_le_bytes()); &buffer[..] } else { // Use 32-bit sizes for standard ZIP buffer[8..12].copy_from_slice(&(output.compressed_size as u32).to_le_bytes()); buffer[12..16].copy_from_slice(&(output.uncompressed_size as u32).to_le_bytes()); &buffer[..16] }; self.inner.writer.write_all(out_data)?; let mut file_header = FileHeader { name_len: self.name_len, compression_method: self.compression_method, local_header_offset: self.local_header_offset, compressed_size: output.compressed_size, uncompressed_size: output.uncompressed_size, crc: output.crc, flags: self.flags, modification_time: self.modification_time, unix_permissions: self.unix_permissions, extra_fields: self.extra_fields, }; file_header.finalize_extra_fields()?; self.inner.files.push(file_header); Ok(self.compressed_bytes) } } impl Write for ZipEntryWriter<'_, W> where W: Write, { fn write(&mut self, buf: &[u8]) -> io::Result { let bytes_written = self.inner.writer.write(buf)?; self.compressed_bytes += bytes_written as u64; Ok(bytes_written) } fn flush(&mut self) -> io::Result<()> { self.inner.writer.flush() } } /// A writer for the uncompressed data of a Zip file entry. /// /// This writer will keep track of the data necessary to write the data /// descriptor (ie: number of bytes written and the CRC32 checksum). /// /// Once all the data has been written, invoke the `finish` method to receive the /// `DataDescriptorOutput` necessary to finalize the entry. #[derive(Debug)] pub struct ZipDataWriter { inner: W, uncompressed_bytes: u64, crc: u32, crc32_option: Crc32Option, } impl ZipDataWriter { /// Creates a new `ZipDataWriter` that writes to an underlying writer. #[deprecated( since = "0.4.0", note = "Use the tuple-based API: `ZipFileBuilder::start()` returns `(writer, builder)` which can propagate the CRC32 option" )] pub fn new(inner: W) -> Self { Self::with_crc32_option(inner, Crc32Option::default()) } /// Creates a new `ZipDataWriter` with the specified CRC32 option. /// /// This is an internal method. Use the tuple-based API via /// `ZipFileBuilder::start()` instead. pub(crate) fn with_crc32(inner: W, crc32_option: Crc32Option) -> Self { Self::with_crc32_option(inner, crc32_option) } /// Creates a new `ZipDataWriter` with a specific CRC32 calculation option. fn with_crc32_option(inner: W, crc32_option: Crc32Option) -> Self { let crc = crc32_option.initial_value(); ZipDataWriter { inner, uncompressed_bytes: 0, crc, crc32_option, } } /// Gets a mutable reference to the underlying writer. pub fn get_mut(&mut self) -> &mut W { &mut self.inner } /// Consumes self and returns the inner writer and the data descriptor to be /// passed to a `ZipEntryWriter`. /// /// The writer is returned to facilitate situations where the underlying /// compressor needs to be notified that no more data will be written so it /// can write any sort of necessary epilogue (think zstd). /// /// The `DataDescriptorOutput` contains the CRC32 checksum and uncompressed size, /// which is needed by `ZipEntryWriter::finish`. pub fn finish(mut self) -> Result<(W, DataDescriptorOutput), Error> where W: Write, { self.flush()?; let output = DataDescriptorOutput { crc: self.crc, compressed_size: 0, uncompressed_size: self.uncompressed_bytes, }; Ok((self.inner, output)) } } impl Write for ZipDataWriter where W: Write, { fn write(&mut self, buf: &[u8]) -> io::Result { let bytes_written = self.inner.write(buf)?; self.uncompressed_bytes += bytes_written as u64; // Only calculate CRC32 if the option is Calculate if matches!(self.crc32_option, Crc32Option::Calculate) { self.crc = crc::crc32_chunk(&buf[..bytes_written], self.crc); } Ok(bytes_written) } fn flush(&mut self) -> io::Result<()> { self.inner.flush() } } /// Contains information written in the data descriptor after the file data. #[derive(Debug, Clone)] pub struct DataDescriptorOutput { crc: u32, compressed_size: u64, uncompressed_size: u64, } impl DataDescriptorOutput { /// Returns the CRC32 checksum of the uncompressed data. pub fn crc(&self) -> u32 { self.crc } /// Returns the uncompressed size of the data. pub fn uncompressed_size(&self) -> u64 { self.uncompressed_size } } #[derive(Debug)] struct FileHeader { name_len: u16, compression_method: CompressionMethod, local_header_offset: u64, compressed_size: u64, uncompressed_size: u64, crc: u32, flags: u16, modification_time: Option, unix_permissions: Option, extra_fields: ExtraFieldsContainer, } impl FileHeader { fn needs_zip64(&self) -> bool { self.compressed_size >= ZIP64_THRESHOLD_FILE_SIZE || self.uncompressed_size >= ZIP64_THRESHOLD_FILE_SIZE || self.local_header_offset >= ZIP64_THRESHOLD_OFFSET } fn finalize_extra_fields(&mut self) -> Result<(), Error> { if self.needs_zip64() { let mut sink = [0u8; 24]; let mut pos = 0; if self.uncompressed_size >= ZIP64_THRESHOLD_FILE_SIZE { sink[pos..pos + 8].copy_from_slice(&self.uncompressed_size.to_le_bytes()); pos += 8; } if self.compressed_size >= ZIP64_THRESHOLD_FILE_SIZE { sink[pos..pos + 8].copy_from_slice(&self.compressed_size.to_le_bytes()); pos += 8; } if self.local_header_offset >= ZIP64_THRESHOLD_OFFSET { sink[pos..pos + 8].copy_from_slice(&self.local_header_offset.to_le_bytes()); pos += 8; } self.extra_fields .add_field(ExtraFieldId::ZIP64, &sink[..pos], Header::CENTRAL)?; } Ok(()) } } /// Writes the ZIP64 End of Central Directory Record fn write_zip64_eocd( writer: &mut W, total_entries: u64, central_directory_size: u64, central_directory_offset: u64, ) -> Result<(), Error> where W: Write, { // ZIP64 End of Central Directory Record signature writer.write_all(&END_OF_CENTRAL_DIR_SIGNATURE64.to_le_bytes())?; // Size of ZIP64 end of central directory record (excluding signature and this field) let record_size = (ZIP64_EOCD_SIZE - 12) as u64; writer.write_all(&record_size.to_le_bytes())?; // Version made by writer.write_all(&ZIP64_VERSION_NEEDED.to_le_bytes())?; // Version needed to extract writer.write_all(&ZIP64_VERSION_NEEDED.to_le_bytes())?; // Number of this disk writer.write_all(&0u32.to_le_bytes())?; // Number of the disk with the start of the central directory writer.write_all(&0u32.to_le_bytes())?; // Total number of entries in the central directory on this disk writer.write_all(&total_entries.to_le_bytes())?; // Total number of entries in the central directory writer.write_all(&total_entries.to_le_bytes())?; // Size of the central directory writer.write_all(¢ral_directory_size.to_le_bytes())?; // Offset of start of central directory with respect to the starting disk number writer.write_all(¢ral_directory_offset.to_le_bytes())?; Ok(()) } /// Writes the ZIP64 End of Central Directory Locator fn write_zip64_eocd_locator(writer: &mut W, zip64_eocd_offset: u64) -> Result<(), Error> where W: Write, { // ZIP64 End of Central Directory Locator signature writer.write_all(&END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE.to_le_bytes())?; // Number of the disk with the start of the ZIP64 end of central directory writer.write_all(&0u32.to_le_bytes())?; // Relative offset of the ZIP64 end of central directory record writer.write_all(&zip64_eocd_offset.to_le_bytes())?; // Total number of disks writer.write_all(&1u32.to_le_bytes())?; Ok(()) } #[derive(Debug, Clone)] struct ZipEntryOptions { compression_method: CompressionMethod, modification_time: Option, unix_permissions: Option, extra_fields: ExtraFieldsContainer, } #[cfg(test)] mod tests { use super::*; use crate::ZipArchive; use std::io::Cursor; #[test] fn test_name_lifetime_independence() { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); // Test file builder with temporary name { let (mut entry, config) = { let temp_name = format!("temp-{}.txt", 42); archive.new_file(&temp_name).start().unwrap() }; let mut writer = config.wrap(&mut entry); writer.write_all(b"test").unwrap(); let (_, desc) = writer.finish().unwrap(); entry.finish(desc).unwrap(); } archive.finish().unwrap(); } #[test] fn test_builder_with_offset_and_capacity() { let mut output = Cursor::new(Vec::new()); output.write_all(b"PREFIX DATA").unwrap(); let offset = output.position(); let mut archive = ZipArchiveWriterBuilder::new() .with_capacity(5) .with_offset(offset) .build(&mut output); let (mut entry, config) = archive.new_file("test.txt").start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"Hello World").unwrap(); let (_, desc) = writer.finish().unwrap(); entry.finish(desc).unwrap(); archive.finish().unwrap(); } #[test] fn test_stream_offset_methods() { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); // Test case 1: Get local header offset let local_header_offset = archive.stream_offset(); let (mut file, config) = archive.new_file("test.txt").start().unwrap(); // Test case 2: Get start of data offset let data_start_offset = file.stream_offset(); // Write some data let mut writer = config.wrap(&mut file); writer.write_all(b"Hello World").unwrap(); let (_, desc) = writer.finish().unwrap(); // Test case 3: Get end of compressed data offset let end_data_offset = file.stream_offset(); let compressed_bytes = file.finish(desc).unwrap(); // Test case 4: Get end of data descriptor offset (next file's local header offset) let end_descriptor_offset = archive.stream_offset(); archive.finish().unwrap(); // Verify the offsets make sense assert_eq!(local_header_offset, 0); assert!(data_start_offset > local_header_offset); assert_eq!( end_data_offset, data_start_offset + b"Hello World".len() as u64 ); assert_eq!(end_descriptor_offset, end_data_offset + 16); // 16 bytes for data descriptor assert_eq!(compressed_bytes, end_data_offset - data_start_offset); } #[test] fn test_crc32_options() { use std::io::Write; let data = b"Hello, world!"; let correct_crc = crate::crc32(data); let incorrect_crc = 0x12345678u32; // Test with default CRC calculation { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive.new_file("normal.txt").start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(data).unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Test with correct custom CRC - should succeed { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("correct.txt") .crc32(Crc32Option::Custom(correct_crc)) .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(data).unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); // Verify the archive can be read let output = output.into_inner(); let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); let wayfinder = entry.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); let mut verifier = entry.verifying_reader(entry.data()); let mut actual = Vec::new(); std::io::copy(&mut verifier, &mut actual).unwrap(); assert_eq!(&actual, data); } // Test with incorrect custom CRC - verification should fail { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("incorrect.txt") .crc32(Crc32Option::Custom(incorrect_crc)) .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(data).unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); // Verify the archive fails verification let output = output.into_inner(); let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); let wayfinder = entry.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); let mut verifier = entry.verifying_reader(entry.data()); let mut actual = Vec::new(); let result = std::io::copy(&mut verifier, &mut actual); // Verification should fail with InvalidChecksum error assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); let source = err.into_inner().unwrap(); let zip_error = source.downcast::().unwrap(); match zip_error.kind() { ErrorKind::InvalidChecksum { expected, actual } => { assert_eq!(*expected, incorrect_crc); assert_eq!(*actual, correct_crc); } _ => panic!("Expected InvalidChecksum error, got {:?}", zip_error.kind()), } } // Test with skipped CRC - should have CRC of 0, and should validate fine { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("skipped.txt") .crc32(Crc32Option::Skip) .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(data).unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); // Verify the archive can be read let output = output.into_inner(); let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); let wayfinder = entry.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); let mut verifier = entry.verifying_reader(entry.data()); let mut actual = Vec::new(); std::io::copy(&mut verifier, &mut actual).unwrap(); assert_eq!(&actual, data); } } #[test] fn test_tuple_api() { use std::io::Write; let data = b"Hello, world!"; let custom_crc = 0x12345678u32; // Test the new tuple-based API with custom CRC let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("test.txt") .crc32(Crc32Option::Custom(custom_crc)) .start() .unwrap(); // Using the new unified API - the CRC option is automatically configured let mut writer = config.wrap(&mut entry); writer.write_all(data).unwrap(); let (_, descriptor) = writer.finish().unwrap(); // Verify the CRC was correctly applied assert_eq!(descriptor.crc, custom_crc); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } #[test] #[allow(deprecated)] fn test_deprecated_create_method() { use std::io::Write; let data = b"Hello, deprecated API!"; // Test that deprecated create() method still works let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let mut entry = archive.new_file("deprecated.txt").create().unwrap(); let mut writer = ZipDataWriter::new(&mut entry); writer.write_all(data).unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); // Verify the archive can be read let output = output.into_inner(); let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); let wayfinder = entry.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); let mut verifier = entry.verifying_reader(entry.data()); let mut actual = Vec::new(); std::io::copy(&mut verifier, &mut actual).unwrap(); assert_eq!(&actual, data); } } rawzip-0.4.4/tests/it/extra_data_zip_tests.rs000064400000000000000000000153161046102023000175170ustar 00000000000000use rawzip::{ZipArchive, ZipArchiveWriter}; use rstest::rstest; use std::io::{Cursor, Write}; /// Helper function to find the start of ZIP data by finding the minimum local header offset fn find_zip_data_start_offset_slice>(archive: &rawzip::ZipSliceArchive) -> u64 { archive .entries() .map(|x| x.unwrap().local_header_offset()) .min() .unwrap_or(archive.directory_offset()) } /// Helper function to find the start of ZIP data by finding the minimum local header offset fn find_zip_data_start_offset_reader(archive: &rawzip::ZipArchive) -> u64 { let mut min_offset = archive.directory_offset(); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let mut entries = archive.entries(&mut buf); while let Some(entry) = entries.next_entry().unwrap() { min_offset = min_offset.min(entry.local_header_offset()); } min_offset } /// Test basic concatenated ZIP functionality: two ZIP files with prefix data #[test] fn test_concatenated_zip_files() { // Create two concatenated ZIP files with prefix data let data = { let mut data = Vec::new(); // First ZIP with prefix data.extend_from_slice(b"PREFIX_FOR_FIRST_ZIP\n"); { let mut archive = ZipArchiveWriter::new(&mut data); let (mut entry, config) = archive.new_file("first.txt").start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"First ZIP content").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Second ZIP with prefix data.extend_from_slice(b"PREFIX_FOR_SECOND_ZIP\n"); { let mut archive = ZipArchiveWriter::new(&mut data); let (mut entry, config) = archive.new_file("second.txt").start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"Second ZIP content").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } data }; // Start off by reading the zip as one normally does let second_archive = ZipArchive::from_slice(&data).unwrap(); // Verify that the last concatenated ZIP would be detected first let entries: Vec<_> = second_archive.entries().collect(); assert_eq!(entries.len(), 1); let entry = entries[0].as_ref().unwrap(); assert_eq!(entry.file_path().as_ref(), b"second.txt"); // Find the start of the second ZIP's data by getting the minimum local header offset let second_zip_start = find_zip_data_start_offset_slice(&second_archive); // Realize that the zip data start is not zero so there is prefix data assert_ne!(second_zip_start, 0); // Attempt to see if there are additional zips in the data. In this test we // could just pass a subset of the slice to the locator // `ZipArchive::from_slice`, but let's emulate what the code would look like // if it was a 100GB file. let locator = rawzip::ZipLocator::new(); let mut buffer = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let reader = std::io::Cursor::new(&data); let first_archive = locator .locate_in_reader(reader, &mut buffer, second_zip_start) .unwrap(); let first_zip_start = find_zip_data_start_offset_reader(&first_archive); // Verify prefix data extraction let prefix = &data[..first_zip_start as usize]; assert_eq!(prefix, b"PREFIX_FOR_FIRST_ZIP\n"); let mut entries_iter = first_archive.entries(&mut buffer); let entry = entries_iter.next_entry().unwrap().unwrap(); assert_eq!(entry.file_path().as_ref(), b"first.txt"); // Verify that we can also recover the prefix data for the second ZIP let first_archive_end = first_archive.end_offset(); let second_prefix = &data[first_archive_end as usize..second_zip_start as usize]; assert_eq!(second_prefix, b"PREFIX_FOR_SECOND_ZIP\n"); } #[test] fn test_zip_with_secret_prelude() { // A secret prelude is where a non zip64 file is preceded by data but is not // captured in the reported central directory offset. This is recoverable // (only for non zip64 files) as we can compare the expected eocd offset // with the actual offset. let cases = [("assets/test.zip", 2)]; for (asset, entries_count) in cases { let data = std::fs::read(asset).unwrap(); let data = [&[0u8; 1000], data.as_slice()].concat(); let archive = rawzip::ZipArchive::from_slice(&data).unwrap(); let zip_start_offset = find_zip_data_start_offset_slice(&archive); let extracted_prefix = &data[..zip_start_offset as usize]; assert_eq!(extracted_prefix, &[0u8; 1000]); let entries: Vec<_> = archive.entries().collect(); assert_eq!(entries.len(), entries_count); } let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; for (asset, entries_count) in cases { let data = std::fs::read(asset).unwrap(); let data = [&[0u8; 1000], data.as_slice()].concat(); let locator = rawzip::ZipLocator::new(); let archive = locator .locate_in_reader(&data, &mut buf, data.len() as u64) .unwrap(); let zip_start_offset = find_zip_data_start_offset_reader(&archive); let extracted_prefix = &data[..zip_start_offset as usize]; assert_eq!(extracted_prefix, &[0u8; 1000]); let mut count = 0; let mut entries = archive.entries(&mut buf); while entries.next_entry().unwrap().is_some() { count += 1; } assert_eq!(count, entries_count); } } #[rstest] #[case(0)] #[case(100)] #[case(65536)] fn test_zip_declared_prelude(#[case] entry_count: usize) { let mut output = Cursor::new(Vec::new()); output.write_all(&[0u8; 1000]).unwrap(); let mut archive = rawzip::ZipArchiveWriter::builder() .with_offset(output.position()) .with_capacity(entry_count) .build(output); for i in 0..entry_count { let filename = format!("file_{:05}.txt", i); let (mut entry, config) = archive.new_file(&filename).start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"x").unwrap(); let (_, descriptor_output) = writer.finish().unwrap(); entry.finish(descriptor_output).unwrap(); } let writer = archive.finish().unwrap(); let data = writer.into_inner(); let archive = rawzip::ZipArchive::from_slice(&data).unwrap(); let zip_start_offset = find_zip_data_start_offset_slice(&archive); assert_eq!(zip_start_offset, 1000); assert_eq!(archive.entries().count(), entry_count); } rawzip-0.4.4/tests/it/extra_fields_test.rs000064400000000000000000000163731046102023000170130ustar 00000000000000use rawzip::{extra_fields::ExtraFieldId, Header, ZipArchive, ZipArchiveWriter, ZipLocator}; use std::io::{Cursor, Write}; #[test] fn test_extra_fields_comprehensive() { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let my_custom_field = ExtraFieldId::new(0x6666); // File with extra fields only in the local file header let (mut local_entry, config) = archive .new_file("video.mp4") .extra_field(my_custom_field, b"field1", Header::LOCAL) .unwrap() .start() .unwrap(); let mut writer = config.wrap(&mut local_entry); writer.write_all(b"video data").unwrap(); let (_, desc) = writer.finish().unwrap(); local_entry.finish(desc).unwrap(); // File with extra fields only in the central directory let (mut central_entry, config) = archive .new_file("document.pdf") .extra_field(my_custom_field, b"field2", Header::CENTRAL) .unwrap() .start() .unwrap(); let mut writer = config.wrap(&mut central_entry); writer.write_all(b"PDF content").unwrap(); let (_, desc) = writer.finish().unwrap(); central_entry.finish(desc).unwrap(); // File with extra fields in both headers for maximum compatibility let (mut both_entry, config) = archive .new_file("important.dat") .extra_field(my_custom_field, b"field3", Header::default()) .unwrap() .start() .unwrap(); let mut writer = config.wrap(&mut both_entry); writer.write_all(b"important data").unwrap(); let (_, desc) = writer.finish().unwrap(); both_entry.finish(desc).unwrap(); archive.finish().unwrap(); // Read it back and verify both central directory and local headers let zip_data = output.into_inner(); let mut buffer = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = ZipLocator::new() .locate_in_reader(&zip_data, &mut buffer, zip_data.len() as u64) .unwrap(); let mut entries = archive.entries(&mut buffer); while let Some(entry) = entries.next_entry().unwrap() { // Test central directory extra fields let central_field_data = entry .extra_fields() .find(|(id, _)| *id == my_custom_field) .map(|(_, data)| data); // Get wayfinder to access local header let wayfinder = entry.wayfinder(); let zip_entry = archive.get_entry(wayfinder).unwrap(); // Test local header extra fields let mut local_buffer = vec![0u8; 1024]; let local_header = zip_entry.local_header(&mut local_buffer).unwrap(); let local_field_data = local_header .extra_fields() .find(|(id, _)| *id == my_custom_field) .map(|(_, data)| data); match entry.file_path().as_ref() { b"video.mp4" => { // LOCAL: not in central directory, but in local header assert_eq!( central_field_data, None, "LOCAL field should not be in central directory" ); assert_eq!( local_field_data, Some(b"field1".as_slice()), "LOCAL field should be in local header" ); } b"document.pdf" => { // CENTRAL: in central directory, but not in local header assert_eq!( central_field_data, Some(b"field2".as_slice()), "CENTRAL field should be in central directory" ); assert_eq!( local_field_data, None, "CENTRAL field should not be in local header" ); } b"important.dat" => { // DEFAULT: in both central directory and local header assert_eq!( central_field_data, Some(b"field3".as_slice()), "DEFAULT field should be in central directory" ); assert_eq!( local_field_data, Some(b"field3".as_slice()), "DEFAULT field should be in local header" ); } _ => {} } } } #[test] fn test_extra_field_size_limit() { let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); // Test individual field size limit let large_data = vec![0u8; 65536]; // Exactly 1 byte too large let result = archive.new_file("test1.txt").extra_field( ExtraFieldId::new(0x1111), &large_data, Header::default(), ); assert!( result.is_err(), "Should fail with oversized individual field" ); // Test total accumulated size limit // Each extra field has 4 bytes overhead (2 bytes ID + 2 bytes length) // So we need multiple fields that total > 65535 bytes including overhead let field_data = vec![0u8; 16380]; // 16380 + 4 = 16384 bytes per field let builder = archive .new_file("test2.txt") .extra_field(ExtraFieldId::new(0x2222), &field_data, Header::default()) .unwrap() .extra_field(ExtraFieldId::new(0x3333), &field_data, Header::default()) .unwrap() .extra_field(ExtraFieldId::new(0x4444), &field_data, Header::default()) .unwrap() .extra_field(ExtraFieldId::new(0x5555), &field_data, Header::default()); // The fourth field should cause us to exceed 65535 bytes total // 4 * (16380 + 4) = 4 * 16384 = 65536 bytes (1 byte over limit) assert!( builder.is_err(), "Should fail when total extra field size exceeds limit" ); } #[test] fn test_extra_field_deduplication_behavior() { // Test that duplicate field IDs are not deduplicated (append-only behavior) let mut output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::new(&mut output); let custom_field = ExtraFieldId::new(0x7777); let (mut entry, config) = archive .new_file("duplicate.txt") .extra_field(custom_field, b"first", Header::default()) .unwrap() .extra_field(custom_field, b"second", Header::default()) .unwrap() .extra_field(custom_field, b"third", Header::default()) .unwrap() .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"test content").unwrap(); let (_, desc) = writer.finish().unwrap(); entry.finish(desc).unwrap(); archive.finish().unwrap(); // Verify all three instances are present in central directory let zip_data = output.into_inner(); let archive = ZipArchive::from_slice(&zip_data).unwrap(); let entry = archive.entries().next().unwrap().unwrap(); // Count instances in central directory let central_instances: Vec<_> = entry .extra_fields() .filter(|(id, _)| *id == custom_field) .map(|(_, data)| data) .collect(); assert_eq!( central_instances.len(), 3, "Should have 3 instances in central directory" ); // Verify the order is preserved assert_eq!(central_instances[0], b"first"); assert_eq!(central_instances[1], b"second"); assert_eq!(central_instances[2], b"third"); } rawzip-0.4.4/tests/it/false_signature_tests.rs000064400000000000000000000105301046102023000176650ustar 00000000000000use rawzip::{ZipArchive, ZipLocator}; use std::io::Read; /// Test handling of false EOCD signatures using the slice API #[test] fn test_false_signature_in_slice() { let mut zip_data = std::fs::read("assets/test.zip").expect("Failed to read test.zip"); zip_data.extend_from_slice(b"This some trailing data: "); zip_data.extend_from_slice(&0x06054b50u32.to_le_bytes()); zip_data.extend_from_slice(b" oh my!\n"); let locator = ZipLocator::new(); let (_, e) = locator.locate_in_slice(&zip_data).unwrap_err(); let offset = e.eocd_offset().unwrap(); assert_eq!(offset, 1195); // Test that we can locate the real zip let archive = locator .locate_in_slice(&zip_data[..offset.saturating_sub(1) as usize]) .unwrap(); assert_eq!(archive.comment().as_bytes(), b"This is a zipfile comment."); } /// Test handling of false signatures using the reader API #[test] fn test_false_signature_in_reader() { let mut zip_data = std::fs::read("assets/test.zip").expect("Failed to read test.zip"); zip_data.extend_from_slice(b"This some trailing data: "); zip_data.extend_from_slice(&0x06054b50u32.to_le_bytes()); zip_data.extend_from_slice(b" oh my\n"); let locator = ZipLocator::new(); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let (_, e) = locator .locate_in_reader(&zip_data, &mut buf, zip_data.len() as u64) .unwrap_err(); let offset = e.eocd_offset().unwrap(); assert_eq!(offset, 1195); // Test that we can locate the real zip let archive = locator .locate_in_reader(&zip_data, &mut buf, offset.saturating_sub(1)) .unwrap(); let mut comment_reader = archive.comment(); let comment_len = comment_reader.remaining() as usize; let mut comment_buffer = vec![0u8; comment_len]; comment_reader.read_exact(&mut comment_buffer).unwrap(); assert_eq!(comment_buffer.as_slice(), b"This is a zipfile comment."); } #[test] fn test_false_eocd_recovery_slice() { for (asset, entries) in &[("assets/test.zip", 2u64), ("assets/zip64.zip", 1u64)] { let mut zip_data = std::fs::read(asset).expect("Failed to read asset"); zip_data.extend_from_slice(b"This some trailing data: "); zip_data.extend_from_slice(&0x06054b50u32.to_le_bytes()); zip_data.extend_from_slice(&[0u8; 18]); let locator = ZipLocator::new(); let archive = locator.locate_in_slice(&zip_data).unwrap(); assert_eq!(archive.entries_hint(), 0); // Use the archive's EOCD offset to restart search and find real archive let offset = archive.eocd_offset(); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let recovered_archive = locator .locate_in_reader(zip_data, &mut buf, offset) .unwrap(); assert_eq!(recovered_archive.entries_hint(), *entries); } } #[test] fn test_eocd_offset_points_to_signature() { for asset in &["assets/test.zip", "assets/zip64.zip"] { let data = std::fs::read(asset).expect("Failed to read asset"); let archive = ZipArchive::from_slice(&data).unwrap(); let eocd_offset = archive.eocd_offset(); let signature = u32::from_le_bytes([ data[eocd_offset as usize], data[eocd_offset as usize + 1], data[eocd_offset as usize + 2], data[eocd_offset as usize + 3], ]); assert_eq!( signature, 0x06054b50, "eocd_offset should point to EOCD signature (0x06054b50), got 0x{:08x}", signature ); } } #[test] fn test_eocd_offset_points_to_signature_reader() { for asset in &["assets/test.zip", "assets/zip64.zip"] { let data = std::fs::read(asset).expect("Failed to read asset"); let archive = ZipArchive::from_file( std::fs::File::open(asset).unwrap(), &mut vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE], ) .unwrap(); let eocd_offset = archive.eocd_offset(); let signature = u32::from_le_bytes([ data[eocd_offset as usize], data[eocd_offset as usize + 1], data[eocd_offset as usize + 2], data[eocd_offset as usize + 3], ]); assert_eq!( signature, 0x06054b50, "eocd_offset should point to EOCD signature (0x06054b50), got 0x{:08x}", signature ); } } rawzip-0.4.4/tests/it/main.rs000064400000000000000000001341511046102023000142220ustar 00000000000000use quickcheck_macros::quickcheck; use rawzip::extra_fields::ExtraFieldId; use rawzip::time::{LocalDateTime, UtcDateTime, ZipDateTimeKind}; use rawzip::{Error, ErrorKind, ZipArchive}; use std::fs::File; use std::io::{Cursor, Read}; use std::path::Path; mod extra_data_zip_tests; mod extra_fields_test; mod false_signature_tests; mod modification_time_tests; mod permission_tests; mod utf8_tests; mod zip64_tests; macro_rules! zip_test_case { ($name:expr, $case:expr) => { paste::paste! { #[test] fn []() { run_zip_test_case_reader(&$case); } #[test] fn []() { run_zip_test_case_slice(&$case); } } }; } #[derive(Debug, Default)] struct ZipTestCase { name: &'static str, comment: Option<&'static [u8]>, files: Vec, expected_error_kind: Option, } #[derive(Debug)] struct ZipTestFileEntry { name: &'static str, expected_content: ExpectedContent, expected_datetime: Option, expected_mode: Option, } #[derive(Debug)] enum ExpectedContent { Content(Vec), File(&'static str), // Size(u64), } zip_test_case!( "test", ZipTestCase { name: "test.zip", comment: Some(b"This is a zipfile comment."), files: vec![ ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(b"This is a test text file.\n".to_vec(),), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 2, 12, 1, 0).unwrap() )), // 2010-09-05 02:12:01 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ZipTestFileEntry { name: "gophercolor16x16.png", expected_content: ExpectedContent::File("gophercolor16x16.png"), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 5, 52, 58, 0).unwrap() )), // 2010-09-05 05:52:58 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ], ..Default::default() } ); zip_test_case!( "readme_notzip", ZipTestCase { name: "readme.notzip", expected_error_kind: Some(ErrorKind::MissingEndOfCentralDirectory), ..Default::default() } ); zip_test_case!( "test_trailing_junk", ZipTestCase { name: "test-trailing-junk.zip", comment: Some(b"This is a zipfile comment."), files: vec![ ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(b"This is a test text file.\n".to_vec(),), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 2, 12, 1, 0).unwrap() )), // 2010-09-05 02:12:01 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ZipTestFileEntry { name: "gophercolor16x16.png", expected_content: ExpectedContent::File("gophercolor16x16.png"), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 5, 52, 58, 0).unwrap() )), // 2010-09-05 05:52:58 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ], ..Default::default() } ); zip_test_case!( "test_prefix", ZipTestCase { name: "test-prefix.zip", comment: Some(b"This is a zipfile comment."), files: vec![ ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(b"This is a test text file.\n".to_vec(),), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 2, 12, 1, 0).unwrap() )), // 2010-09-05 02:12:01 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ZipTestFileEntry { name: "gophercolor16x16.png", expected_content: ExpectedContent::File("gophercolor16x16.png"), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 5, 52, 58, 0).unwrap() )), // 2010-09-05 05:52:58 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ], ..Default::default() } ); zip_test_case!( "symlink", ZipTestCase { name: "symlink.zip", files: vec![ZipTestFileEntry { name: "symlink", expected_content: ExpectedContent::Content(b"../target".to_vec()), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2012, 2, 3, 21, 56, 48, 0).unwrap() )), // 2012-02-03 21:56:48 (UTC from archive) expected_mode: Some(0o120777), // Symlink with 777 permissions }], ..Default::default() } ); zip_test_case!( "readme", ZipTestCase { name: "readme.zip", ..Default::default() } ); zip_test_case!( "winxp", ZipTestCase { // created in windows XP file manager. name: "winxp.zip", files: vec![ ZipTestFileEntry { name: "hello", expected_content: ExpectedContent::Content(b"world \r\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Local( LocalDateTime::from_components(2011, 12, 8, 10, 4, 24, 0).unwrap() )), expected_mode: Some(0o100666), // Regular file with 666 permissions (Windows) }, ZipTestFileEntry { name: "dir/bar", expected_content: ExpectedContent::Content(b"foo \r\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Local( LocalDateTime::from_components(2011, 12, 8, 10, 4, 50, 0).unwrap() )), expected_mode: Some(0o100666), // Regular file with 666 permissions (Windows) }, ZipTestFileEntry { name: "dir/empty/", expected_content: ExpectedContent::Content(b"".to_vec()), expected_datetime: Some(ZipDateTimeKind::Local( LocalDateTime::from_components(2011, 12, 8, 10, 8, 6, 0).unwrap() )), expected_mode: Some(0o040777), // Directory with 777 permissions (Windows) }, ZipTestFileEntry { name: "readonly", expected_content: ExpectedContent::Content(b"important \r\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Local( LocalDateTime::from_components(2011, 12, 8, 10, 6, 8, 0).unwrap() )), expected_mode: Some(0o100444), // Read-only file (Windows) }, ], ..Default::default() } ); zip_test_case!( "unix", ZipTestCase { // created by Zip 3.0 under Linux name: "unix.zip", files: vec![ ZipTestFileEntry { name: "hello", expected_content: ExpectedContent::Content(b"world \r\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2011, 12, 8, 10, 4, 24, 0).unwrap() )), // 2011-12-08 10:04:24 UTC (but stored as local time) expected_mode: Some(0o100666), // Regular file with 666 permissions (Unix) }, ZipTestFileEntry { name: "dir/bar", expected_content: ExpectedContent::Content(b"foo \r\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2011, 12, 8, 10, 4, 50, 0).unwrap() )), // 2011-12-08 10:04:50 UTC (but stored as local time) expected_mode: Some(0o100666), // Regular file with 666 permissions (Unix) }, ZipTestFileEntry { name: "dir/empty/", expected_content: ExpectedContent::Content(b"".to_vec()), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2011, 12, 8, 10, 8, 6, 0).unwrap() )), // 2011-12-08 10:08:06 UTC (but stored as local time) expected_mode: Some(0o040777), // Directory with 777 permissions (Unix) }, ZipTestFileEntry { name: "readonly", expected_content: ExpectedContent::Content(b"important \r\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2011, 12, 8, 10, 6, 8, 0).unwrap() )), // 2011-12-08 10:06:08 UTC (but stored as local time) expected_mode: Some(0o100444), // Read-only file (Unix) }, ], ..Default::default() } ); zip_test_case!( "go_with_datadesc_sig", ZipTestCase { // created by Go, after we wrote the "optional" data // descriptor signatures (which are required by macOS) name: "go-with-datadesc-sig.zip", files: vec![ ZipTestFileEntry { name: "foo.txt", expected_content: ExpectedContent::Content(b"foo\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Local( LocalDateTime::from_components(1980, 1, 1, 0, 0, 0, 0).unwrap() )), // DOS timestamp 0x0000 0x0000 normalized to 1980-01-01 00:00:00 expected_mode: Some(0o100666), // Regular file with 666 permissions }, ZipTestFileEntry { name: "bar.txt", expected_content: ExpectedContent::Content(b"bar\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Local( LocalDateTime::from_components(1980, 1, 1, 0, 0, 0, 0).unwrap() )), // DOS timestamp 0x0000 0x0000 normalized to 1980-01-01 00:00:00 expected_mode: Some(0o100666), // Regular file with 666 permissions }, ], ..Default::default() } ); zip_test_case!( "crc32_not_streamed", ZipTestCase { name: "crc32-not-streamed.zip", files: vec![ ZipTestFileEntry { name: "foo.txt", expected_content: ExpectedContent::Content(b"foo\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2012, 3, 9, 0, 59, 10, 0).unwrap() )), // 2012-03-09 00:59:10 (UTC from archive) expected_mode: Some(0o100644), // Regular file with 644 permissions }, ZipTestFileEntry { name: "bar.txt", expected_content: ExpectedContent::Content(b"bar\n".to_vec()), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2012, 3, 9, 0, 59, 12, 0).unwrap() )), // 2012-03-09 00:59:12 (UTC from archive) expected_mode: Some(0o100644), // Regular file with 644 permissions }, ], ..Default::default() } ); zip_test_case!( "zip64_2", ZipTestCase { name: "zip64-2.zip", files: vec![ZipTestFileEntry { name: "README", expected_content: ExpectedContent::Content( b"This small file is in ZIP64 format.\n".to_vec(), ), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2012, 8, 10, 18, 33, 32, 0).unwrap() )), // 2012-08-10 18:33:32 (UTC from archive) expected_mode: Some(0o100644), // Regular file with 644 permissions }], ..Default::default() } ); zip_test_case!( "time_7zip", ZipTestCase { name: "time-7zip.zip", files: vec![ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(vec![]), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2017, 11, 1, 4, 11, 57, 244817900).unwrap() )), // 2017-10-31 21:11:57.244817900 (-7 hours) = 2017-11-01 04:11:57.244817900 UTC expected_mode: Some(0o100666), // Regular file with 666 permissions }], ..Default::default() } ); zip_test_case!( "time_infozip", ZipTestCase { name: "time-infozip.zip", files: vec![ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(vec![]), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2017, 11, 1, 4, 11, 57, 0).unwrap() )), // 2017-10-31 21:11:57.000 (-7 hours) = 2017-11-01 04:11:57.000 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }], ..Default::default() } ); zip_test_case!( "time_osx", ZipTestCase { name: "time-osx.zip", files: vec![ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(vec![]), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2017, 11, 1, 4, 11, 57, 0).unwrap() )), // 2017-10-31 21:11:57.000 (-7 hours) = 2017-11-01 04:11:57.000 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }], ..Default::default() } ); zip_test_case!( "time_win7", ZipTestCase { name: "time-win7.zip", files: vec![ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(vec![]), expected_datetime: Some(ZipDateTimeKind::Local( LocalDateTime::from_components(2017, 10, 31, 21, 11, 58, 0).unwrap() )), // 2017-10-31 21:11:58.000 (DOS local time) expected_mode: Some(0o100666), // Regular file with 666 permissions }], ..Default::default() } ); zip_test_case!( "time_winrar", ZipTestCase { name: "time-winrar.zip", files: vec![ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(vec![]), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2017, 11, 1, 4, 11, 57, 244817900).unwrap() )), // 2017-10-31 21:11:57.244817900 (-7 hours) = 2017-11-01 04:11:57.244817900 UTC expected_mode: Some(0o100666), // Regular file with 666 permissions }], ..Default::default() } ); zip_test_case!( "time_winzip", ZipTestCase { name: "time-winzip.zip", files: vec![ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(vec![]), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2017, 11, 1, 4, 11, 57, 244000000).unwrap() )), // 2017-10-31 21:11:57.244000000 (-7 hours) = 2017-11-01 04:11:57.244000000 UTC expected_mode: Some(0o100666), // Regular file with 666 permissions }], ..Default::default() } ); zip_test_case!( "time_go", ZipTestCase { name: "time-go.zip", files: vec![ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(vec![]), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2017, 11, 1, 4, 11, 57, 0).unwrap() )), // 2017-10-31 21:11:57.000 (-7 hours) = 2017-11-01 04:11:57.000 UTC expected_mode: Some(0o100666), // Regular file with 666 permissions }], ..Default::default() } ); zip_test_case!( "badbase", ZipTestCase { name: "test-badbase.zip", comment: Some(b"This is a zipfile comment."), files: vec![ ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(b"This is a test text file.\n".to_vec(),), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 2, 12, 1, 0).unwrap() )), // 2010-09-05 02:12:01 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ZipTestFileEntry { name: "gophercolor16x16.png", expected_content: ExpectedContent::File("gophercolor16x16.png"), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 5, 52, 58, 0).unwrap() )), // 2010-09-05 05:52:58 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ], ..Default::default() } ); zip_test_case!( "baddirsz", ZipTestCase { name: "test-baddirsz.zip", comment: Some(b"This is a zipfile comment."), files: vec![ ZipTestFileEntry { name: "test.txt", expected_content: ExpectedContent::Content(b"This is a test text file.\n".to_vec(),), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 2, 12, 1, 0).unwrap() )), // 2010-09-05 02:12:01 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ZipTestFileEntry { name: "gophercolor16x16.png", expected_content: ExpectedContent::File("gophercolor16x16.png"), expected_datetime: Some(ZipDateTimeKind::Utc( UtcDateTime::from_components(2010, 9, 5, 5, 52, 58, 0).unwrap() )), // 2010-09-05 05:52:58 UTC expected_mode: Some(0o100644), // Regular file with 644 permissions }, ], ..Default::default() } ); fn process_archive_files( archive: &rawzip::ZipArchive, case: &ZipTestCase, buf: &mut [u8], ) -> Result<(), Error> { if let Some(expected_comment_bytes) = case.comment { let mut comment_reader = archive.comment(); let comment_len = comment_reader.remaining() as usize; let mut comment_buffer = vec![0u8; comment_len]; comment_reader.read_exact(&mut comment_buffer).unwrap(); assert_eq!( comment_buffer.as_slice(), expected_comment_bytes, "Comment mismatch for {}", case.name ); } let mut actual_files_found = 0; for expected_file in &case.files { let mut found_file = false; let mut entries_for_current_expected_file = archive.entries(buf); loop { match entries_for_current_expected_file.next_entry() { Ok(Some(entry)) => { if entry.file_path().try_normalize().unwrap().as_ref() == expected_file.name { actual_files_found += 1; found_file = true; if let Some(expected_dt) = &expected_file.expected_datetime { let actual_dt = entry.last_modified(); assert_eq!( &actual_dt, expected_dt, "Datetime mismatch for file {}: expected {}, got {}", expected_file.name, expected_dt, actual_dt ); } if let Some(expected_mode) = expected_file.expected_mode { let actual_mode = entry.mode().value(); assert_eq!( actual_mode, expected_mode, "Mode mismatch for file {}: expected 0o{:o}, got 0o{:o}", expected_file.name, expected_mode, actual_mode ); } let position = entry.wayfinder(); let ent = archive.get_entry(position)?; let mut data = Vec::new(); match entry.compression_method() { rawzip::CompressionMethod::Deflate => { let inflater = flate2::read::DeflateDecoder::new(ent.reader()); let mut verifier = ent.verifying_reader(inflater); std::io::copy(&mut verifier, &mut Cursor::new(&mut data)).unwrap(); } rawzip::CompressionMethod::Store => { let mut verifier = ent.verifying_reader(ent.reader()); std::io::copy(&mut verifier, &mut Cursor::new(&mut data)).unwrap(); } _ => todo!( "Compression method not yet handled: {:?}", entry.compression_method() ), } match &expected_file.expected_content { ExpectedContent::Content(expected_bytes) => { assert_eq!( &data, expected_bytes, "Content mismatch for file {} in {}", expected_file.name, case.name ); } ExpectedContent::File(content_file_name) => { let content_path = Path::new("assets").join(content_file_name); let expected_bytes = std::fs::read(content_path).unwrap(); assert_eq!( &data, &expected_bytes, "Content mismatch for file {} (from {}) in {}", expected_file.name, content_file_name, case.name ); } } break; } } Ok(None) => break, Err(e) => panic!("Error iterating entries in {}: {:?}", case.name, e), } } if !found_file { panic!( "Expected file {} not found in archive {}", expected_file.name, case.name ); } } assert_eq!( actual_files_found, case.files.len(), "File count mismatch for {}. Expected {}, found {}", case.name, case.files.len(), actual_files_found ); Ok(()) } fn process_slice_archive_files( archive: &rawzip::ZipSliceArchive<&[u8]>, case: &ZipTestCase, ) -> Result<(), Error> { if let Some(expected_comment_bytes) = case.comment { assert_eq!( archive.comment().as_bytes(), expected_comment_bytes, "Comment mismatch for {}", case.name ); } let mut actual_files_found = 0; for expected_file in &case.files { let mut found_file = false; let mut entries_for_current_expected_file = archive.entries(); loop { match entries_for_current_expected_file.next_entry() { Ok(Some(entry)) => { if entry.file_path().try_normalize().unwrap().as_ref() == expected_file.name { actual_files_found += 1; found_file = true; if let Some(expected_dt) = &expected_file.expected_datetime { let actual_dt = entry.last_modified(); assert_eq!( &actual_dt, expected_dt, "Datetime mismatch for file {}: expected {}, got {}", expected_file.name, expected_dt, actual_dt ); } if let Some(expected_mode) = expected_file.expected_mode { let actual_mode = entry.mode().value(); assert_eq!( actual_mode, expected_mode, "Mode mismatch for file {}: expected 0o{:o}, got 0o{:o}", expected_file.name, expected_mode, actual_mode ); } let position = entry.wayfinder(); let ent = archive.get_entry(position)?; let mut data = Vec::new(); match entry.compression_method() { rawzip::CompressionMethod::Deflate => { let inflater = flate2::read::DeflateDecoder::new(ent.data()); let mut verifier = ent.verifying_reader(inflater); std::io::copy(&mut verifier, &mut Cursor::new(&mut data)).unwrap(); } rawzip::CompressionMethod::Store => { let mut verifier = ent.verifying_reader(ent.data()); std::io::copy(&mut verifier, &mut Cursor::new(&mut data)).unwrap(); } _ => todo!( "Compression method not yet handled: {:?}", entry.compression_method() ), } match &expected_file.expected_content { ExpectedContent::Content(expected_bytes) => { assert_eq!( &data, expected_bytes, "Content mismatch for file {} in {}", expected_file.name, case.name ); } ExpectedContent::File(content_file_name) => { let content_path = Path::new("assets").join(content_file_name); let expected_bytes = std::fs::read(content_path).unwrap(); assert_eq!( &data, &expected_bytes, "Content mismatch for file {} (from {}) in {}", expected_file.name, content_file_name, case.name ); } } break; } } Ok(None) => break, Err(e) => panic!("Error iterating entries in {}: {:?}", case.name, e), } } if !found_file { panic!( "Expected file {} not found in archive {}", expected_file.name, case.name ); } } assert_eq!( actual_files_found, case.files.len(), "File count mismatch for {}. Expected {}, found {}", case.name, case.files.len(), actual_files_found ); Ok(()) } fn run_zip_test_case_reader(case: &ZipTestCase) { let file_path = Path::new("assets").join(case.name); let f = File::open(file_path).unwrap(); fn processor(f: File, case: &ZipTestCase) -> Result<(), Error> { let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = rawzip::ZipArchive::from_file(f, &mut buf[..])?; process_archive_files(&archive, case, &mut buf)?; Ok(()) } match (processor(f, case), case.expected_error_kind.as_ref()) { (Ok(_), None) => {} (Ok(_), Some(expected)) => { panic!( "Expected error {:?}, but got Ok for {}", expected, case.name ); } (Err(e), None) => { panic!("Unexpected error {:?} for {}", e, case.name); } (Err(e), Some(expected)) => { assert!( errors_eq(&e, expected), "Error kind mismatch for {}: {:?} != {:?}", case.name, e.kind(), expected ); } }; } fn run_zip_test_case_slice(case: &ZipTestCase) { fn processor(case: &ZipTestCase) -> Result<(), Error> { let file_path = Path::new("assets").join(case.name); let data = std::fs::read(file_path).unwrap(); let archive = rawzip::ZipArchive::from_slice(data.as_slice())?; process_slice_archive_files(&archive, case)?; Ok(()) } match (processor(case), case.expected_error_kind.as_ref()) { (Ok(_), None) => {} (Ok(_), Some(expected)) => { panic!( "Expected error {:?}, but got Ok for {}", expected, case.name ); } (Err(e), None) => { panic!("Unexpected error {:?} for {}", e, case.name); } (Err(e), Some(expected)) => { assert!( errors_eq(&e, expected), "Error kind mismatch for {}: {:?} != {:?}", case.name, e.kind(), expected ); } }; } fn errors_eq(a: &Error, b: &ErrorKind) -> bool { match (a.kind(), b) { ( ErrorKind::InvalidSignature { expected: a_exp, .. }, ErrorKind::InvalidSignature { expected: b_exp, .. }, ) => a_exp == b_exp, ( ErrorKind::InvalidChecksum { expected: a_exp, .. }, ErrorKind::InvalidChecksum { expected: b_exp, .. }, ) => a_exp == b_exp, ( ErrorKind::InvalidSize { expected: a_exp, .. }, ErrorKind::InvalidSize { expected: b_exp, .. }, ) => a_exp == b_exp, (ErrorKind::InvalidUtf8(a), ErrorKind::InvalidUtf8(b)) => a == b, (ErrorKind::InvalidInput { msg: a }, ErrorKind::InvalidInput { msg: b }) => a == b, (ErrorKind::IO(a), ErrorKind::IO(b)) => a.kind() == b.kind(), (ErrorKind::Eof, ErrorKind::Eof) => true, (ErrorKind::MissingEndOfCentralDirectory, ErrorKind::MissingEndOfCentralDirectory) => true, ( ErrorKind::MissingZip64EndOfCentralDirectory, ErrorKind::MissingZip64EndOfCentralDirectory, ) => true, (ErrorKind::BufferTooSmall, ErrorKind::BufferTooSmall) => true, _ => false, } } #[test] fn catch_incorrect_crc_without_data_descriptor() { let mut data = std::fs::read("assets/crc32-not-streamed.zip").unwrap(); let archive = ZipArchive::from_slice(data.as_slice()).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert!(!entry.has_data_descriptor()); // Mutate the central directory CRC to be incorrect let crc_offset = entry.central_directory_offset() as usize + 16; let original_crc = u32::from_le_bytes(data[crc_offset..crc_offset + 4].try_into().unwrap()); let corrupted_crc = original_crc ^ 0xffff_ffff; data[crc_offset..crc_offset + 4].copy_from_slice(&corrupted_crc.to_le_bytes()); // Ensure that the slice verifier rejects the bad CRC let archive = ZipArchive::from_slice(data.as_slice()).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); let ent = archive.get_entry(entry.wayfinder()).unwrap(); let mut verifier = ent.verifying_reader(ent.data()); let slice_result = std::io::copy(&mut verifier, &mut std::io::sink()); let err = slice_result.unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); let source = err.into_inner().unwrap(); let zip_error = source.downcast::().unwrap(); match zip_error.kind() { ErrorKind::InvalidChecksum { expected, actual } => { assert_eq!(*expected, corrupted_crc); assert_eq!(*actual, original_crc); } other => panic!("expected InvalidChecksum error, got {:?}", other), } // Ensure that the reader verifier rejects the bad CRC let mut buffer = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_seekable(Cursor::new(data), &mut buffer).unwrap(); let mut entries = archive.entries(&mut buffer); let entry = entries.next_entry().unwrap().unwrap(); let ent = archive.get_entry(entry.wayfinder()).unwrap(); let mut verifier = ent.verifying_reader(ent.reader()); let reader_result = std::io::copy(&mut verifier, &mut std::io::sink()); let err = reader_result.unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidData); let source = err.into_inner().unwrap(); let zip_error = source.downcast::().unwrap(); match zip_error.kind() { ErrorKind::InvalidChecksum { expected, actual } => { assert_eq!(*expected, corrupted_crc); assert_eq!(*actual, original_crc); } other => panic!("expected InvalidChecksum error, got {:?}", other), } } /// This test is to ensure that the ZipArchive can be created from a Vec #[test] fn zip_integration_tests_vec() { let data = std::fs::read("assets/zip64.zip").unwrap(); let archive = rawzip::ZipArchive::from_slice(data).unwrap(); assert_eq!(archive.comment().as_bytes(), b""); let reader = archive.into_zip_archive(); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let mut entries = reader.entries(&mut buf); let mut count = 0; while let Some(entry) = entries.next_entry().unwrap() { if entry.is_dir() { continue; } count += 1; } assert_eq!(count, 1); } /// This test is to ensure that the ZipArchive can be created from a custom type /// that implements AsRef but not ReaderAt. #[test] fn zip_integration_test_custom_as_ref() { struct MyBuffer { data: Vec, } impl AsRef<[u8]> for MyBuffer { fn as_ref(&self) -> &[u8] { &self.data } } let data = std::fs::read("assets/zip64.zip").unwrap(); let my_buffer = MyBuffer { data }; let archive = rawzip::ZipArchive::from_slice(&my_buffer).unwrap(); assert_eq!(archive.comment().as_bytes(), b""); let reader = archive.into_zip_archive(); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let mut entries = reader.entries(&mut buf); let mut count = 0; while let Some(entry) = entries.next_entry().unwrap() { if entry.is_dir() { continue; } count += 1; } assert_eq!(count, 1); } #[quickcheck] fn test_read_what_we_write_slice(data: Vec) { let mut output = Vec::new(); { let mut archive = rawzip::ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive.new_file("file.txt").start().unwrap(); let mut writer = config.wrap(&mut entry); std::io::copy(&mut Cursor::new(&data), &mut writer).unwrap(); let (_, descriptor) = writer.finish().unwrap(); assert_eq!(descriptor.uncompressed_size(), data.len() as u64); let compressed = entry.finish(descriptor).unwrap(); assert_eq!(compressed, data.len() as u64); archive.finish().unwrap(); } let archive = rawzip::ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "file.txt" ); assert_eq!(entry.compression_method(), rawzip::CompressionMethod::Store); assert_eq!(entry.uncompressed_size_hint(), data.len() as u64); assert_eq!(entry.compressed_size_hint(), data.len() as u64); let wayfinder = entry.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); let mut actual = Vec::new(); std::io::copy(&mut entry.data(), &mut Cursor::new(&mut actual)).unwrap(); assert_eq!(data, actual); } #[test] fn invalid_directory_offset_should_fail_to_parse() { let data = [ 80, 75, 5, 6, 255, 255, 6, 1, 250, 255, 255, 255, 255, 255, 255, 80, 75, 255, 255, 249, 255, 255, 255, 255, 127, 255, ]; let result = rawzip::ZipArchive::from_slice(&data); assert!(result.is_err()); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let locator = rawzip::ZipLocator::new(); let result = locator.locate_in_reader(&data[..], &mut buf, data.len() as u64); assert!(result.is_err()); } #[test] fn test_should_not_overflow_on_offsets() { let data = [ 80, 75, 3, 4, 20, 0, 0, 0, 8, 0, 48, 116, 10, 65, 126, 231, 255, 105, 36, 0, 0, 0, 36, 1, 0, 0, 0, 0, 0, 0, 69, 219, 65, 68, 77, 69, 11, 201, 200, 44, 86, 40, 206, 77, 204, 201, 81, 72, 203, 204, 73, 34, 0, 60, 242, 76, 243, 20, 48, 162, 204, 3, 20, 210, 242, 139, 114, 19, 75, 244, 184, 0, 80, 75, 1, 2, 45, 3, 45, 0, 0, 0, 8, 0, 48, 114, 10, 65, 126, 231, 255, 105, 255, 255, 255, 255, 255, 255, 255, 255, 6, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 164, 129, 0, 0, 0, 0, 82, 69, 65, 68, 77, 69, 1, 0, 16, 0, 36, 16, 0, 0, 0, 80, 75, 5, 255, 255, 255, 255, 255, 255, 255, 255, 80, 75, 6, 6, 44, 0, 0, 0, 0, 0, 0, 0, 45, 0, 45, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 128, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 80, 75, 6, 7, 0, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 80, 75, 5, 6, 0, 0, 0, 64, 255, 255, 0, 0, 8, 0, 53, 116, 10, 65, 126, 231, 0, 0, 0, 0, 7, 0, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, ]; let result = rawzip::ZipArchive::from_slice(&data).unwrap(); let entries = result.entries(); let mut has_error = false; for entry in entries { let Ok(entry) = entry else { has_error = true; break; }; has_error |= result.get_entry(entry.wayfinder()).is_err(); } assert!(has_error); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let locator = rawzip::ZipLocator::new(); let result = locator .locate_in_reader(&data[..], &mut buf, data.len() as u64) .unwrap(); let mut entries = result.entries(&mut buf); let mut has_error = false; loop { let Ok(entry) = entries.next_entry() else { has_error = true; break; }; let Some(entry) = entry else { break; }; has_error |= result.get_entry(entry.wayfinder()).is_err(); } assert!(has_error); } #[test] fn test_java_jar_cafe_extra_field() { let file = std::fs::File::open("assets/test.jar").expect("Failed to open test.jar"); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(file, &mut buf).expect("Failed to create ZipArchive"); let mut entries = archive.entries(&mut buf); let entry = entries.next_entry().unwrap().unwrap(); let mut extra_fields = entry.extra_fields(); let mut found = false; for (field_id, _field_data) in extra_fields.by_ref() { if field_id == ExtraFieldId::JAVA_JAR { found = true; break; } } assert!(found, "Expected to find JAVA_JAR extra field (CAFE)"); assert!( extra_fields.remaining_bytes().is_empty(), "No remaining bytes expected after consuming fields" ); } #[test] fn test_filename_mismatch_handling() { // Test the filename mismatch test fixture with ZipEntry (file-based) let file = std::fs::File::open("assets/filename_mismatch_test.zip") .expect("Failed to open filename_mismatch_test.zip"); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(file, &mut buf).expect("Failed to create ZipArchive"); let mut entries = archive.entries(&mut buf); let entry_header = entries.next_entry().unwrap().unwrap(); // Central directory should show "malware.exe" assert_eq!(entry_header.file_path().as_ref(), b"malware.exe",); // Get the ZipEntry to access local_file_path let wayfinder = entry_header.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); let mut local_buffer = vec![0u8; 512]; let local_header = entry.local_header(&mut local_buffer).unwrap(); assert_eq!(local_header.file_path().as_ref(), b"safe_file.txt"); // Test slice version let data = std::fs::read("assets/filename_mismatch_test.zip").unwrap(); let slice_archive = rawzip::ZipArchive::from_slice(data.as_slice()).unwrap(); let mut slice_entries = slice_archive.entries(); let slice_header = slice_entries.next_entry().unwrap().unwrap(); assert_eq!(slice_header.file_path().as_ref(), b"malware.exe",); let slice_wayfinder = slice_header.wayfinder(); let slice_entry = slice_archive.get_entry(slice_wayfinder).unwrap(); let slice_local_filename = slice_entry.file_path(); assert_eq!(slice_local_filename.as_ref(), b"safe_file.txt",); } #[test] fn test_central_directory_offset_consistency() { let test_files = [ "test.zip", "test-prefix.zip", "test-trailing-junk.zip", "unix.zip", "winxp.zip", "zip64-2.zip", "zip64.zip", ]; for test_file in &test_files { let file_path = Path::new("assets").join(test_file); let data = std::fs::read(&file_path).unwrap(); // Test with ZipSliceArchive let slice_archive = ZipArchive::from_slice(data.as_slice()).unwrap(); let slice_entries: Vec<_> = slice_archive .entries() .collect::, _>>() .unwrap(); // Test with ZipArchive let file = File::open(&file_path).unwrap(); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let file_archive = ZipArchive::from_file(file, &mut buf).unwrap(); let mut entries_iter = file_archive.entries(&mut buf); for slice_entry in slice_entries.iter() { let file_entry = entries_iter.next_entry().unwrap().unwrap(); assert_eq!( slice_entry.central_directory_offset(), file_entry.central_directory_offset(), "Central directory offset mismatch", ); } assert!( entries_iter.next_entry().unwrap().is_none(), "More entries in file archive than in slice archive" ); } } #[test] fn test_ff_optimized_jar() { // Firefox's omni.ja is an interesting use case where the central directory // is placed at the start of the file. Rawzip can parse this file, but it // requires the end user to do entry bookkeeping, as central directory will // end long before the EOCD is encountered. The test case is a smaller // version of omni.ja: https://taras.glek.net/posts/optimized-zip-format/ let data = std::fs::read("assets/omni-mini.ja").unwrap(); let archive = ZipArchive::from_slice(&data).unwrap(); let mut entries = archive.entries(); assert_eq!(archive.entries_hint(), 1); let first = entries.next().unwrap().unwrap(); let wayfinder = first.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); assert_eq!( first.compression_method(), rawzip::CompressionMethod::Deflate ); let reader = flate2::read::DeflateDecoder::new(entry.data()); let mut reader = entry.verifying_reader(reader); let count = std::io::copy(&mut reader, &mut std::io::sink()).unwrap(); assert_eq!(first.uncompressed_size_hint(), count); // We expect an error, but we provide enough tools for consumers to be able // to swallow this error if they choose when certain conditions are met // (like the number of entries seen are expected). entries.next().unwrap().unwrap_err(); } #[test] fn test_ff_optimized_jar_reader() { let data = std::fs::File::open("assets/omni-mini.ja").unwrap(); let mut buffer = vec![0; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(data, &mut buffer).unwrap(); let mut entries = archive.entries(&mut buffer); assert_eq!(archive.entries_hint(), 1); let first = entries.next_entry().unwrap().unwrap(); let wayfinder = first.wayfinder(); let entry = archive.get_entry(wayfinder).unwrap(); assert_eq!( first.compression_method(), rawzip::CompressionMethod::Deflate ); let reader = flate2::read::DeflateDecoder::new(entry.reader()); let mut reader = entry.verifying_reader(reader); let count = std::io::copy(&mut reader, &mut std::io::sink()).unwrap(); assert_eq!(first.uncompressed_size_hint(), count); entries.next_entry().unwrap_err(); } #[test] fn test_custom_crc_reader() { // Tests that consumers can "bring their own CRC" if they want let f = File::open("assets/test.zip").unwrap(); let mut buf = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(f, &mut buf).unwrap(); let mut entries = archive.entries(&mut buf); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.compression_method(), rawzip::CompressionMethod::Deflate ); let wayfinder = entry.wayfinder(); let ent = archive.get_entry(wayfinder).unwrap(); let zip_reader = ent.reader(); let decoder = flate2::read::DeflateDecoder::new(zip_reader); let mut reader = flate2::CrcReader::new(decoder); std::io::copy(&mut reader, &mut std::io::sink()).unwrap(); let actual_crc = reader.crc().sum(); let size = reader.get_ref().total_out(); let zip_reader = reader.into_inner().into_inner(); let verification = zip_reader.claim_verifier().unwrap(); assert_ne!(actual_crc, 0, "CRC should not be zero"); assert_eq!(verification.crc(), actual_crc); verification .valid(rawzip::ZipVerification { crc: actual_crc, uncompressed_size: size, }) .unwrap(); } rawzip-0.4.4/tests/it/modification_time_tests.rs000064400000000000000000000346751046102023000202150ustar 00000000000000use rawzip::{ extra_fields::{ExtraFieldId, ExtraFields}, time::{LocalDateTime, UtcDateTime, ZipDateTimeKind}, ZipArchive, ZipArchiveWriter, }; use std::io::Write; /// Test that modification times are preserved in a round-trip for files #[test] fn test_modification_time_roundtrip_file() { let datetime = UtcDateTime::from_components(2023, 6, 15, 14, 30, 45, 0).unwrap(); let mut output = Vec::new(); // Create archive with modification time { let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("test.txt") .last_modified(datetime) .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"Hello, world!").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Read back and verify modification time let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "test.txt" ); let actual_datetime = entry.last_modified(); assert_eq!(actual_datetime, ZipDateTimeKind::Utc(datetime)); } /// Test that modification times are preserved in a round-trip for directories #[test] fn test_modification_time_roundtrip_directory() { let datetime = UtcDateTime::from_components(2023, 8, 20, 9, 15, 30, 0).unwrap(); let mut output = Vec::new(); // Create archive with directory modification time { let mut archive = ZipArchiveWriter::new(&mut output); archive .new_dir("test_dir/") .last_modified(datetime) .create() .unwrap(); archive.finish().unwrap(); } // Read back and verify modification time let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "test_dir/" ); let actual_datetime = entry.last_modified(); assert_eq!(actual_datetime, ZipDateTimeKind::Utc(datetime)); } /// Test that files without modification time use DOS timestamp 0 #[test] fn test_no_modification_time_defaults_to_zero() { let mut output = Vec::new(); // Create archive without modification time { let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive.new_file("test.txt").start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"Hello, world!").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Read back and verify it uses the "zero" timestamp (1980-01-01 00:00:00) let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "test.txt" ); let actual_datetime = entry.last_modified(); // Should be the DOS timestamp 0 normalized to 1980-01-01 00:00:00 let expected = ZipDateTimeKind::Local(LocalDateTime::from_components(1980, 1, 1, 0, 0, 0, 0).unwrap()); assert_eq!(actual_datetime, expected); } /// Test that extended timestamp format is used when modification time is provided #[test] fn test_extended_timestamp_format_present() { let datetime = UtcDateTime::from_components(2023, 6, 15, 14, 30, 45, 0).unwrap(); let mut output = Vec::new(); // Create archive with modification time { let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("test.txt") .last_modified(datetime) .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"Hello, world!").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Check that the extended timestamp extra field is present // Extended timestamp field ID is 0x5455 let extended_timestamp_id_bytes = 0x5455u16.to_le_bytes(); let contains_extended_timestamp = output.windows(2).any(|w| w == extended_timestamp_id_bytes); assert!( contains_extended_timestamp, "Extended timestamp extra field should be present when modification time is provided" ); } /// Test that no extended timestamp format is used when no modification time is provided #[test] fn test_no_extended_timestamp_without_modification_time() { let mut output = Vec::new(); // Create archive without modification time { let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive.new_file("test.txt").start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"Hello, world!").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Check that the extended timestamp extra field is NOT present let extended_timestamp_id_bytes = 0x5455u16.to_le_bytes(); let contains_extended_timestamp = output.windows(2).any(|w| w == extended_timestamp_id_bytes); assert!( !contains_extended_timestamp, "Extended timestamp extra field should NOT be present when no modification time is provided" ); } /// Test that we can handle timestamps outside DOS range (before 1980) #[test] fn test_timestamp_before_dos_range() { let datetime = UtcDateTime::from_components(1970, 1, 1, 0, 0, 0, 0).unwrap(); let mut output = Vec::new(); // Create archive with pre-1980 timestamp { let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("test.txt") .last_modified(datetime) .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"Hello, world!").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "test.txt" ); let actual_datetime = entry.last_modified(); assert_eq!(actual_datetime, ZipDateTimeKind::Utc(datetime)); } /// Test multiple files with different modification times #[test] fn test_multiple_files_different_timestamps() { let datetime1 = UtcDateTime::from_components(2023, 1, 15, 10, 0, 0, 0).unwrap(); let datetime2 = UtcDateTime::from_components(2023, 6, 20, 15, 30, 45, 0).unwrap(); let mut output = Vec::new(); // Create archive with multiple files having different timestamps { let mut archive = ZipArchiveWriter::new(&mut output); // First file let (mut entry1, config1) = archive .new_file("file1.txt") .last_modified(datetime1) .start() .unwrap(); let mut writer1 = config1.wrap(&mut entry1); writer1.write_all(b"File 1").unwrap(); let (_, descriptor1) = writer1.finish().unwrap(); entry1.finish(descriptor1).unwrap(); // Second file let (mut entry2, config2) = archive .new_file("file2.txt") .last_modified(datetime2) .start() .unwrap(); let mut writer2 = config2.wrap(&mut entry2); writer2.write_all(b"File 2").unwrap(); let (_, descriptor2) = writer2.finish().unwrap(); entry2.finish(descriptor2).unwrap(); archive.finish().unwrap(); } // Read back and verify timestamps let archive = ZipArchive::from_slice(&output).unwrap(); let entries: Vec<_> = archive.entries().collect(); assert_eq!(entries.len(), 2); // Find entries by name and check timestamps for entry in entries { let entry = entry.unwrap(); let file_path = entry.file_path(); let filename = file_path.try_normalize().unwrap(); match filename.as_ref() { "file1.txt" => { assert_eq!(entry.last_modified(), ZipDateTimeKind::Utc(datetime1)); } "file2.txt" => { // Since we now require UTC timestamps, the result should be identical assert_eq!(entry.last_modified(), ZipDateTimeKind::Utc(datetime2)); } name => panic!("Unexpected file: {}", name), } } } #[test] fn test_new_dir_with_options() { let datetime = UtcDateTime::from_components(2023, 12, 25, 12, 0, 0, 0).unwrap(); let mut output = Vec::new(); // Create archive with directory using options { let mut archive = ZipArchiveWriter::new(&mut output); // This should compile and work (breaking change) archive .new_dir("christmas/") .last_modified(datetime) .create() .unwrap(); archive.finish().unwrap(); } // Verify the directory was created with the correct timestamp let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "christmas/" ); assert!(entry.is_dir()); assert_eq!(entry.last_modified(), ZipDateTimeKind::Utc(datetime)); } /// Test compile-time timezone API and date validation #[test] fn test_timezone_api_and_validation() { // Create UTC timestamp with validation let utc_time = UtcDateTime::from_components(2023, 6, 15, 14, 30, 45, 0).unwrap(); let local_time = LocalDateTime::from_components(2023, 6, 15, 14, 30, 45, 0).unwrap(); // Verify timestamp properties assert_eq!(utc_time.year(), 2023); assert_eq!(utc_time.month(), 6); assert_eq!(utc_time.day(), 15); assert_eq!(utc_time.hour(), 14); assert_eq!(utc_time.minute(), 30); assert_eq!(utc_time.second(), 45); assert_eq!(utc_time.nanosecond(), 0); // Verify timezone types work assert_eq!(utc_time.timezone(), rawzip::time::TimeZone::Utc); assert_eq!(local_time.timezone(), rawzip::time::TimeZone::Local); // Test that only UTC timestamps can be used for last_modified let mut output = Vec::new(); let mut archive = ZipArchiveWriter::new(&mut output); let _builder = archive.new_file("test.txt").last_modified(utc_time); // Test date validation assert!(UtcDateTime::from_components(2023, 2, 30, 0, 0, 0, 0).is_none()); // Feb 30th assert!(LocalDateTime::from_components(2023, 13, 1, 0, 0, 0, 0).is_none()); // 13th month assert!(UtcDateTime::from_components(2023, 4, 31, 0, 0, 0, 0).is_none()); // April 31st // Test leap year validation assert!(UtcDateTime::from_components(2020, 2, 29, 0, 0, 0, 0).is_some()); // 2020 is leap year assert!(UtcDateTime::from_components(2021, 2, 29, 0, 0, 0, 0).is_none()); // 2021 is not leap year } /// Test ZipDateTimeKind functionality and timezone handling #[test] fn test_parsed_datetime_functionality() { // UTC timestamps can be used for Extended Timestamp writing let utc_dt = UtcDateTime::from_components(2023, 6, 15, 14, 30, 45, 0).unwrap(); // Local timestamps are for reading legacy ZIP files let local_dt = LocalDateTime::from_components(1995, 1, 1, 12, 0, 0, 0).unwrap(); // ZipDateTimeKind can represent either let parsed_utc = ZipDateTimeKind::Utc(utc_dt); let parsed_local = ZipDateTimeKind::Local(local_dt); // Both can be queried uniformly assert_eq!(parsed_utc.year(), 2023); assert_eq!(parsed_local.year(), 1995); assert_eq!(parsed_utc.timezone(), rawzip::time::TimeZone::Utc); assert_eq!(parsed_local.timezone(), rawzip::time::TimeZone::Local); } /// Test demonstrating when the local header contains richer timestamp data #[test] fn test_infozip_extended_timestamps() { let file = std::fs::File::open("assets/time-infozip.zip").expect("Failed to open time-infozip.zip"); let mut buffer = vec![0u8; rawzip::RECOMMENDED_BUFFER_SIZE]; let archive = ZipArchive::from_file(file, &mut buffer).expect("Failed to create ZipArchive"); let mut entries = archive.entries(&mut buffer); let entry = entries.next_entry().unwrap().unwrap(); // Get local header extra fields let wayfinder = entry.wayfinder(); let zip_entry = archive.get_entry(wayfinder).unwrap(); let mut local_buffer = vec![0u8; 256]; let local_header = zip_entry.local_header(&mut local_buffer).unwrap(); assert_extend_time_extra_field_difference(entry.extra_fields(), local_header.extra_fields()); } /// Test demonstrating when the local header contains richer timestamp data using ZipSliceArchive #[test] fn test_infozip_extended_timestamps_slice() { let archive_data = std::fs::read("assets/time-infozip.zip").expect("Failed to read time-infozip.zip"); let archive = rawzip::ZipArchive::from_slice(&archive_data).expect("Failed to create ZipSliceArchive"); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); // Get local header extra fields let wayfinder = entry.wayfinder(); let zip_entry = archive.get_entry(wayfinder).unwrap(); let local_fields = zip_entry.extra_fields(); assert_extend_time_extra_field_difference(entry.extra_fields(), local_fields); } fn assert_extend_time_extra_field_difference(mut central: ExtraFields, mut local: ExtraFields) { let central_et = central .find(|(id, _)| *id == ExtraFieldId::EXTENDED_TIMESTAMP) .expect("Central directory should have Extended Timestamp field"); let local_et = local .find(|(id, _)| *id == ExtraFieldId::EXTENDED_TIMESTAMP) .expect("Local header should have Extended Timestamp field"); let (_, central_data) = central_et; let (_, local_data) = local_et; assert_eq!( central_data.len(), 5, "Central directory should have 5 bytes (mod time only)" ); assert_eq!(local_data.len(), 9, "Local header should have 9 bytes (mod + access times) and have richer timestamp data than central directory"); } rawzip-0.4.4/tests/it/permission_tests.rs000064400000000000000000000072071046102023000167110ustar 00000000000000use rawzip::{ZipArchive, ZipArchiveWriter}; use std::io::Write; #[test] fn test_unix_permissions_roundtrip() { let test_cases = vec![ (0o644, 0o100644, "Regular file (644)"), (0o755, 0o100755, "Executable file (755)"), (0o600, 0o100600, "Owner-only file (600)"), (0o777, 0o100777, "World-writable file (777)"), (0o040755, 0o040755, "Directory (040755)"), (0o100644, 0o100644, "Regular file with type (100644)"), (0o120777, 0o120777, "Symbolic link (120777)"), ]; for (permissions, expected_mode, description) in test_cases { let mut output = Vec::new(); // Write archive with permissions { let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive .new_file("test_file.txt") .unix_permissions(permissions) .start() .unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"test content").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Read archive and verify permissions let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "test_file.txt" ); let actual_mode = entry.mode().value(); assert_eq!( actual_mode, expected_mode, "{}: expected permissions 0o{:o}, got 0o{:o}", description, expected_mode, actual_mode ); } } #[test] fn test_directory_permissions_roundtrip() { let mut output = Vec::new(); // Write archive with directory { let mut archive = ZipArchiveWriter::new(&mut output); archive .new_dir("test_dir/") .unix_permissions(0o040755) .create() .unwrap(); archive.finish().unwrap(); } // Read archive and verify directory permissions let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); assert_eq!( entry.file_path().try_normalize().unwrap().as_ref(), "test_dir/" ); assert!(entry.is_dir()); let actual_mode = entry.mode().value(); assert_eq!( actual_mode, 0o040755, "Directory permissions: expected 0o040755, got 0o{:o}", actual_mode ); } #[test] fn test_permissions_without_unix_permissions() { let mut output = Vec::new(); // Write archive without explicit permissions { let mut archive = ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive.new_file("test_file.txt").start().unwrap(); // No unix_permissions set let mut writer = config.wrap(&mut entry); writer.write_all(b"test content").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Read archive and verify default behavior let archive = ZipArchive::from_slice(&output).unwrap(); let mut entries = archive.entries(); let entry = entries.next_entry().unwrap().unwrap(); // When no unix permissions are set, we should get default permissions let actual_mode = entry.mode().value(); assert_eq!( actual_mode, 0o100666, "Default permissions: expected 0o100666, got 0o{:o}", actual_mode ); } rawzip-0.4.4/tests/it/utf8_tests.rs000064400000000000000000000061201046102023000154000ustar 00000000000000use rstest::rstest; use std::io::Write; /// Test filename UTF-8 flag behavior with various filenames #[rstest] #[case("file.txt", false)] #[case("MixedCase123.TXT", false)] #[case("with-dashes_and_underscores.txt", false)] #[case("🦀🔥_rust_file.txt", true)] #[case("テストファイル.txt", true)] #[case("café.txt", true)] #[case("file~backup.txt", true)] // Tilde character - UTF-8 flag (EUC-KR conflict) #[case("path\\file.txt", false)] #[case("normal-file_123.txt", false)] #[case("test|file.txt", false)] #[case("test}file.txt", false)] fn test_filename_utf8_flag(#[case] filename: &str, #[case] should_have_utf8_flag: bool) { let mut output = Vec::new(); { let mut archive = rawzip::ZipArchiveWriter::new(&mut output); let (mut entry, config) = archive.new_file(filename).start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"test content").unwrap(); let (_, descriptor) = writer.finish().unwrap(); entry.finish(descriptor).unwrap(); archive.finish().unwrap(); } // Parse the ZIP file to verify the UTF-8 flag is set correctly let flags = extract_flags_from_zip(&output); let utf8_flag_present = (flags & 0x800) != 0; assert_eq!( utf8_flag_present, should_have_utf8_flag, "UTF-8 flag mismatch for filename '{}': expected {}, got {}", filename, should_have_utf8_flag, utf8_flag_present ); } /// Test directory UTF-8 flag behavior with various directory names #[rstest] #[case("ascii_dir/", false)] #[case("🦀🔥/", true)] #[case("フォルダ/", true)] #[case("dossier/", false)] #[case("café_folder/", true)] #[case("file~backup/", true)] fn test_directory_utf8_flag(#[case] dirname: &str, #[case] should_have_utf8_flag: bool) { let mut output = Vec::new(); { let mut archive = rawzip::ZipArchiveWriter::new(&mut output); archive.new_dir(dirname).create().unwrap(); archive.finish().unwrap(); } // Parse the ZIP file to verify the UTF-8 flag is set correctly let flags = extract_flags_from_zip(&output); let utf8_flag_present = (flags & 0x800) != 0; assert_eq!( utf8_flag_present, should_have_utf8_flag, "UTF-8 flag mismatch for directory '{}': expected {}, got {}", dirname, should_have_utf8_flag, utf8_flag_present ); } /// Test the UTF-8 /// Helper function to extract the general purpose bit flags from the first local file header /// This is a simplified parser just for testing purposes fn extract_flags_from_zip(zip_data: &[u8]) -> u16 { // ZIP local file header structure: // 0-3: signature (0x04034b50) // 4-5: version needed // 6-7: general purpose bit flag <- this is what we want // 8-9: compression method // ... // Check for local file header signature let signature = u32::from_le_bytes([zip_data[0], zip_data[1], zip_data[2], zip_data[3]]); if signature != 0x04034b50 { panic!("Invalid local file header signature: 0x{:x}", signature); } // Extract general purpose bit flag (bytes 6-7) u16::from_le_bytes([zip_data[6], zip_data[7]]) } rawzip-0.4.4/tests/it/zip64_tests.rs000064400000000000000000000054771046102023000155040ustar 00000000000000use rawzip::{ZipArchive, ZipArchiveWriter, RECOMMENDED_BUFFER_SIZE}; use rstest::rstest; use std::io::{Cursor, Write}; // ZIP64 signatures to check for const ZIP64_EOCD_SIGNATURE: u32 = 0x06064b50; const ZIP64_EOCD_LOCATOR_SIGNATURE: u32 = 0x07064b50; /// Helper function to check if ZIP64 structures are present in the archive fn contains_zip64_signatures(data: &[u8]) -> bool { let zip64_eocd_sig_bytes = ZIP64_EOCD_SIGNATURE.to_le_bytes(); let zip64_locator_sig_bytes = ZIP64_EOCD_LOCATOR_SIGNATURE.to_le_bytes(); let has_eocd = data.windows(4).any(|w| w == zip64_eocd_sig_bytes); let has_locator = data.windows(4).any(|w| w == zip64_locator_sig_bytes); has_eocd && has_locator } fn verify_expected_entries(data: &[u8], expected_count: u64) { // Verify with slice let read_archive = ZipArchive::from_slice(data).unwrap(); assert_eq!(read_archive.entries_hint(), expected_count); let entries = read_archive.entries(); let mut count = 0; for _ in entries { count += 1; } assert_eq!(count, expected_count as usize); // Verify with reader let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE]; let read_archive = ZipArchive::from_seekable(Cursor::new(data), &mut buffer).unwrap(); assert_eq!(read_archive.entries_hint(), expected_count); let mut entries = read_archive.entries(&mut buffer); let mut count = 0; while entries.next_entry().unwrap().is_some() { count += 1; } assert_eq!(count, expected_count as usize); } /// Test ZIP64 threshold behavior with different entry counts #[rstest] #[case(65534, false)] #[case(65535, true)] #[case(65536, true)] fn test_zip64_threshold_entries(#[case] entry_count: usize, #[case] should_be_zip64: bool) { let output = Cursor::new(Vec::new()); let mut archive = ZipArchiveWriter::builder() .with_capacity(entry_count) .build(output); for i in 0..entry_count { let filename = format!("file_{:05}.txt", i); let (mut entry, config) = archive.new_file(&filename).start().unwrap(); let mut writer = config.wrap(&mut entry); writer.write_all(b"x").unwrap(); let (_, descriptor_output) = writer.finish().unwrap(); entry.finish(descriptor_output).unwrap(); } let writer = archive.finish().unwrap(); let data = writer.into_inner(); let archive_type = if should_be_zip64 { "ZIP64" } else { "standard ZIP" }; println!( "Created {} archive with {} entries", archive_type, entry_count ); // Verify ZIP64 signatures presence matches expectation let has_zip64 = contains_zip64_signatures(&data); assert_eq!( has_zip64, should_be_zip64, "{} entries expected zip64: {}", entry_count, should_be_zip64 ); verify_expected_entries(&data, entry_count as u64); }