diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5cde1657..ac237f1b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,11 @@ version: 2 updates: -- package-ecosystem: cargo - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 + - package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 35d4a6e9..38d2fcd4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,8 +3,6 @@ name: CI on: pull_request: push: - branches: - - master env: RUSTFLAGS: -Dwarnings @@ -16,7 +14,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - rust: [stable, 1.59.0] + rust: [stable, 1.66.0] steps: - uses: actions/checkout@master @@ -27,23 +25,29 @@ jobs: toolchain: ${{ matrix.rust }} override: true - - name: check + - name: Check uses: actions-rs/cargo@v1 with: command: check args: --all --bins --examples - - name: tests + - name: Tests uses: actions-rs/cargo@v1 with: command: test args: --all + - name: Tests (no features) + uses: actions-rs/cargo@v1 + with: + command: test + args: --all --no-default-features + clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: @@ -73,13 +77,13 @@ jobs: run: cargo fmt --all -- --check - name: Docs - run: cargo doc + run: cargo doc --no-deps - fuzz: + fuzz_read: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -90,3 +94,65 @@ jobs: - name: compile fuzz run: | cargo fuzz build fuzz_read + - name: run fuzz + run: | + cargo fuzz run fuzz_read -- -timeout=1s -jobs=100 -workers=2 -runs=1000000 -max_len=5000000000 + - name: Upload any failure inputs + if: always() + uses: actions/upload-artifact@v3 + with: + name: fuzz_read_bad_inputs + path: fuzz/artifacts/fuzz_read/crash-* + if-no-files-found: ignore + + fuzz_write: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + + - run: cargo install cargo-fuzz + - name: compile fuzz + run: | + cargo fuzz build fuzz_write + - name: run fuzz + run: | + cargo fuzz run fuzz_write -- -timeout=5s -jobs=100 -workers=2 -runs=10000 -max_len=5000000000 + - name: Upload any failure inputs + if: always() + uses: actions/upload-artifact@v3 + with: + name: fuzz_write_bad_inputs + path: fuzz/artifacts/fuzz_write/crash-* + if-no-files-found: ignore + + fuzz_write_with_no_features: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + + - run: cargo install cargo-fuzz + - name: compile fuzz + run: | + cargo fuzz build --no-default-features fuzz_write + - name: run fuzz + run: | + cargo fuzz run fuzz_write -- -timeout=5s -jobs=100 -workers=2 -runs=10000 -max_len=5000000000 + - name: Upload any failure inputs + if: always() + uses: actions/upload-artifact@v3 + with: + name: fuzz_write_bad_inputs + path: fuzz/artifacts/fuzz_write/crash-* + if-no-files-found: ignore \ No newline at end of file diff --git a/.github/workflows/dependabot_automation.yml b/.github/workflows/dependabot_automation.yml new file mode 100644 index 00000000..91ff8c9e --- /dev/null +++ b/.github/workflows/dependabot_automation.yml @@ -0,0 +1,27 @@ +name: Dependabot auto-approve and auto-merge +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.6.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Approve + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Enable auto-merge + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.whitesource b/.whitesource new file mode 100644 index 00000000..9c7ae90b --- /dev/null +++ b/.whitesource @@ -0,0 +1,14 @@ +{ + "scanSettings": { + "baseBranches": [] + }, + "checkRunSettings": { + "vulnerableCheckRunConclusionLevel": "failure", + "displayMode": "diff", + "useMendCheckNames": true + }, + "issueSettings": { + "minSeverityLevel": "LOW", + "issueType": "DEPENDENCY" + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c6994f..e8a652dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,230 @@ # Changelog -## [0.6.6] -### Changed - -- Updated `aes` dependency to `0.8.2` (https://github.com/zip-rs/zip/pull/354) - -## [0.6.5] -### Changed - -- Added experimental [`zip::unstable::write::FileOptions::with_deprecated_encryption`] API to enable encrypting files with PKWARE encryption. - ## [0.6.4] ### Changed - [#333](https://github.com/zip-rs/zip/pull/333): disabled the default features of the `time` dependency, and also `formatting` and `macros`, as they were enabled by mistake. - Deprecated [`DateTime::from_time`](https://docs.rs/zip/0.6/zip/struct.DateTime.html#method.from_time) in favor of [`DateTime::try_from`](https://docs.rs/zip/0.6/zip/struct.DateTime.html#impl-TryFrom-for-DateTime) - \ No newline at end of file + +## [0.6.5] + +### Added + + - `shallow_copy_file` method: copy a file from within the ZipWriter + +## [0.6.6] + +### Fixed + + - Unused flag `#![feature(read_buf)]` was breaking compatibility with stable compiler. + +### Changed + + - Updated dependency versions. + +## [0.6.7] + +### Added + + - `deep_copy_file` method: more standards-compliant way to copy a file from within the ZipWriter + +## [0.6.8] + +### Added + + - Detects duplicate filenames. + +### Fixed + + - `deep_copy_file` could set incorrect Unix permissions. + - `deep_copy_file` could handle files incorrectly if their compressed size was u32::MAX bytes or less but their + uncompressed size was not. + - Documented that `deep_copy_file` does not copy a directory's contents. + +### Changed + + - Improved performance of `deep_copy_file` by using a HashMap and eliminating a redundant search. + +## [0.6.9] + +### Fixed + + - Fixed an issue that prevented `ZipWriter` from implementing `Send`. + +## [0.6.10] + +### Changed + + - Updated dependency versions. + +## [0.6.11] + +### Fixed + + - Fixed a bug that could cause later writes to fail after a `deep_copy_file` call. + +## [0.6.12] + +### Fixed + + - Fixed a Clippy warning that was missed during the last release. + +## [0.6.13] + +### Fixed + + - Fixed a possible bug in deep_copy_file. + +## [0.7.0] + +### Fixed + + - Calling `start_file` with invalid parameters no longer closes the `ZipWriter`. + - Attempting to write a 4GiB file without calling `FileOptions::large_file(true)` now removes the file from the archive + but does not close the `ZipWriter`. + - Attempting to write a file with an unrepresentable or invalid last-modified date will instead add it with a date of + 1980-01-01 00:00:00. + +### Added + + - Method `is_writing_file` - indicates whether a file is open for writing. + +## [0.7.1] + +### Changed + + - Bumped the version number in order to upload an updated README to crates.io. + +## [0.7.2] + +### Added + + - Method `abort_file` - removes the current or most recently-finished file from the archive. + +### Fixed + + - Fixed a bug where a file could remain open for writing after validations failed. + +## [0.7.3] + +### Fixed + + - Fixed a bug that occurs when a filename in a ZIP32 file includes the ZIP64 magic bytes. + +## [0.7.4] + +### Merged from upstream + + - Added experimental [`zip_next::unstable::write::FileOptions::with_deprecated_encryption`] API to enable encrypting + files with PKWARE encryption. + +## [0.7.5] + +### Fixed + + - Fixed a bug that occurs when ZIP64 magic bytes occur twice in a filename or across two filenames. + +## [0.8.0] + +### Deleted + + - Methods `start_file_aligned`, `start_file_with_extra_data`, `end_local_start_central_extra_data` and + `end_extra_data` (see below). + +### Changed + + - Alignment and extra-data fields are now attributes of [`zip_next::unstable::write::FileOptions`], allowing them to be + specified for `add_directory` and `add_symlink`. + - Extra-data fields are now formatted by the `FileOptions` method `add_extra_data`. + - Improved performance, especially for `shallow_copy_file` and `deep_copy_file` on files with extra data. + +### Fixed + + - Fixes a rare bug where the size of the extra-data field could overflow when `large_file` was set. + - Fixes more cases of a bug when ZIP64 magic bytes occur in filenames. + +## [0.8.1] + +### Fixed + + - `ZipWriter` now once again implements `Send` if the underlying writer does. + +## [0.8.2] + +### Fixed + + - Fixed an issue where code might spuriously fail during write fuzzing. + +### Added + + - New method `with_alignment` on `FileOptions`. + +## [0.8.3] + +### Merged from upstream + + - Uses the `aes::cipher::KeyInit` trait from `aes` 0.8.2 where appropriate. + +### Fixed + + - Calling `abort_file()` no longer corrupts the archive if called on a + shallow copy of a remaining file, or on an archive whose CDR entries are out + of sequence. However, it may leave an unused entry in the archive. + - Calling `abort_file()` while writing a ZipCrypto-encrypted file no longer + causes a crash. + - Calling `abort_file()` on the last file before `finish()` no longer produces + an invalid ZIP file or garbage in the comment. + + ### Added + + - `ZipWriter` methods `get_comment()` and `get_raw_comment()`. + +## [0.9.0] + +### Added + + - `flush_on_finish_file` parameter for `ZipWriter`. + +## [0.9.1] + +### Added + + - Zopfli for aggressive Deflate compression. + +## [0.9.2] + +### Added + + - `zlib-ng` for fast Deflate compression. This is now the default for compression levels 0-9. + - `chrono` to convert zip_next::DateTime to and from chrono::NaiveDateTime + +## [0.10.0] + +### Changed + + - Replaces the `flush_on_finish_file` parameter of `ZipWriter::new` and `ZipWriter::Append` with + a `set_flush_on_finish_file` method. + +### Fixed + + - Fixes build errors that occur when all default features are disabled. + - Fixes more cases of a bug when ZIP64 magic bytes occur in filenames. + +## [0.10.1] + +### Changed + + - Date and time conversion methods now return `DateTimeRangeError` rather than `()` on error. + +## [0.10.2] + +### Changed + + - Where possible, methods are now `const`. This improves performance, especially when reading. + +## [0.10.3] + +### Changed + + - Updated dependencies. diff --git a/Cargo.toml b/Cargo.toml index 9dd4a103..8104b2b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,46 +1,55 @@ [package] -name = "zip" -version = "0.6.6" -authors = ["Mathijs van de Nes ", "Marli Frost ", "Ryan Levick "] +name = "zip_next" +version = "0.10.3" +authors = ["Mathijs van de Nes ", "Marli Frost ", "Ryan Levick ", +"Chris Hennick "] license = "MIT" -repository = "https://github.com/zip-rs/zip.git" +repository = "https://github.com/Pr0methean/zip-next.git" keywords = ["zip", "archive"] description = """ +rust-version = "1.66.0" Library to support the reading and writing of zip files. """ edition = "2021" -rust-version = "1.59.0" [dependencies] -aes = { version = "0.8.2", optional = true } +aes = { version = "0.8.3", optional = true } byteorder = "1.4.3" -bzip2 = { version = "0.4.3", optional = true } -constant_time_eq = { version = "0.1.5", optional = true } +bzip2 = { version = "0.4.4", optional = true } +chrono = { version = "0.4.26", optional = true } +constant_time_eq = { version = "0.3.0", optional = true } crc32fast = "1.3.2" -flate2 = { version = "1.0.23", default-features = false, optional = true } +flate2 = { version = "1.0.26", default-features = false, optional = true } hmac = { version = "0.12.1", optional = true, features = ["reset"] } -pbkdf2 = {version = "0.11.0", optional = true } -sha1 = {version = "0.10.1", optional = true } -time = { version = "0.3.7", optional = true, default-features = false, features = ["std"] } -zstd = { version = "0.11.2", optional = true } -deflate64 = { version = "0.1.4", optional = true } +pbkdf2 = {version = "0.12.1", optional = true } +sha1 = {version = "0.10.5", optional = true } +time = { version = "0.3.22", optional = true, default-features = false, features = ["std"] } +zstd = { version = "0.12.3", optional = true, default-features = false } +zopfli = { version = "0.7.4", optional = true } +deflate64 = { version = "0.1.5", optional = true } [target.'cfg(any(all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc"))'.dependencies] -crossbeam-utils = "0.8.8" +crossbeam-utils = "0.8.16" + +[target.'cfg(fuzzing)'.dependencies] +arbitrary = { version = "1.3.0", features = ["derive"] } [dev-dependencies] bencher = "0.1.5" -getrandom = "0.2.5" -walkdir = "2.3.2" -time = { version = "0.3.7", features = ["formatting", "macros"] } +getrandom = { version = "0.2.10", features = ["js"] } +walkdir = "2.3.3" +time = { version = "0.3.22", features = ["formatting", "macros"] } [features] aes-crypto = [ "aes", "constant_time_eq", "hmac", "pbkdf2", "sha1" ] +chrono = ["chrono/default"] deflate = ["flate2/rust_backend"] deflate-miniz = ["flate2/default"] deflate-zlib = ["flate2/zlib"] +deflate-zlib-ng = ["flate2/zlib-ng"] +deflate-zopfli = ["zopfli"] unreserved = [] -default = ["aes-crypto", "bzip2", "deflate", "time", "zstd"] +default = ["aes-crypto", "bzip2", "deflate", "deflate-zlib-ng", "deflate-zopfli", "time", "zstd"] [[bench]] name = "read_entry" diff --git a/README.md b/README.md index 8ed79612..d5a7325e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -zip-rs -====== +zip_next +======== -[![Build Status](https://img.shields.io/github/workflow/status/zip-rs/zip/CI)](https://github.com/zip-rs/zip/actions?query=branch%3Amaster+workflow%3ACI) -[![Crates.io version](https://img.shields.io/crates/v/zip.svg)](https://crates.io/crates/zip) -[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/rQ7H9cSsF4) +[![Build Status](https://github.com/Pr0methean/zip-next/actions/workflows/ci.yaml/badge.svg)](https://github.com/Pr0methean/zip-next/actions?query=branch%3Amaster+workflow%3ACI) +[![Crates.io version](https://img.shields.io/crates/v/zip_next.svg)](https://crates.io/crates/zip_next) -[Documentation](https://docs.rs/zip/0.6.3/zip/) +[Documentation](https://docs.rs/zip_next/0.10.1/zip_next/) Info ---- -A zip library for rust which supports reading and writing of simple ZIP files. +A zip library for rust which supports reading and writing of simple ZIP files. Forked from https://crates.io/crates/zip +to add more features and improve test coverage. Supported compression formats: @@ -33,31 +33,38 @@ With all default features: ```toml [dependencies] -zip = "0.6" +zip_next = "0.10.3" ``` Without the default features: ```toml [dependencies] -zip = { version = "0.6.6", default-features = false } +zip_next = { version = "0.10.3", default-features = false } ``` The features available are: * `aes-crypto`: Enables decryption of files which were encrypted with AES. Supports AE-1 and AE-2 methods. -* `deflate`: Enables the deflate compression algorithm, which is the default for zip files. +* `deflate`: Enables decompressing the deflate compression algorithm, which is the default for zip files. +* `deflate-miniz`: Enables deflating files with the `miniz_oxide` library (used when compression quality is 0..=9). +* `deflate-zlib`: Enables deflating files with the `zlib` library (used when compression quality is 0..=9). +* `deflate-zlib-ng`: Enables deflating files with the `zlib-ng` library (used when compression quality is 0..=9). + This is the fastest `deflate` implementation available. +* `deflate-zopfli`: Enables deflating files with the `zopfli` library (used when compression quality is 10..=264). This + is the most effective `deflate` implementation available. * `deflate64`: Enables the deflate64 compression algorithm. Decompression is only supported. * `bzip2`: Enables the BZip2 compression algorithm. * `time`: Enables features using the [time](https://github.com/rust-lang-deprecated/time) crate. +* `chrono`: Enables converting last-modified `zip_next::DateTime` to and from `chrono::NaiveDateTime`. * `zstd`: Enables the Zstandard compression algorithm. -All of these are enabled by default. +By default `aes-crypto`, `deflate`, `deflate-zlib-ng`, `deflate-zopfli`, `bzip2`, `time` and `zstd` are enabled. MSRV ---- -Our current Minimum Supported Rust Version is **1.59.0**. When adding features, +Our current Minimum Supported Rust Version is **1.66.0**. When adding features, we will follow these guidelines: - We will always support the latest four minor Rust versions. This gives you a 6 @@ -95,3 +102,9 @@ To start fuzzing zip extraction: ```bash cargo +nightly fuzz run fuzz_read ``` + +To start fuzzing zip creation: + +```bash +cargo +nightly fuzz run fuzz_write +``` diff --git a/benches/read_entry.rs b/benches/read_entry.rs index af9affe3..4ee20b02 100644 --- a/benches/read_entry.rs +++ b/benches/read_entry.rs @@ -4,13 +4,13 @@ use std::io::{Cursor, Read, Write}; use bencher::Bencher; use getrandom::getrandom; -use zip::{ZipArchive, ZipWriter}; +use zip_next::{ZipArchive, ZipWriter}; fn generate_random_archive(size: usize) -> Vec { let data = Vec::new(); let mut writer = ZipWriter::new(Cursor::new(data)); - let options = - zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + let options = zip_next::write::FileOptions::default() + .compression_method(zip_next::CompressionMethod::Stored); writer.start_file("random.dat", options).unwrap(); let mut bytes = vec![0u8; size]; diff --git a/benches/read_metadata.rs b/benches/read_metadata.rs index 95334b1c..f9be2ec3 100644 --- a/benches/read_metadata.rs +++ b/benches/read_metadata.rs @@ -3,7 +3,8 @@ use bencher::{benchmark_group, benchmark_main}; use std::io::{Cursor, Write}; use bencher::Bencher; -use zip::{ZipArchive, ZipWriter}; +use zip_next::write::FileOptions; +use zip_next::{CompressionMethod, ZipArchive, ZipWriter}; const FILE_COUNT: usize = 15_000; const FILE_SIZE: usize = 1024; @@ -11,14 +12,13 @@ const FILE_SIZE: usize = 1024; fn generate_random_archive(count_files: usize, file_size: usize) -> Vec { let data = Vec::new(); let mut writer = ZipWriter::new(Cursor::new(data)); - let options = - zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + let options = FileOptions::default().compression_method(CompressionMethod::Stored); let bytes = vec![0u8; file_size]; for i in 0..count_files { let name = format!("file_deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef_{i}.dat"); - writer.start_file(name, options).unwrap(); + writer.start_file(name, options.clone()).unwrap(); writer.write_all(&bytes).unwrap(); } diff --git a/examples/extract.rs b/examples/extract.rs index 30807162..e5bad04c 100644 --- a/examples/extract.rs +++ b/examples/extract.rs @@ -14,7 +14,7 @@ fn real_main() -> i32 { let fname = std::path::Path::new(&*args[1]); let file = fs::File::open(fname).unwrap(); - let mut archive = zip::ZipArchive::new(file).unwrap(); + let mut archive = zip_next::ZipArchive::new(file).unwrap(); for i in 0..archive.len() { let mut file = archive.by_index(i).unwrap(); diff --git a/examples/extract_lorem.rs b/examples/extract_lorem.rs index bc50abe1..0d1705c5 100644 --- a/examples/extract_lorem.rs +++ b/examples/extract_lorem.rs @@ -13,7 +13,7 @@ fn real_main() -> i32 { let fname = std::path::Path::new(&*args[1]); let zipfile = std::fs::File::open(fname).unwrap(); - let mut archive = zip::ZipArchive::new(zipfile).unwrap(); + let mut archive = zip_next::ZipArchive::new(zipfile).unwrap(); let mut file = match archive.by_name("test/lorem_ipsum.txt") { Ok(file) => file, diff --git a/examples/file_info.rs b/examples/file_info.rs index 6a2adc58..1730eb8d 100644 --- a/examples/file_info.rs +++ b/examples/file_info.rs @@ -15,7 +15,7 @@ fn real_main() -> i32 { let file = fs::File::open(fname).unwrap(); let reader = BufReader::new(file); - let mut archive = zip::ZipArchive::new(reader).unwrap(); + let mut archive = zip_next::ZipArchive::new(reader).unwrap(); for i in 0..archive.len() { let file = archive.by_index(i).unwrap(); diff --git a/examples/stdin_info.rs b/examples/stdin_info.rs index a609916a..f49d6e73 100644 --- a/examples/stdin_info.rs +++ b/examples/stdin_info.rs @@ -10,7 +10,7 @@ fn real_main() -> i32 { let mut buf = [0u8; 16]; loop { - match zip::read::read_zipfile_from_stream(&mut stdin_handle) { + match zip_next::read::read_zipfile_from_stream(&mut stdin_handle) { Ok(Some(mut file)) => { println!( "{}: {} bytes ({} bytes packed)", diff --git a/examples/write_dir.rs b/examples/write_dir.rs index 3b043528..6d92d161 100644 --- a/examples/write_dir.rs +++ b/examples/write_dir.rs @@ -1,8 +1,8 @@ use std::io::prelude::*; use std::io::{Seek, Write}; use std::iter::Iterator; -use zip::result::ZipError; -use zip::write::FileOptions; +use zip_next::result::ZipError; +use zip_next::write::FileOptions; use std::fs::File; use std::path::Path; @@ -12,30 +12,35 @@ fn main() { std::process::exit(real_main()); } -const METHOD_STORED: Option = Some(zip::CompressionMethod::Stored); +const METHOD_STORED: Option = + Some(zip_next::CompressionMethod::Stored); #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] -const METHOD_DEFLATED: Option = Some(zip::CompressionMethod::Deflated); +const METHOD_DEFLATED: Option = + Some(zip_next::CompressionMethod::Deflated); #[cfg(not(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" )))] -const METHOD_DEFLATED: Option = None; +const METHOD_DEFLATED: Option = None; #[cfg(feature = "bzip2")] -const METHOD_BZIP2: Option = Some(zip::CompressionMethod::Bzip2); +const METHOD_BZIP2: Option = Some(zip_next::CompressionMethod::Bzip2); #[cfg(not(feature = "bzip2"))] -const METHOD_BZIP2: Option = None; +const METHOD_BZIP2: Option = None; #[cfg(feature = "zstd")] -const METHOD_ZSTD: Option = Some(zip::CompressionMethod::Zstd); +const METHOD_ZSTD: Option = Some(zip_next::CompressionMethod::Zstd); #[cfg(not(feature = "zstd"))] -const METHOD_ZSTD: Option = None; +const METHOD_ZSTD: Option = None; fn real_main() -> i32 { let args: Vec<_> = std::env::args().collect(); @@ -66,12 +71,12 @@ fn zip_dir( it: &mut dyn Iterator, prefix: &str, writer: T, - method: zip::CompressionMethod, -) -> zip::result::ZipResult<()> + method: zip_next::CompressionMethod, +) -> zip_next::result::ZipResult<()> where T: Write + Seek, { - let mut zip = zip::ZipWriter::new(writer); + let mut zip = zip_next::ZipWriter::new(writer); let options = FileOptions::default() .compression_method(method) .unix_permissions(0o755); @@ -86,7 +91,7 @@ where if path.is_file() { println!("adding file {path:?} as {name:?} ..."); #[allow(deprecated)] - zip.start_file_from_path(name, options)?; + zip.start_file_from_path(name, options.clone())?; let mut f = File::open(path)?; f.read_to_end(&mut buffer)?; @@ -97,18 +102,18 @@ where // and mapname conversion failed error on unzip println!("adding dir {path:?} as {name:?} ..."); #[allow(deprecated)] - zip.add_directory_from_path(name, options)?; + zip.add_directory_from_path(name, options.clone())?; } } zip.finish()?; - Result::Ok(()) + Ok(()) } fn doit( src_dir: &str, dst_file: &str, - method: zip::CompressionMethod, -) -> zip::result::ZipResult<()> { + method: zip_next::CompressionMethod, +) -> zip_next::result::ZipResult<()> { if !Path::new(src_dir).is_dir() { return Err(ZipError::FileNotFound); } diff --git a/examples/write_sample.rs b/examples/write_sample.rs index 2e45cb1e..bb9739d0 100644 --- a/examples/write_sample.rs +++ b/examples/write_sample.rs @@ -1,5 +1,5 @@ use std::io::prelude::*; -use zip::write::FileOptions; +use zip_next::write::FileOptions; fn main() { std::process::exit(real_main()); @@ -21,16 +21,16 @@ fn real_main() -> i32 { 0 } -fn doit(filename: &str) -> zip::result::ZipResult<()> { +fn doit(filename: &str) -> zip_next::result::ZipResult<()> { let path = std::path::Path::new(filename); let file = std::fs::File::create(path).unwrap(); - let mut zip = zip::ZipWriter::new(file); + let mut zip = zip_next::ZipWriter::new(file); zip.add_directory("test/", Default::default())?; let options = FileOptions::default() - .compression_method(zip::CompressionMethod::Stored) + .compression_method(zip_next::CompressionMethod::Stored) .unix_permissions(0o755); zip.start_file("test/☃.txt", options)?; zip.write_all(b"Hello, World!\n")?; diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index bfdb764c..f63ff859 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,8 +10,9 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" +arbitrary = { version = "1.3.0", features = ["derive"] } -[dependencies.zip] +[dependencies.zip_next] path = ".." # Prevent this from interfering with workspaces @@ -23,3 +24,9 @@ name = "fuzz_read" path = "fuzz_targets/fuzz_read.rs" test = false doc = false + +[[bin]] +name = "fuzz_write" +path = "fuzz_targets/fuzz_write.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_read.rs b/fuzz/fuzz_targets/fuzz_read.rs index 97fbdd3b..359cf227 100644 --- a/fuzz/fuzz_targets/fuzz_read.rs +++ b/fuzz/fuzz_targets/fuzz_read.rs @@ -3,7 +3,7 @@ use libfuzzer_sys::fuzz_target; fn decompress_all(data: &[u8]) -> Result<(), Box> { let reader = std::io::Cursor::new(data); - let mut zip = zip::ZipArchive::new(reader)?; + let mut zip = zip_next::ZipArchive::new(reader)?; for i in 0..zip.len() { let mut file = zip.by_index(i)?; diff --git a/fuzz/fuzz_targets/fuzz_write.rs b/fuzz/fuzz_targets/fuzz_write.rs new file mode 100644 index 00000000..b09b7ef9 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_write.rs @@ -0,0 +1,104 @@ +#![no_main] + +use std::cell::RefCell; +use libfuzzer_sys::fuzz_target; +use arbitrary::Arbitrary; +use std::io::{Cursor, Read, Seek, Write}; +use std::path::{PathBuf}; + +#[derive(Arbitrary,Clone,Debug)] +pub enum BasicFileOperation { + WriteNormalFile { + contents: Vec>, + options: zip_next::write::FileOptions, + }, + WriteDirectory(zip_next::write::FileOptions), + WriteSymlinkWithTarget { + target: Box, + options: zip_next::write::FileOptions, + }, + ShallowCopy(Box), + DeepCopy(Box), +} + +#[derive(Arbitrary,Clone,Debug)] +pub struct FileOperation { + basic: BasicFileOperation, + name: String, + reopen: bool, + // 'abort' flag is separate, to prevent trying to copy an aborted file +} + +#[derive(Arbitrary,Clone,Debug)] +pub struct FuzzTestCase { + comment: Vec, + operations: Vec<(FileOperation, bool)>, + flush_on_finish_file: bool, +} + +impl FileOperation { + fn referenceable_name(&self) -> String { + if let BasicFileOperation::WriteDirectory(_) = self.basic { + if !self.name.ends_with('\\') && !self.name.ends_with('/') { + return self.name.to_owned() + "/"; + } + } + self.name.to_owned() + } +} + +fn do_operation(writer: &mut RefCell>, + operation: FileOperation, + abort: bool, flush_on_finish_file: bool) -> Result<(), Box> + where T: Read + Write + Seek { + writer.borrow_mut().set_flush_on_finish_file(flush_on_finish_file); + let name = operation.name; + match operation.basic { + BasicFileOperation::WriteNormalFile {contents, mut options, ..} => { + let uncompressed_size = contents.iter().map(Vec::len).sum::(); + if uncompressed_size >= u32::MAX as usize { + options = options.large_file(true); + } + writer.borrow_mut().start_file(name, options)?; + for chunk in contents { + writer.borrow_mut().write_all(chunk.as_slice())?; + } + } + BasicFileOperation::WriteDirectory(options) => { + writer.borrow_mut().add_directory(name, options)?; + } + BasicFileOperation::WriteSymlinkWithTarget {target, options} => { + writer.borrow_mut().add_symlink(name, target.to_string_lossy(), options)?; + } + BasicFileOperation::ShallowCopy(base) => { + let base_name = base.referenceable_name(); + do_operation(writer, *base, false, flush_on_finish_file)?; + writer.borrow_mut().shallow_copy_file(&base_name, &name)?; + } + BasicFileOperation::DeepCopy(base) => { + let base_name = base.referenceable_name(); + do_operation(writer, *base, false, flush_on_finish_file)?; + writer.borrow_mut().deep_copy_file(&base_name, &name)?; + } + } + if abort { + writer.borrow_mut().abort_file().unwrap(); + } + if operation.reopen { + let old_comment = writer.borrow().get_raw_comment().to_owned(); + let new_writer = zip_next::ZipWriter::new_append( + writer.borrow_mut().finish().unwrap()).unwrap(); + assert_eq!(&old_comment, new_writer.get_raw_comment()); + *writer = new_writer.into(); + } + Ok(()) +} + +fuzz_target!(|test_case: FuzzTestCase| { + let mut writer = RefCell::new(zip_next::ZipWriter::new(Cursor::new(Vec::new()))); + writer.borrow_mut().set_raw_comment(test_case.comment); + for (operation, abort) in test_case.operations { + let _ = do_operation(&mut writer, operation, abort, test_case.flush_on_finish_file); + } + let _ = zip_next::ZipArchive::new(writer.borrow_mut().finish().unwrap()); +}); \ No newline at end of file diff --git a/src/aes.rs b/src/aes.rs index 8997705c..55ea8bf3 100644 --- a/src/aes.rs +++ b/src/aes.rs @@ -9,7 +9,7 @@ use crate::types::AesMode; use constant_time_eq::constant_time_eq; use hmac::{Hmac, Mac}; use sha1::Sha1; -use std::io::{self, Read}; +use std::io::{self, Error, ErrorKind, Read}; /// The length of the password verifcation value in bytes const PWD_VERIFY_LENGTH: usize = 2; @@ -45,7 +45,7 @@ pub struct AesReader { } impl AesReader { - pub fn new(reader: R, aes_mode: AesMode, compressed_size: u64) -> AesReader { + pub const fn new(reader: R, aes_mode: AesMode, compressed_size: u64) -> AesReader { let data_length = compressed_size - (PWD_VERIFY_LENGTH + AUTH_CODE_LENGTH + aes_mode.salt_length()) as u64; @@ -84,7 +84,8 @@ impl AesReader { let mut derived_key: Vec = vec![0; derived_key_len]; // use PBKDF2 with HMAC-Sha1 to derive the key - pbkdf2::pbkdf2::>(password, &salt, ITERATION_COUNT, &mut derived_key); + pbkdf2::pbkdf2::>(password, &salt, ITERATION_COUNT, &mut derived_key) + .map_err(|e| Error::new(ErrorKind::InvalidInput, e))?; let decrypt_key = &derived_key[0..key_length]; let hmac_key = &derived_key[key_length..key_length * 2]; let pwd_verify = &derived_key[derived_key_len - 2..]; @@ -165,8 +166,8 @@ impl Read for AesReaderValid { // use constant time comparison to mitigate timing attacks if !constant_time_eq(computed_auth_code, &read_auth_code) { return Err( - io::Error::new( - io::ErrorKind::InvalidData, + Error::new( + ErrorKind::InvalidData, "Invalid authentication code, this could be due to an invalid password or errors in the data" ) ); diff --git a/src/aes_ctr.rs b/src/aes_ctr.rs index 211727c0..a1636b4b 100644 --- a/src/aes_ctr.rs +++ b/src/aes_ctr.rs @@ -5,7 +5,6 @@ //! See [AesCtrZipKeyStream] for more information. use aes::cipher::generic_array::GenericArray; -// use aes::{BlockEncrypt, NewBlockCipher}; use aes::cipher::{BlockEncrypt, KeyInit}; use byteorder::WriteBytesExt; use std::{any, fmt}; @@ -28,7 +27,7 @@ pub trait AesKind { /// Key type. type Key: AsRef<[u8]>; /// Cipher used to decrypt. - type Cipher; + type Cipher: KeyInit; } impl AesKind for Aes128 { diff --git a/src/compression.rs b/src/compression.rs index 5b75380e..5352c487 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -11,6 +11,7 @@ use std::fmt; /// When creating ZIP files, you may choose the method to use with /// [`crate::write::FileOptions::compression_method`] #[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] #[non_exhaustive] pub enum CompressionMethod { /// Store the file as is @@ -19,7 +20,9 @@ pub enum CompressionMethod { #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" ))] Deflated, /// Compress the file using Deflate64. @@ -39,7 +42,10 @@ pub enum CompressionMethod { #[cfg(feature = "zstd")] Zstd, /// Unsupported compression method - #[deprecated(since = "0.5.7", note = "use the constants instead")] + #[cfg_attr( + not(fuzzing), + deprecated(since = "0.5.7", note = "use the constants instead") + )] Unsupported(u16), } #[allow(deprecated, missing_docs)] @@ -55,13 +61,17 @@ impl CompressionMethod { #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" ))] pub const DEFLATE: Self = CompressionMethod::Deflated; #[cfg(not(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" )))] pub const DEFLATE: Self = CompressionMethod::Unsupported(8); #[cfg(feature = "deflate64")] @@ -97,14 +107,16 @@ impl CompressionMethod { since = "0.5.7", note = "use a constant to construct a compression method" )] - pub fn from_u16(val: u16) -> CompressionMethod { + pub const fn from_u16(val: u16) -> CompressionMethod { #[allow(deprecated)] match val { 0 => CompressionMethod::Stored, #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" ))] 8 => CompressionMethod::Deflated, #[cfg(feature = "deflate64")] @@ -125,14 +137,16 @@ impl CompressionMethod { since = "0.5.7", note = "to match on other compression methods, use a constant" )] - pub fn to_u16(self) -> u16 { + pub const fn to_u16(self) -> u16 { #[allow(deprecated)] match self { CompressionMethod::Stored => 0, #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" ))] CompressionMethod::Deflated => 8, #[cfg(feature = "deflate64")] @@ -149,6 +163,55 @@ impl CompressionMethod { } } +impl Default for CompressionMethod { + fn default() -> Self { + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" + ))] + return CompressionMethod::Deflated; + + #[cfg(all( + not(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" + )), + feature = "bzip2" + ))] + return CompressionMethod::Bzip2; + + #[cfg(all( + not(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli", + feature = "bzip2" + )), + feature = "zstd" + ))] + return CompressionMethod::Zstd; + + #[cfg(not(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli", + feature = "bzip2", + feature = "zstd" + )))] + return CompressionMethod::Stored; + } +} + impl fmt::Display for CompressionMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Just duplicate what the Debug format looks like, i.e, the enum key: @@ -162,7 +225,9 @@ pub const SUPPORTED_COMPRESSION_METHODS: &[CompressionMethod] = &[ #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" ))] CompressionMethod::Deflated, #[cfg(feature = "deflate64")] diff --git a/src/lib.rs b/src/lib.rs index e2228e5b..27771a21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,6 @@ //! //! //! - #![warn(missing_docs)] pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; @@ -50,6 +49,6 @@ mod zipcrypto; /// /// ```toml /// [dependencies] -/// zip = "=0.6.6" +/// zip_next = "=0.10.3" /// ``` pub mod unstable; diff --git a/src/read.rs b/src/read.rs index c94e5254..5f8484fd 100644 --- a/src/read.rs +++ b/src/read.rs @@ -19,7 +19,8 @@ use std::sync::Arc; #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] use flate2::read::DeflateDecoder; @@ -54,13 +55,13 @@ pub(crate) mod zip_archive { /// /// ```no_run /// use std::io::prelude::*; - /// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> { - /// let mut zip = zip::ZipArchive::new(reader)?; + /// fn list_zip_contents(reader: impl Read + Seek) -> zip_next::result::ZipResult<()> { + /// let mut zip = zip_next::ZipArchive::new(reader)?; /// /// for i in 0..zip.len() { /// let mut file = zip.by_index(i)?; /// println!("Filename: {}", file.name()); - /// std::io::copy(&mut file, &mut std::io::stdout()); + /// std::io::copy(&mut file, &mut std::io::stdout())?; /// } /// /// Ok(()) @@ -74,8 +75,9 @@ pub(crate) mod zip_archive { } pub use zip_archive::ZipArchive; + #[allow(clippy::large_enum_variant)] -enum CryptoReader<'a> { +pub(crate) enum CryptoReader<'a> { Plaintext(io::Take<&'a mut dyn Read>), ZipCrypto(ZipCryptoReaderValid>), #[cfg(feature = "aes-crypto")] @@ -108,7 +110,7 @@ impl<'a> CryptoReader<'a> { } /// Returns `true` if the data is encrypted using AE2. - pub fn is_ae2_encrypted(&self) -> bool { + pub const fn is_ae2_encrypted(&self) -> bool { #[cfg(feature = "aes-crypto")] return matches!( self, @@ -122,16 +124,17 @@ impl<'a> CryptoReader<'a> { } } -enum ZipFileReader<'a> { +pub(crate) enum ZipFileReader<'a> { NoReader, - Raw(io::Take<&'a mut dyn io::Read>), + Raw(io::Take<&'a mut dyn Read>), Stored(Crc32Reader>), #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] - Deflated(Crc32Reader>>), + Deflated(Crc32Reader>>), #[cfg(feature = "deflate64")] Deflate64(Crc32Reader>>>), #[cfg(feature = "bzip2")] @@ -149,7 +152,8 @@ impl<'a> Read for ZipFileReader<'a> { #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] ZipFileReader::Deflated(r) => r.read(buf), #[cfg(feature = "deflate64")] @@ -172,7 +176,8 @@ impl<'a> ZipFileReader<'a> { #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] ZipFileReader::Deflated(r) => r.into_inner().into_inner().into_inner(), #[cfg(feature = "deflate64")] @@ -187,12 +192,12 @@ impl<'a> ZipFileReader<'a> { /// A struct for reading a zip file pub struct ZipFile<'a> { - data: Cow<'a, ZipFileData>, - crypto_reader: Option>, - reader: ZipFileReader<'a>, + pub(crate) data: Cow<'a, ZipFileData>, + pub(crate) crypto_reader: Option>, + pub(crate) reader: ZipFileReader<'a>, } -fn find_content<'a>( +pub(crate) fn find_content<'a>( data: &ZipFileData, reader: &'a mut (impl Read + Seek), ) -> ZipResult> { @@ -215,12 +220,12 @@ fn find_content<'a>( } #[allow(clippy::too_many_arguments)] -fn make_crypto_reader<'a>( - compression_method: crate::compression::CompressionMethod, +pub(crate) fn make_crypto_reader<'a>( + compression_method: CompressionMethod, crc32: u32, last_modified_time: DateTime, using_data_descriptor: bool, - reader: io::Take<&'a mut dyn io::Read>, + reader: io::Take<&'a mut dyn Read>, password: Option<&[u8]>, aes_info: Option<(AesMode, AesVendorVersion)>, #[cfg(feature = "aes-crypto")] compressed_size: u64, @@ -266,7 +271,7 @@ fn make_crypto_reader<'a>( Ok(Ok(reader)) } -fn make_reader( +pub(crate) fn make_reader( compression_method: CompressionMethod, crc32: u32, reader: CryptoReader, @@ -280,7 +285,8 @@ fn make_reader( #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] CompressionMethod::Deflated => { let deflate_reader = DeflateDecoder::new(reader); @@ -305,107 +311,153 @@ fn make_reader( } } -impl ZipArchive { +pub(crate) struct DirectoryCounts { + pub(crate) archive_offset: u64, + pub(crate) directory_start: u64, + pub(crate) number_of_files: usize, + pub(crate) disk_number: u32, + pub(crate) disk_with_central_directory: u32, +} + +impl ZipArchive { + fn get_directory_counts_zip32( + footer: &spec::CentralDirectoryEnd, + cde_start_pos: u64, + ) -> ZipResult { + // Some zip files have data prepended to them, resulting in the + // offsets all being too small. Get the amount of error by comparing + // the actual file position we found the CDE at with the offset + // recorded in the CDE. + let archive_offset = cde_start_pos + .checked_sub(footer.central_directory_size as u64) + .and_then(|x| x.checked_sub(footer.central_directory_offset as u64)) + .ok_or(ZipError::InvalidArchive( + "Invalid central directory size or offset", + ))?; + + let directory_start = footer.central_directory_offset as u64 + archive_offset; + let number_of_files = footer.number_of_files_on_this_disk as usize; + Ok(DirectoryCounts { + archive_offset, + directory_start, + number_of_files, + disk_number: footer.disk_number as u32, + disk_with_central_directory: footer.disk_with_central_directory as u32, + }) + } + + fn get_directory_counts_zip64( + reader: &mut R, + footer: &spec::CentralDirectoryEnd, + cde_start_pos: u64, + ) -> ZipResult { + // See if there's a ZIP64 footer. The ZIP64 locator if present will + // have its signature 20 bytes in front of the standard footer. The + // standard footer, in turn, is 22+N bytes large, where N is the + // comment length. Therefore: + reader.seek(io::SeekFrom::End( + -(20 + 22 + footer.zip_file_comment.len() as i64), + ))?; + let locator64 = spec::Zip64CentralDirectoryEndLocator::parse(reader)?; + + // We need to reassess `archive_offset`. We know where the ZIP64 + // central-directory-end structure *should* be, but unfortunately we + // don't know how to precisely relate that location to our current + // actual offset in the file, since there may be junk at its + // beginning. Therefore we need to perform another search, as in + // read::CentralDirectoryEnd::find_and_parse, except now we search + // forward. + + let search_upper_bound = cde_start_pos + .checked_sub(60) // minimum size of Zip64CentralDirectoryEnd + Zip64CentralDirectoryEndLocator + .ok_or(ZipError::InvalidArchive( + "File cannot contain ZIP64 central directory end", + ))?; + let (footer64, archive_offset) = spec::Zip64CentralDirectoryEnd::find_and_parse( + reader, + locator64.end_of_central_directory_offset, + search_upper_bound, + )?; + + let directory_start = footer64 + .central_directory_offset + .checked_add(archive_offset) + .ok_or(ZipError::InvalidArchive( + "Invalid central directory size or offset", + ))?; + if directory_start > search_upper_bound { + return Err(ZipError::InvalidArchive( + "Invalid central directory size or offset", + )); + } + if footer64.number_of_files_on_this_disk > footer64.number_of_files { + return Err(ZipError::InvalidArchive( + "ZIP64 footer indicates more files on this disk than in the whole archive", + )); + } + if footer64.version_needed_to_extract > footer64.version_made_by { + return Err(ZipError::InvalidArchive( + "ZIP64 footer indicates a new version is needed to extract this archive than the \ + version that wrote it", + )); + } + + Ok(DirectoryCounts { + archive_offset, + directory_start, + number_of_files: footer64.number_of_files as usize, + disk_number: footer64.disk_number, + disk_with_central_directory: footer64.disk_with_central_directory, + }) + } + /// Get the directory start offset and number of files. This is done in a /// separate function to ease the control flow design. pub(crate) fn get_directory_counts( reader: &mut R, footer: &spec::CentralDirectoryEnd, cde_start_pos: u64, - ) -> ZipResult<(u64, u64, usize)> { - // See if there's a ZIP64 footer. The ZIP64 locator if present will - // have its signature 20 bytes in front of the standard footer. The - // standard footer, in turn, is 22+N bytes large, where N is the - // comment length. Therefore: - let zip64locator = if reader - .seek(io::SeekFrom::End( - -(20 + 22 + footer.zip_file_comment.len() as i64), - )) - .is_ok() - { - match spec::Zip64CentralDirectoryEndLocator::parse(reader) { - Ok(loc) => Some(loc), - Err(ZipError::InvalidArchive(_)) => { - // No ZIP64 header; that's actually fine. We're done here. - None + ) -> ZipResult { + // Check if file has a zip64 footer + let counts_64 = Self::get_directory_counts_zip64(reader, footer, cde_start_pos); + let counts_32 = Self::get_directory_counts_zip32(footer, cde_start_pos); + match counts_64 { + Err(_) => match counts_32 { + Err(e) => Err(e), + Ok(counts) => { + if counts.disk_number != counts.disk_with_central_directory { + return unsupported_zip_error( + "Support for multi-disk files is not implemented", + ); + } + Ok(counts) } - Err(e) => { - // Yikes, a real problem - return Err(e); + }, + Ok(counts_64) => { + match counts_32 { + Err(_) => Ok(counts_64), + Ok(counts_32) => { + // Both zip32 and zip64 footers exist, so check if the zip64 footer is valid; if not, try zip32 + if counts_64.number_of_files != counts_32.number_of_files + && counts_32.number_of_files != u16::MAX as usize + { + return Ok(counts_32); + } + if counts_64.disk_number != counts_32.disk_number + && counts_32.disk_number != u16::MAX as u32 + { + return Ok(counts_32); + } + if counts_64.disk_with_central_directory + != counts_32.disk_with_central_directory + && counts_32.disk_with_central_directory != u16::MAX as u32 + { + return Ok(counts_32); + } + Ok(counts_64) + } } } - } else { - // Empty Zip files will have nothing else so this error might be fine. If - // not, we'll find out soon. - None - }; - - match zip64locator { - None => { - // Some zip files have data prepended to them, resulting in the - // offsets all being too small. Get the amount of error by comparing - // the actual file position we found the CDE at with the offset - // recorded in the CDE. - let archive_offset = cde_start_pos - .checked_sub(footer.central_directory_size as u64) - .and_then(|x| x.checked_sub(footer.central_directory_offset as u64)) - .ok_or(ZipError::InvalidArchive( - "Invalid central directory size or offset", - ))?; - - let directory_start = footer.central_directory_offset as u64 + archive_offset; - let number_of_files = footer.number_of_files_on_this_disk as usize; - Ok((archive_offset, directory_start, number_of_files)) - } - Some(locator64) => { - // If we got here, this is indeed a ZIP64 file. - - if !footer.record_too_small() - && footer.disk_number as u32 != locator64.disk_with_central_directory - { - return unsupported_zip_error( - "Support for multi-disk files is not implemented", - ); - } - - // We need to reassess `archive_offset`. We know where the ZIP64 - // central-directory-end structure *should* be, but unfortunately we - // don't know how to precisely relate that location to our current - // actual offset in the file, since there may be junk at its - // beginning. Therefore we need to perform another search, as in - // read::CentralDirectoryEnd::find_and_parse, except now we search - // forward. - - let search_upper_bound = cde_start_pos - .checked_sub(60) // minimum size of Zip64CentralDirectoryEnd + Zip64CentralDirectoryEndLocator - .ok_or(ZipError::InvalidArchive( - "File cannot contain ZIP64 central directory end", - ))?; - let (footer, archive_offset) = spec::Zip64CentralDirectoryEnd::find_and_parse( - reader, - locator64.end_of_central_directory_offset, - search_upper_bound, - )?; - - if footer.disk_number != footer.disk_with_central_directory { - return unsupported_zip_error( - "Support for multi-disk files is not implemented", - ); - } - - let directory_start = footer - .central_directory_offset - .checked_add(archive_offset) - .ok_or({ - ZipError::InvalidArchive("Invalid central directory size or offset") - })?; - - Ok(( - archive_offset, - directory_start, - footer.number_of_files as usize, - )) - } } } @@ -415,32 +467,34 @@ impl ZipArchive { pub fn new(mut reader: R) -> ZipResult> { let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut reader)?; - if !footer.record_too_small() && footer.disk_number != footer.disk_with_central_directory { + let counts = Self::get_directory_counts(&mut reader, &footer, cde_start_pos)?; + + if counts.disk_number != counts.disk_with_central_directory { return unsupported_zip_error("Support for multi-disk files is not implemented"); } - let (archive_offset, directory_start, number_of_files) = - Self::get_directory_counts(&mut reader, &footer, cde_start_pos)?; - // If the parsed number of files is greater than the offset then // something fishy is going on and we shouldn't trust number_of_files. - let file_capacity = if number_of_files > cde_start_pos as usize { + let file_capacity = if counts.number_of_files > cde_start_pos as usize { 0 } else { - number_of_files + counts.number_of_files }; let mut files = Vec::with_capacity(file_capacity); let mut names_map = HashMap::with_capacity(file_capacity); - if reader.seek(io::SeekFrom::Start(directory_start)).is_err() { + if reader + .seek(io::SeekFrom::Start(counts.directory_start)) + .is_err() + { return Err(ZipError::InvalidArchive( "Could not seek to start of central directory", )); } - for _ in 0..number_of_files { - let file = central_header_to_zip_file(&mut reader, archive_offset)?; + for _ in 0..counts.number_of_files { + let file = central_header_to_zip_file(&mut reader, counts.archive_offset)?; names_map.insert(file.file_name.clone(), files.len()); files.push(file); } @@ -448,7 +502,7 @@ impl ZipArchive { let shared = Arc::new(zip_archive::Shared { files, names_map, - offset: archive_offset, + offset: counts.archive_offset, comment: footer.zip_file_comment, }); @@ -543,7 +597,7 @@ impl ZipArchive { } /// Search for a file entry by name - pub fn by_name<'a>(&'a mut self, name: &str) -> ZipResult> { + pub fn by_name(&mut self, name: &str) -> ZipResult { Ok(self.by_name_with_optional_password(name, None)?.unwrap()) } @@ -574,11 +628,11 @@ impl ZipArchive { /// There are many passwords out there that will also pass the validity checks /// we are able to perform. This is a weakness of the ZipCrypto algorithm, /// due to its fairly primitive approach to cryptography. - pub fn by_index_decrypt<'a>( - &'a mut self, + pub fn by_index_decrypt( + &mut self, file_number: usize, password: &[u8], - ) -> ZipResult, InvalidPassword>> { + ) -> ZipResult> { self.by_index_with_optional_password(file_number, Some(password)) } @@ -605,11 +659,11 @@ impl ZipArchive { }) } - fn by_index_with_optional_password<'a>( - &'a mut self, + fn by_index_with_optional_password( + &mut self, file_number: usize, mut password: Option<&[u8]>, - ) -> ZipResult, InvalidPassword>> { + ) -> ZipResult> { let data = self .shared .files @@ -652,12 +706,12 @@ impl ZipArchive { } } -fn unsupported_zip_error(detail: &'static str) -> ZipResult { +const fn unsupported_zip_error(detail: &'static str) -> ZipResult { Err(ZipError::UnsupportedArchive(detail)) } /// Parse a central directory entry to collect the information for the file. -pub(crate) fn central_header_to_zip_file( +pub(crate) fn central_header_to_zip_file( reader: &mut R, archive_offset: u64, ) -> ZipResult { @@ -730,7 +784,8 @@ fn central_header_to_zip_file_inner( uncompressed_size: uncompressed_size as u64, file_name, file_name_raw, - extra_field, + extra_field: Arc::new(extra_field), + central_extra_field: Arc::new(vec![]), file_comment, header_start: offset, central_header_start, @@ -762,7 +817,7 @@ fn central_header_to_zip_file_inner( } fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> { - let mut reader = io::Cursor::new(&file.extra_field); + let mut reader = io::Cursor::new(file.extra_field.as_ref()); while (reader.position() as usize) < file.extra_field.len() { let kind = reader.read_u16::()?; @@ -887,7 +942,7 @@ impl<'a> ZipFile<'a> { note = "by stripping `..`s from the path, the meaning of paths can change. `mangled_name` can be used if this behaviour is desirable" )] - pub fn sanitized_name(&self) -> ::std::path::PathBuf { + pub fn sanitized_name(&self) -> std::path::PathBuf { self.mangled_name() } @@ -903,7 +958,7 @@ impl<'a> ZipFile<'a> { /// [`ZipFile::enclosed_name`] is the better option in most scenarios. /// /// [`ParentDir`]: `Component::ParentDir` - pub fn mangled_name(&self) -> ::std::path::PathBuf { + pub fn mangled_name(&self) -> std::path::PathBuf { self.data.file_name_sanitized() } @@ -949,8 +1004,7 @@ impl<'a> ZipFile<'a> { pub fn is_dir(&self) -> bool { self.name() .chars() - .rev() - .next() + .next_back() .map_or(false, |c| c == '/' || c == '\\') } @@ -1003,13 +1057,13 @@ impl<'a> Drop for ZipFile<'a> { let mut buffer = [0; 1 << 16]; // Get the inner `Take` reader so all decryption, decompression and CRC calculation is skipped. - let mut reader: std::io::Take<&mut dyn std::io::Read> = match &mut self.reader { + let mut reader: io::Take<&mut dyn Read> = match &mut self.reader { ZipFileReader::NoReader => { - let innerreader = ::std::mem::replace(&mut self.crypto_reader, None); + let innerreader = self.crypto_reader.take(); innerreader.expect("Invalid reader state").into_inner() } reader => { - let innerreader = ::std::mem::replace(reader, ZipFileReader::NoReader); + let innerreader = std::mem::replace(reader, ZipFileReader::NoReader); innerreader.into_inner() } }; @@ -1043,9 +1097,7 @@ impl<'a> Drop for ZipFile<'a> { /// * `comment`: set to an empty string /// * `data_start`: set to 0 /// * `external_attributes`: `unix_mode()`: will return None -pub fn read_zipfile_from_stream<'a, R: io::Read>( - reader: &'a mut R, -) -> ZipResult>> { +pub fn read_zipfile_from_stream<'a, R: Read>(reader: &'a mut R) -> ZipResult>> { let signature = reader.read_u32::()?; match signature { @@ -1092,7 +1144,8 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( uncompressed_size: uncompressed_size as u64, file_name, file_name_raw, - extra_field, + extra_field: Arc::new(extra_field), + central_extra_field: Arc::new(vec![]), file_comment: String::new(), // file comment is only available in the central directory // header_start and data start are not available, but also don't matter, since seeking is // not available. @@ -1119,7 +1172,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( return unsupported_zip_error("The file length is not available in the local header"); } - let limit_reader = (reader as &'a mut dyn io::Read).take(result.compressed_size); + let limit_reader = (reader as &'a mut dyn Read).take(result.compressed_size); let result_crc32 = result.crc32; let result_compression_method = result.compression_method; @@ -1145,47 +1198,46 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( #[cfg(test)] mod test { + use crate::ZipArchive; + use std::io::Cursor; + #[test] fn invalid_offset() { use super::ZipArchive; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!("../tests/data/invalid_offset.zip")); - let reader = ZipArchive::new(io::Cursor::new(v)); + let reader = ZipArchive::new(Cursor::new(v)); assert!(reader.is_err()); } #[test] fn invalid_offset2() { use super::ZipArchive; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!("../tests/data/invalid_offset2.zip")); - let reader = ZipArchive::new(io::Cursor::new(v)); + let reader = ZipArchive::new(Cursor::new(v)); assert!(reader.is_err()); } #[test] fn zip64_with_leading_junk() { use super::ZipArchive; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!("../tests/data/zip64_demo.zip")); - let reader = ZipArchive::new(io::Cursor::new(v)).unwrap(); + let reader = ZipArchive::new(Cursor::new(v)).unwrap(); assert_eq!(reader.len(), 1); } #[test] fn zip_contents() { use super::ZipArchive; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); - let mut reader = ZipArchive::new(io::Cursor::new(v)).unwrap(); + let mut reader = ZipArchive::new(Cursor::new(v)).unwrap(); assert_eq!(reader.comment(), b""); assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77); } @@ -1193,11 +1245,10 @@ mod test { #[test] fn zip_read_streaming() { use super::read_zipfile_from_stream; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); - let mut reader = io::Cursor::new(v); + let mut reader = Cursor::new(v); loop { if read_zipfile_from_stream(&mut reader).unwrap().is_none() { break; @@ -1208,11 +1259,11 @@ mod test { #[test] fn zip_clone() { use super::ZipArchive; - use std::io::{self, Read}; + use std::io::Read; let mut v = Vec::new(); v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); - let mut reader1 = ZipArchive::new(io::Cursor::new(v)).unwrap(); + let mut reader1 = ZipArchive::new(Cursor::new(v)).unwrap(); let mut reader2 = reader1.clone(); let mut file1 = reader1.by_index(0).unwrap(); @@ -1249,11 +1300,10 @@ mod test { #[test] fn file_and_dir_predicates() { use super::ZipArchive; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!("../tests/data/files_and_dirs.zip")); - let mut zip = ZipArchive::new(io::Cursor::new(v)).unwrap(); + let mut zip = ZipArchive::new(Cursor::new(v)).unwrap(); for i in 0..zip.len() { let zip_file = zip.by_index(i).unwrap(); @@ -1266,20 +1316,35 @@ mod test { } } + #[test] + fn zip64_magic_in_filenames() { + let files = vec![ + include_bytes!("../tests/data/zip64_magic_in_filename_1.zip").to_vec(), + include_bytes!("../tests/data/zip64_magic_in_filename_2.zip").to_vec(), + include_bytes!("../tests/data/zip64_magic_in_filename_3.zip").to_vec(), + include_bytes!("../tests/data/zip64_magic_in_filename_4.zip").to_vec(), + include_bytes!("../tests/data/zip64_magic_in_filename_5.zip").to_vec(), + ]; + // Although we don't allow adding files whose names contain the ZIP64 CDB-end or + // CDB-end-locator signatures, we still read them when they aren't genuinely ambiguous. + for file in files { + ZipArchive::new(Cursor::new(file)).unwrap(); + } + } + /// test case to ensure we don't preemptively over allocate based on the /// declared number of files in the CDE of an invalid zip when the number of /// files declared is more than the alleged offset in the CDE #[test] fn invalid_cde_number_of_files_allocation_smaller_offset() { use super::ZipArchive; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!( "../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip" )); - let reader = ZipArchive::new(io::Cursor::new(v)); - assert!(reader.is_err()); + let reader = ZipArchive::new(Cursor::new(v)); + assert!(reader.is_err() || reader.unwrap().is_empty()); } /// test case to ensure we don't preemptively over allocate based on the @@ -1288,13 +1353,12 @@ mod test { #[test] fn invalid_cde_number_of_files_allocation_greater_offset() { use super::ZipArchive; - use std::io; let mut v = Vec::new(); v.extend_from_slice(include_bytes!( "../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip" )); - let reader = ZipArchive::new(io::Cursor::new(v)); + let reader = ZipArchive::new(Cursor::new(v)); assert!(reader.is_err()); } } diff --git a/src/read/stream.rs b/src/read/stream.rs index 5a01b23f..97e7019a 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -1,6 +1,6 @@ use std::fs; use std::io::{self, Read}; -use std::path::Path; +use std::path::{Path, PathBuf}; use super::{ central_header_to_zip_file_inner, read_zipfile_from_stream, spec, ZipError, ZipFile, @@ -15,7 +15,7 @@ pub struct ZipStreamReader(R); impl ZipStreamReader { /// Create a new ZipStreamReader - pub fn new(reader: R) -> Self { + pub const fn new(reader: R) -> Self { Self(reader) } } @@ -162,7 +162,7 @@ impl ZipStreamFileMetadata { /// [`ZipFile::enclosed_name`] is the better option in most scenarios. /// /// [`ParentDir`]: `Component::ParentDir` - pub fn mangled_name(&self) -> ::std::path::PathBuf { + pub fn mangled_name(&self) -> PathBuf { self.0.file_name_sanitized() } @@ -184,8 +184,7 @@ impl ZipStreamFileMetadata { pub fn is_dir(&self) -> bool { self.name() .chars() - .rev() - .next() + .next_back() .map_or(false, |c| c == '/' || c == '\\') } @@ -205,7 +204,7 @@ impl ZipStreamFileMetadata { } /// Get unix mode for the file - pub fn unix_mode(&self) -> Option { + pub const fn unix_mode(&self) -> Option { self.0.unix_mode() } } diff --git a/src/result.rs b/src/result.rs index 00d558cb..0c4776eb 100644 --- a/src/result.rs +++ b/src/result.rs @@ -3,6 +3,8 @@ use std::error::Error; use std::fmt; use std::io; +use std::io::IntoInnerError; +use std::num::TryFromIntError; /// Generic result type with ZipError as its error variant pub type ZipResult = Result; @@ -41,6 +43,12 @@ impl From for ZipError { } } +impl From> for ZipError { + fn from(value: IntoInnerError) -> Self { + ZipError::Io(value.into_error()) + } +} + impl fmt::Display for ZipError { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match self { @@ -65,8 +73,8 @@ impl ZipError { /// The text used as an error when a password is required and not supplied /// /// ```rust,no_run - /// # use zip::result::ZipError; - /// # let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&[])).unwrap(); + /// # use zip_next::result::ZipError; + /// # let mut archive = zip_next::ZipArchive::new(std::io::Cursor::new(&[])).unwrap(); /// match archive.by_index(1) { /// Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)) => eprintln!("a password is needed to unzip this file"), /// _ => (), @@ -86,6 +94,13 @@ impl From for io::Error { #[derive(Debug)] pub struct DateTimeRangeError; +// TryFromIntError is also an out-of-range error. +impl From for DateTimeRangeError { + fn from(_value: TryFromIntError) -> Self { + DateTimeRangeError + } +} + impl fmt::Display for DateTimeRangeError { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!( diff --git a/src/spec.rs b/src/spec.rs index 1d8cb0a6..691f2cab 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -5,9 +5,9 @@ use std::io::prelude::*; pub const LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x04034b50; pub const CENTRAL_DIRECTORY_HEADER_SIGNATURE: u32 = 0x02014b50; -const CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06054b50; +pub(crate) const CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06054b50; pub const ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06064b50; -const ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE: u32 = 0x07064b50; +pub(crate) const ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE: u32 = 0x07064b50; pub const ZIP64_BYTES_THR: u64 = u32::MAX as u64; pub const ZIP64_ENTRY_THR: usize = u16::MAX as usize; @@ -23,18 +23,6 @@ pub struct CentralDirectoryEnd { } impl CentralDirectoryEnd { - // Per spec 4.4.1.4 - a CentralDirectoryEnd field might be insufficient to hold the - // required data. In this case the file SHOULD contain a ZIP64 format record - // and the field of this record will be set to -1 - pub(crate) fn record_too_small(&self) -> bool { - self.disk_number == 0xFFFF - || self.disk_with_central_directory == 0xFFFF - || self.number_of_files_on_this_disk == 0xFFFF - || self.number_of_files == 0xFFFF - || self.central_directory_size == 0xFFFFFFFF - || self.central_directory_offset == 0xFFFFFFFF - } - pub fn parse(reader: &mut T) -> ZipResult { let magic = reader.read_u32::()?; if magic != CENTRAL_DIRECTORY_END_SIGNATURE { @@ -61,14 +49,12 @@ impl CentralDirectoryEnd { }) } - pub fn find_and_parse( - reader: &mut T, - ) -> ZipResult<(CentralDirectoryEnd, u64)> { + pub fn find_and_parse(reader: &mut T) -> ZipResult<(CentralDirectoryEnd, u64)> { const HEADER_SIZE: u64 = 22; const BYTES_BETWEEN_MAGIC_AND_COMMENT_SIZE: u64 = HEADER_SIZE - 6; let file_length = reader.seek(io::SeekFrom::End(0))?; - let search_upper_bound = file_length.saturating_sub(HEADER_SIZE + ::std::u16::MAX as u64); + let search_upper_bound = file_length.saturating_sub(HEADER_SIZE + u16::MAX as u64); if file_length < HEADER_SIZE { return Err(ZipError::InvalidArchive("Invalid zip header")); @@ -155,14 +141,14 @@ pub struct Zip64CentralDirectoryEnd { } impl Zip64CentralDirectoryEnd { - pub fn find_and_parse( + pub fn find_and_parse( reader: &mut T, nominal_offset: u64, search_upper_bound: u64, ) -> ZipResult<(Zip64CentralDirectoryEnd, u64)> { - let mut pos = nominal_offset; + let mut pos = search_upper_bound; - while pos <= search_upper_bound { + while pos >= nominal_offset { reader.seek(io::SeekFrom::Start(pos))?; if reader.read_u32::()? == ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE { @@ -194,8 +180,11 @@ impl Zip64CentralDirectoryEnd { archive_offset, )); } - - pos += 1; + if pos > 0 { + pos -= 1; + } else { + break; + } } Err(ZipError::InvalidArchive( diff --git a/src/types.rs b/src/types.rs index c3d0a45d..78896903 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,18 +1,20 @@ //! Types that specify what is contained in a ZIP. +use path::{Component, Path, PathBuf}; use std::path; +use std::sync::Arc; +#[cfg(feature = "chrono")] +use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; #[cfg(not(any( all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc" )))] use std::sync::atomic; -#[cfg(not(feature = "time"))] -use std::time::SystemTime; #[cfg(doc)] use {crate::read::ZipFile, crate::write::FileOptions}; -mod ffi { +pub(crate) mod ffi { pub const S_IFDIR: u32 = 0o0040000; pub const S_IFREG: u32 = 0o0100000; } @@ -49,7 +51,6 @@ mod atomic { } } -#[cfg(feature = "time")] use crate::result::DateTimeRangeError; #[cfg(feature = "time")] use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; @@ -62,7 +63,7 @@ pub enum System { } impl System { - pub fn from_u8(system: u8) -> System { + pub const fn from_u8(system: u8) -> System { use self::System::*; match system { @@ -100,7 +101,51 @@ pub struct DateTime { second: u8, } -impl ::std::default::Default for DateTime { +#[cfg(fuzzing)] +impl arbitrary::Arbitrary<'_> for DateTime { + fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result { + Ok(DateTime { + year: u.int_in_range(1980..=2107)?, + month: u.int_in_range(1..=12)?, + day: u.int_in_range(1..=31)?, + hour: u.int_in_range(0..=23)?, + minute: u.int_in_range(0..=59)?, + second: u.int_in_range(0..=60)?, + }) + } +} + +#[cfg(feature = "chrono")] +impl TryFrom for DateTime { + type Error = DateTimeRangeError; + + fn try_from(value: NaiveDateTime) -> Result { + DateTime::from_date_and_time( + value.year().try_into()?, + value.month().try_into()?, + value.day().try_into()?, + value.hour().try_into()?, + value.minute().try_into()?, + value.second().try_into()?, + ) + } +} + +#[cfg(feature = "chrono")] +impl TryInto for DateTime { + type Error = DateTimeRangeError; + + fn try_into(self) -> Result { + let date = NaiveDate::from_ymd_opt(self.year.into(), self.month.into(), self.day.into()) + .ok_or(DateTimeRangeError)?; + let time = + NaiveTime::from_hms_opt(self.hour.into(), self.minute.into(), self.second.into()) + .ok_or(DateTimeRangeError)?; + Ok(NaiveDateTime::new(date, time)) + } +} + +impl Default for DateTime { /// Constructs an 'default' datetime of 1980-01-01 00:00:00 fn default() -> DateTime { DateTime { @@ -116,7 +161,7 @@ impl ::std::default::Default for DateTime { impl DateTime { /// Converts an msdos (u16, u16) pair to a DateTime object - pub fn from_msdos(datepart: u16, timepart: u16) -> DateTime { + pub const fn from_msdos(datepart: u16, timepart: u16) -> DateTime { let seconds = (timepart & 0b0000000000011111) << 1; let minutes = (timepart & 0b0000011111100000) >> 5; let hours = (timepart & 0b1111100000000000) >> 11; @@ -143,7 +188,6 @@ impl DateTime { /// * hour: [0, 23] /// * minute: [0, 59] /// * second: [0, 60] - #[allow(clippy::result_unit_err)] pub fn from_date_and_time( year: u16, month: u8, @@ -151,7 +195,7 @@ impl DateTime { hour: u8, minute: u8, second: u8, - ) -> Result { + ) -> Result { if (1980..=2107).contains(&year) && (1..=12).contains(&month) && (1..=31).contains(&day) @@ -168,27 +212,39 @@ impl DateTime { second, }) } else { - Err(()) + Err(DateTimeRangeError) } } + /// Indicates whether this date and time can be written to a zip archive. + pub fn is_valid(&self) -> bool { + DateTime::from_date_and_time( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + ) + .is_ok() + } + #[cfg(feature = "time")] /// Converts a OffsetDateTime object to a DateTime /// /// Returns `Err` when this object is out of bounds - #[allow(clippy::result_unit_err)] #[deprecated(note = "use `DateTime::try_from()`")] - pub fn from_time(dt: OffsetDateTime) -> Result { - dt.try_into().map_err(|_err| ()) + pub fn from_time(dt: OffsetDateTime) -> Result { + dt.try_into().map_err(|_err| DateTimeRangeError) } /// Gets the time portion of this datetime in the msdos representation - pub fn timepart(&self) -> u16 { + pub const fn timepart(&self) -> u16 { ((self.second as u16) >> 1) | ((self.minute as u16) << 5) | ((self.hour as u16) << 11) } /// Gets the date portion of this datetime in the msdos representation - pub fn datepart(&self) -> u16 { + pub const fn datepart(&self) -> u16 { (self.day as u16) | ((self.month as u16) << 5) | ((self.year - 1980) << 9) } @@ -202,7 +258,7 @@ impl DateTime { } /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018. - pub fn year(&self) -> u16 { + pub const fn year(&self) -> u16 { self.year } @@ -211,7 +267,7 @@ impl DateTime { /// # Warning /// /// When read from a zip file, this may not be a reasonable value - pub fn month(&self) -> u8 { + pub const fn month(&self) -> u8 { self.month } @@ -220,7 +276,7 @@ impl DateTime { /// # Warning /// /// When read from a zip file, this may not be a reasonable value - pub fn day(&self) -> u8 { + pub const fn day(&self) -> u8 { self.day } @@ -229,7 +285,7 @@ impl DateTime { /// # Warning /// /// When read from a zip file, this may not be a reasonable value - pub fn hour(&self) -> u8 { + pub const fn hour(&self) -> u8 { self.hour } @@ -238,7 +294,7 @@ impl DateTime { /// # Warning /// /// When read from a zip file, this may not be a reasonable value - pub fn minute(&self) -> u8 { + pub const fn minute(&self) -> u8 { self.minute } @@ -247,7 +303,7 @@ impl DateTime { /// # Warning /// /// When read from a zip file, this may not be a reasonable value - pub fn second(&self) -> u8 { + pub const fn second(&self) -> u8 { self.second } } @@ -259,8 +315,8 @@ impl TryFrom for DateTime { fn try_from(dt: OffsetDateTime) -> Result { if dt.year() >= 1980 && dt.year() <= 2107 { Ok(DateTime { - year: (dt.year()) as u16, - month: (dt.month()) as u8, + year: (dt.year()).try_into()?, + month: dt.month().into(), day: dt.day(), hour: dt.hour(), minute: dt.minute(), @@ -282,7 +338,7 @@ pub const DEFAULT_VERSION: u8 = 46; pub struct AtomicU64(atomic::AtomicU64); impl AtomicU64 { - pub fn new(v: u64) -> Self { + pub const fn new(v: u64) -> Self { Self(atomic::AtomicU64::new(v)) } @@ -333,7 +389,9 @@ pub struct ZipFileData { /// Raw file name. To be used when file_name was incorrectly decoded. pub file_name_raw: Vec, /// Extra field usually used for storage expansion - pub extra_field: Vec, + pub extra_field: Arc>, + /// Extra field only written to central directory + pub central_extra_field: Arc>, /// File comment pub file_comment: String, /// Specifies where the local header of the file starts @@ -353,7 +411,7 @@ pub struct ZipFileData { } impl ZipFileData { - pub fn file_name_sanitized(&self) -> ::std::path::PathBuf { + pub fn file_name_sanitized(&self) -> PathBuf { let no_null_filename = match self.file_name.find('\0') { Some(index) => &self.file_name[0..index], None => &self.file_name, @@ -363,7 +421,7 @@ impl ZipFileData { // zip files can contain both / and \ as separators regardless of the OS // and as we want to return a sanitized PathBuf that only supports the // OS separator let's convert incompatible separators to compatible ones - let separator = ::std::path::MAIN_SEPARATOR; + let separator = path::MAIN_SEPARATOR; let opposite_separator = match separator { '/' => '\\', _ => '/', @@ -371,34 +429,34 @@ impl ZipFileData { let filename = no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string()); - ::std::path::Path::new(&filename) + Path::new(&filename) .components() - .filter(|component| matches!(*component, ::std::path::Component::Normal(..))) - .fold(::std::path::PathBuf::new(), |mut path, ref cur| { + .filter(|component| matches!(*component, path::Component::Normal(..))) + .fold(PathBuf::new(), |mut path, ref cur| { path.push(cur.as_os_str()); path }) } - pub(crate) fn enclosed_name(&self) -> Option<&path::Path> { + pub(crate) fn enclosed_name(&self) -> Option<&Path> { if self.file_name.contains('\0') { return None; } - let path = path::Path::new(&self.file_name); + let path = Path::new(&self.file_name); let mut depth = 0usize; for component in path.components() { match component { - path::Component::Prefix(_) | path::Component::RootDir => return None, - path::Component::ParentDir => depth = depth.checked_sub(1)?, - path::Component::Normal(_) => depth += 1, - path::Component::CurDir => (), + Component::Prefix(_) | Component::RootDir => return None, + Component::ParentDir => depth = depth.checked_sub(1)?, + Component::Normal(_) => depth += 1, + Component::CurDir => (), } } Some(path) } /// Get unix mode for the file - pub(crate) fn unix_mode(&self) -> Option { + pub(crate) const fn unix_mode(&self) -> Option { if self.external_attributes == 0 { return None; } @@ -422,13 +480,13 @@ impl ZipFileData { } } - pub fn zip64_extension(&self) -> bool { + pub const fn zip64_extension(&self) -> bool { self.uncompressed_size > 0xFFFFFFFF || self.compressed_size > 0xFFFFFFFF || self.header_start > 0xFFFFFFFF } - pub fn version_needed(&self) -> u16 { + pub const fn version_needed(&self) -> u16 { // higher versions matched first match (self.zip64_extension(), self.compression_method) { #[cfg(feature = "bzip2")] @@ -459,11 +517,11 @@ pub enum AesMode { #[cfg(feature = "aes-crypto")] impl AesMode { - pub fn salt_length(&self) -> usize { + pub const fn salt_length(&self) -> usize { self.key_length() / 2 } - pub fn key_length(&self) -> usize { + pub const fn key_length(&self) -> usize { match self { Self::Aes128 => 16, Self::Aes192 => 24, @@ -500,7 +558,8 @@ mod test { uncompressed_size: 0, file_name: file_name.clone(), file_name_raw: file_name.into_bytes(), - extra_field: Vec::new(), + extra_field: Arc::new(vec![]), + central_extra_field: Arc::new(vec![]), file_comment: String::new(), header_start: 0, data_start: AtomicU64::new(0), @@ -509,10 +568,7 @@ mod test { large_file: false, aes_mode: None, }; - assert_eq!( - data.file_name_sanitized(), - ::std::path::PathBuf::from("path/etc/passwd") - ); + assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd")); } #[test] diff --git a/src/unstable.rs b/src/unstable.rs index f8b46a97..cc03ff9a 100644 --- a/src/unstable.rs +++ b/src/unstable.rs @@ -8,7 +8,7 @@ pub mod write { /// Unstable methods for [`FileOptions`]. pub trait FileOptionsExt { /// Write the file with the given password using the deprecated ZipCrypto algorithm. - /// + /// /// This is not recommended for new archives, as ZipCrypto is not secure. fn with_deprecated_encryption(self, password: &[u8]) -> Self; } @@ -17,4 +17,4 @@ pub mod write { self.with_deprecated_encryption(password) } } -} \ No newline at end of file +} diff --git a/src/write.rs b/src/write.rs index b5f6a933..8278a2ea 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,24 +1,49 @@ //! Types for creating ZIP archives use crate::compression::CompressionMethod; -use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile}; +use crate::read::{central_header_to_zip_file, find_content, ZipArchive, ZipFile, ZipFileReader}; use crate::result::{ZipError, ZipResult}; use crate::spec; -use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION}; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use crate::types::{ffi, AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION}; +use byteorder::{LittleEndian, WriteBytesExt}; use crc32fast::Hasher; +use std::collections::HashMap; +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli", + feature = "bzip2", + feature = "zstd", + feature = "time" +))] use std::convert::TryInto; use std::default::Default; use std::io; use std::io::prelude::*; +use std::io::{BufReader, SeekFrom}; use std::mem; +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "zopfli", + feature = "bzip2", + feature = "zstd", +))] +use std::num::NonZeroU8; +use std::str::{from_utf8, Utf8Error}; +use std::sync::Arc; #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] -use flate2::write::DeflateEncoder; +use flate2::{write::DeflateEncoder, Compression}; #[cfg(feature = "bzip2")] use bzip2::write::BzEncoder; @@ -26,6 +51,12 @@ use bzip2::write::BzEncoder; #[cfg(feature = "time")] use time::OffsetDateTime; +#[cfg(feature = "deflate-zopfli")] +use zopfli::Options; + +#[cfg(feature = "deflate-zopfli")] +use std::io::BufWriter; + #[cfg(feature = "zstd")] use zstd::stream::write::Encoder as ZstdEncoder; @@ -47,40 +78,47 @@ impl Write for MaybeEncrypted { } } } -enum GenericZipWriter { +enum GenericZipWriter { Closed, Storer(MaybeEncrypted), #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] Deflater(DeflateEncoder>), + #[cfg(feature = "deflate-zopfli")] + ZopfliDeflater(zopfli::DeflateEncoder>), + #[cfg(feature = "deflate-zopfli")] + BufferedZopfliDeflater(BufWriter>>), #[cfg(feature = "bzip2")] Bzip2(BzEncoder>), #[cfg(feature = "zstd")] Zstd(ZstdEncoder<'static, MaybeEncrypted>), } + // Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely pub(crate) mod zip_writer { use super::*; + use std::collections::HashMap; /// ZIP archive generator /// /// Handles the bookkeeping involved in building an archive, and provides an /// API to edit its contents. /// /// ``` - /// # fn doit() -> zip::result::ZipResult<()> + /// # fn doit() -> zip_next::result::ZipResult<()> /// # { - /// # use zip::ZipWriter; + /// # use zip_next::ZipWriter; /// use std::io::Write; - /// use zip::write::FileOptions; + /// use zip_next::write::FileOptions; /// /// // We use a buffer here, though you'd normally use a `File` /// let mut buf = [0; 65536]; - /// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..])); + /// let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf[..])); /// - /// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + /// let options = FileOptions::default().compression_method(zip_next::CompressionMethod::Stored); /// zip.start_file("hello_world.txt", options)?; /// zip.write(b"Hello, World!")?; /// @@ -92,17 +130,21 @@ pub(crate) mod zip_writer { /// # } /// # doit().unwrap(); /// ``` - pub struct ZipWriter { + pub struct ZipWriter { pub(super) inner: GenericZipWriter, pub(super) files: Vec, + pub(super) files_by_name: HashMap, pub(super) stats: ZipWriterStats, pub(super) writing_to_file: bool, - pub(super) writing_to_extra_field: bool, - pub(super) writing_to_central_extra_field_only: bool, pub(super) writing_raw: bool, pub(super) comment: Vec, + pub(super) flush_on_finish_file: bool, } } +use crate::result::ZipError::InvalidArchive; +use crate::write::GenericZipWriter::{Closed, Storer}; +use crate::zipcrypto::ZipCryptoKeys; +use crate::CompressionMethod::Stored; pub use zip_writer::ZipWriter; #[derive(Default)] @@ -119,23 +161,82 @@ struct ZipRawValues { } /// Metadata for a file to be written -#[derive(Copy, Clone)] +#[derive(Clone, Debug)] pub struct FileOptions { - compression_method: CompressionMethod, - compression_level: Option, - last_modified_time: DateTime, - permissions: Option, - large_file: bool, - encrypt_with: Option, + pub(crate) compression_method: CompressionMethod, + pub(crate) compression_level: Option, + pub(crate) last_modified_time: DateTime, + pub(crate) permissions: Option, + pub(crate) large_file: bool, + encrypt_with: Option, + extra_data: Arc>, + central_extra_data: Arc>, + alignment: u16, + #[cfg(feature = "deflate-zopfli")] + pub(super) zopfli_buffer_size: Option, +} + +#[cfg(fuzzing)] +impl arbitrary::Arbitrary<'_> for FileOptions { + fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result { + let mut options = FileOptions { + compression_method: CompressionMethod::arbitrary(u)?, + compression_level: None, + last_modified_time: DateTime::arbitrary(u)?, + permissions: Option::::arbitrary(u)?, + large_file: bool::arbitrary(u)?, + encrypt_with: Option::::arbitrary(u)?, + extra_data: Arc::new(vec![]), + central_extra_data: Arc::new(vec![]), + alignment: u16::arbitrary(u)?, + #[cfg(feature = "deflate-zopfli")] + zopfli_buffer_size: None, + }; + match options.compression_method { + #[cfg(feature = "deflate-zopfli")] + CompressionMethod::Deflated => { + if bool::arbitrary(u)? { + let level = u.int_in_range(0..=24)?; + options.compression_level = Some(level); + if level > Compression::best().level().try_into().unwrap() { + options.zopfli_buffer_size = Some(1 << u.int_in_range(9..=30)?); + } + } + } + Stored => { + if bool::arbitrary(u)? { + options.compression_level = Some(1); + } + } + _ => { + if bool::arbitrary(u)? { + options.compression_level = Some(u.int_in_range(0..=10)?); + } + } + } + u.arbitrary_loop(Some(0), Some((u16::MAX / 4) as u32), |u| { + options + .add_extra_data( + u16::arbitrary(u)?, + &Vec::::arbitrary(u)?, + bool::arbitrary(u)?, + ) + .map_err(|_| arbitrary::Error::IncorrectFormat)?; + Ok(core::ops::ControlFlow::Continue(())) + })?; + Ok(options) + } } impl FileOptions { /// Set the compression method for the new file /// - /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is - /// disabled, `CompressionMethod::Stored` becomes the default. + /// The default is `CompressionMethod::Deflated` if it is enabled. If not, + /// `CompressionMethod::Bzip2` is the default if it is enabled. If neither `bzip2` nor `deflate` + /// is enabled, `CompressionMethod::Zlib` is the default. If all else fails, + /// `CompressionMethod::Stored` becomes the default and files are written uncompressed. #[must_use] - pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions { + pub const fn compression_method(mut self, method: CompressionMethod) -> FileOptions { self.compression_method = method; self } @@ -145,12 +246,13 @@ impl FileOptions { /// `None` value specifies default compression level. /// /// Range of values depends on compression method: - /// * `Deflated`: 0 - 9. Default is 6 + /// * `Deflated`: 10 - 264 for Zopfli, 0 - 9 for other encoders. Default is 24 if Zopfli is the + /// only encoder, or 6 otherwise. /// * `Bzip2`: 0 - 9. Default is 6 /// * `Zstd`: -7 - 22, with zero being mapped to default level. Default is 3 /// * others: only `None` is allowed #[must_use] - pub fn compression_level(mut self, level: Option) -> FileOptions { + pub const fn compression_level(mut self, level: Option) -> FileOptions { self.compression_level = level; self } @@ -160,7 +262,7 @@ impl FileOptions { /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01 /// otherwise #[must_use] - pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions { + pub const fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions { self.last_modified_time = mod_time; self } @@ -175,43 +277,102 @@ impl FileOptions { /// higher file mode bits. So it cannot be used to denote an entry as a directory, /// symlink, or other special file type. #[must_use] - pub fn unix_permissions(mut self, mode: u32) -> FileOptions { + pub const fn unix_permissions(mut self, mode: u32) -> FileOptions { self.permissions = Some(mode & 0o777); self } /// Set whether the new file's compressed and uncompressed size is less than 4 GiB. /// - /// If set to `false` and the file exceeds the limit, an I/O error is thrown. If set to `true`, - /// readers will require ZIP64 support and if the file does not exceed the limit, 20 B are - /// wasted. The default is `false`. + /// If set to `false` and the file exceeds the limit, an I/O error is thrown and the file is + /// aborted. If set to `true`, readers will require ZIP64 support and if the file does not + /// exceed the limit, 20 B are wasted. The default is `false`. #[must_use] - pub fn large_file(mut self, large: bool) -> FileOptions { + pub const fn large_file(mut self, large: bool) -> FileOptions { self.large_file = large; self } pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> FileOptions { - self.encrypt_with = Some(crate::zipcrypto::ZipCryptoKeys::derive(password)); + self.encrypt_with = Some(ZipCryptoKeys::derive(password)); self } + + /// Adds an extra data field. + pub fn add_extra_data( + &mut self, + header_id: u16, + data: &[u8], + central_only: bool, + ) -> ZipResult<()> { + validate_extra_data(header_id, data)?; + let len = data.len() + 4; + if self.extra_data.len() + self.central_extra_data.len() + len > u16::MAX as usize { + Err(InvalidArchive( + "Extra data field would be longer than allowed", + )) + } else { + let field = if central_only { + &mut self.central_extra_data + } else { + &mut self.extra_data + }; + let vec = Arc::get_mut(field); + let vec = match vec { + Some(exclusive) => exclusive, + None => { + *field = Arc::new(field.to_vec()); + Arc::get_mut(field).unwrap() + } + }; + vec.reserve_exact(data.len() + 4); + vec.write_u16::(header_id)?; + vec.write_u16::(data.len() as u16)?; + vec.write_all(data)?; + Ok(()) + } + } + + /// Removes the extra data fields. + #[must_use] + pub fn clear_extra_data(mut self) -> FileOptions { + if self.extra_data.len() > 0 { + self.extra_data = Arc::new(vec![]); + } + if self.central_extra_data.len() > 0 { + self.central_extra_data = Arc::new(vec![]); + } + self + } + + /// Sets the alignment to the given number of bytes. + #[must_use] + pub const fn with_alignment(mut self, alignment: u16) -> FileOptions { + self.alignment = alignment; + self + } + + /// Sets the size of the buffer used to hold the next block that Zopfli will compress. The + /// larger the buffer, the more effective the compression, but the more memory is required. + /// A value of `None` indicates no buffer, which is recommended only when all non-empty writes + /// are larger than about 32 KiB. + #[must_use] + #[cfg(feature = "deflate-zopfli")] + pub const fn with_zopfli_buffer(mut self, size: Option) -> FileOptions { + self.zopfli_buffer_size = size; + self + } + + /// Returns the compression level currently set. + pub const fn get_compression_level(&self) -> Option { + self.compression_level + } } impl Default for FileOptions { /// Construct a new FileOptions object fn default() -> Self { Self { - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - compression_method: CompressionMethod::Deflated, - #[cfg(not(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - )))] - compression_method: CompressionMethod::Stored, + compression_method: Default::default(), compression_level: None, #[cfg(feature = "time")] last_modified_time: OffsetDateTime::now_utc().try_into().unwrap_or_default(), @@ -220,11 +381,16 @@ impl Default for FileOptions { permissions: None, large_file: false, encrypt_with: None, + extra_data: Arc::new(vec![]), + central_extra_data: Arc::new(vec![]), + alignment: 1, + #[cfg(feature = "deflate-zopfli")] + zopfli_buffer_size: Some(1 << 15), } } } -impl Write for ZipWriter { +impl Write for ZipWriter { fn write(&mut self, buf: &[u8]) -> io::Result { if !self.writing_to_file { return Err(io::Error::new( @@ -232,30 +398,29 @@ impl Write for ZipWriter { "No file has been started", )); } + if buf.is_empty() { + return Ok(0); + } match self.inner.ref_mut() { Some(ref mut w) => { - if self.writing_to_extra_field { - self.files.last_mut().unwrap().extra_field.write(buf) - } else { - let write_result = w.write(buf); - if let Ok(count) = write_result { - self.stats.update(&buf[0..count]); - if self.stats.bytes_written > spec::ZIP64_BYTES_THR - && !self.files.last_mut().unwrap().large_file - { - let _inner = mem::replace(&mut self.inner, GenericZipWriter::Closed); - return Err(io::Error::new( - io::ErrorKind::Other, - "Large file option has not been set", - )); - } + let write_result = w.write(buf); + if let Ok(count) = write_result { + self.stats.update(&buf[0..count]); + if self.stats.bytes_written > spec::ZIP64_BYTES_THR + && !self.files.last_mut().unwrap().large_file + { + self.abort_file().unwrap(); + return Err(io::Error::new( + io::ErrorKind::Other, + "Large file option has not been set", + )); } - write_result } + write_result } None => Err(io::Error::new( io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", + "write(): ZipWriter was already closed", )), } } @@ -265,7 +430,7 @@ impl Write for ZipWriter { Some(ref mut w) => w.flush(), None => Err(io::Error::new( io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", + "flush(): ZipWriter was already closed", )), } } @@ -278,65 +443,148 @@ impl ZipWriterStats { } } -impl ZipWriter { +impl ZipWriter { /// Initializes the archive from an existing ZIP archive, making it ready for append. pub fn new_append(mut readwriter: A) -> ZipResult> { let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut readwriter)?; - if footer.disk_number != footer.disk_with_central_directory { + let counts = ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?; + + if counts.disk_number != counts.disk_with_central_directory { return Err(ZipError::UnsupportedArchive( "Support for multi-disk files is not implemented", )); } - let (archive_offset, directory_start, number_of_files) = - ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?; - if readwriter - .seek(io::SeekFrom::Start(directory_start)) + .seek(SeekFrom::Start(counts.directory_start)) .is_err() { - return Err(ZipError::InvalidArchive( + return Err(InvalidArchive( "Could not seek to start of central directory", )); } - let files = (0..number_of_files) - .map(|_| central_header_to_zip_file(&mut readwriter, archive_offset)) + let files = (0..counts.number_of_files) + .map(|_| central_header_to_zip_file(&mut readwriter, counts.archive_offset)) .collect::, _>>()?; - let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it + let mut files_by_name = HashMap::new(); + for (index, file) in files.iter().enumerate() { + files_by_name.insert(file.file_name.to_owned(), index); + } + + let _ = readwriter.seek(SeekFrom::Start(counts.directory_start)); // seek directory_start to overwrite it Ok(ZipWriter { - inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), + inner: Storer(MaybeEncrypted::Unencrypted(readwriter)), files, + files_by_name, stats: Default::default(), writing_to_file: false, - writing_to_extra_field: false, - writing_to_central_extra_field_only: false, comment: footer.zip_file_comment, writing_raw: true, // avoid recomputing the last file's header + flush_on_finish_file: false, }) } + + /// `flush_on_finish_file` is designed to support a streaming `inner` that may unload flushed + /// bytes. It flushes a file's header and body once it starts writing another file. A ZipWriter + /// will not try to seek back into where a previous file was written unless + /// either [`ZipWriter::abort_file`] is called while [`ZipWriter::is_writing_file`] returns + /// false, or [`ZipWriter::deep_copy_file`] is called. In the latter case, it will only need to + /// read previously-written files and not overwrite them. + /// + /// Note: when using an `inner` that cannot overwrite flushed bytes, do not wrap it in a + /// [std::io::BufWriter], because that has a [seek] method that implicitly calls [flush], and + /// ZipWriter needs to seek backward to update each file's header with the size and checksum + /// after writing the body. + /// + /// This setting is false by default. + pub fn set_flush_on_finish_file(&mut self, flush_on_finish_file: bool) { + self.flush_on_finish_file = flush_on_finish_file; + } } -impl ZipWriter { +impl ZipWriter { + /// Adds another copy of a file already in this archive. This will produce a larger but more + /// widely-compatible archive compared to [shallow_copy_file]. Does not copy alignment. + pub fn deep_copy_file(&mut self, src_name: &str, dest_name: &str) -> ZipResult<()> { + self.finish_file()?; + let write_position = self.inner.get_plain().stream_position()?; + let src_index = self.index_by_name(src_name)?; + let src_data = &self.files[src_index]; + let data_start = src_data.data_start.load(); + let compressed_size = src_data.compressed_size; + debug_assert!(compressed_size <= write_position - data_start); + let uncompressed_size = src_data.uncompressed_size; + let mut options = FileOptions { + compression_method: src_data.compression_method, + compression_level: src_data.compression_level, + last_modified_time: src_data.last_modified_time, + permissions: src_data.unix_mode(), + large_file: src_data.large_file, + encrypt_with: None, + extra_data: src_data.extra_field.clone(), + central_extra_data: src_data.central_extra_field.clone(), + alignment: 1, + #[cfg(feature = "deflate-zopfli")] + zopfli_buffer_size: None, + }; + if let Some(perms) = src_data.unix_mode() { + options = options.unix_permissions(perms); + } + Self::normalize_options(&mut options); + let raw_values = ZipRawValues { + crc32: src_data.crc32, + compressed_size, + uncompressed_size, + }; + let mut reader = BufReader::new(ZipFileReader::Raw(find_content( + src_data, + self.inner.get_plain(), + )?)); + let mut copy = Vec::with_capacity(compressed_size as usize); + reader.read_to_end(&mut copy)?; + drop(reader); + self.inner + .get_plain() + .seek(SeekFrom::Start(write_position))?; + self.start_entry(dest_name, options, Some(raw_values))?; + self.writing_to_file = true; + self.writing_raw = true; + if let Err(e) = self.write_all(©) { + self.abort_file().unwrap(); + return Err(e.into()); + } + self.finish_file() + } +} + +impl ZipWriter { /// Initializes the archive. /// /// Before writing to this object, the [`ZipWriter::start_file`] function should be called. + /// After a successful write, the file remains open for writing. After a failed write, call + /// [`ZipWriter::is_writing_file`] to determine if the file remains open. pub fn new(inner: W) -> ZipWriter { ZipWriter { - inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(inner)), + inner: Storer(MaybeEncrypted::Unencrypted(inner)), files: Vec::new(), + files_by_name: HashMap::new(), stats: Default::default(), writing_to_file: false, - writing_to_extra_field: false, - writing_to_central_extra_field_only: false, writing_raw: false, comment: Vec::new(), + flush_on_finish_file: false, } } + /// Returns true if a file is currently open for writing. + pub const fn is_writing_file(&self) -> bool { + self.writing_to_file && !self.inner.is_closed() + } + /// Set ZIP archive comment. pub fn set_comment(&mut self, comment: S) where @@ -353,6 +601,19 @@ impl ZipWriter { self.comment = comment; } + /// Get ZIP archive comment. + pub fn get_comment(&mut self) -> Result<&str, Utf8Error> { + from_utf8(self.get_raw_comment()) + } + + /// Get ZIP archive comment. + /// + /// This returns the raw bytes of the comment. The comment + /// is typically expected to be encoded in UTF-8 + pub const fn get_raw_comment(&self) -> &Vec { + &self.comment + } + /// Start a new file for with the requested options. fn start_entry( &mut self, @@ -372,11 +633,11 @@ impl ZipWriter { }); { - let writer = self.inner.get_plain(); - let header_start = writer.stream_position()?; + let header_start = self.inner.get_plain().stream_position()?; + let name = name.into(); let permissions = options.permissions.unwrap_or(0o100644); - let mut file = ZipFileData { + let file = ZipFileData { system: System::Unix, version_made_by: DEFAULT_VERSION, encrypted: options.encrypt_with.is_some(), @@ -387,9 +648,10 @@ impl ZipWriter { crc32: raw_values.crc32, compressed_size: raw_values.compressed_size, uncompressed_size: raw_values.uncompressed_size, - file_name: name.into(), + file_name: name, file_name_raw: Vec::new(), // Never used for saving - extra_field: Vec::new(), + extra_field: options.extra_data, + central_extra_field: options.central_extra_data, file_comment: String::new(), header_start, data_start: AtomicU64::new(0), @@ -398,41 +660,127 @@ impl ZipWriter { large_file: options.large_file, aes_mode: None, }; - write_local_file_header(writer, &file)?; + let index = self.insert_file_data(file)?; + let file = &mut self.files[index]; + let writer = self.inner.get_plain(); + writer.write_u32::(spec::LOCAL_FILE_HEADER_SIGNATURE)?; + // version needed to extract + writer.write_u16::(file.version_needed())?; + // general purpose bit flag + let flag = if !file.file_name.is_ascii() { + 1u16 << 11 + } else { + 0 + } | if file.encrypted { 1u16 << 0 } else { 0 }; + writer.write_u16::(flag)?; + // Compression method + #[allow(deprecated)] + writer.write_u16::(file.compression_method.to_u16())?; + // last mod file time and last mod file date + writer.write_u16::(file.last_modified_time.timepart())?; + writer.write_u16::(file.last_modified_time.datepart())?; + // crc-32 + writer.write_u32::(file.crc32)?; + // compressed size and uncompressed size + if file.large_file { + writer.write_u32::(spec::ZIP64_BYTES_THR as u32)?; + writer.write_u32::(spec::ZIP64_BYTES_THR as u32)?; + } else { + writer.write_u32::(file.compressed_size as u32)?; + writer.write_u32::(file.uncompressed_size as u32)?; + } + // file name length + writer.write_u16::(file.file_name.as_bytes().len() as u16)?; + // extra field length + let mut extra_field_length = file.extra_field.len(); + if file.large_file { + extra_field_length += 20; + } + if extra_field_length + file.central_extra_field.len() > u16::MAX as usize { + let _ = self.abort_file(); + return Err(InvalidArchive("Extra data field is too large")); + } + let extra_field_length = extra_field_length as u16; + writer.write_u16::(extra_field_length)?; + // file name + writer.write_all(file.file_name.as_bytes())?; + // zip64 extra field + if file.large_file { + write_local_zip64_extra_field(writer, file)?; + } + writer.write_all(&file.extra_field)?; + let mut header_end = writer.stream_position()?; + if options.alignment > 1 { + let align = options.alignment as u64; + if header_end % align != 0 { + let pad_length = (align - (header_end + 4) % align) % align; + if pad_length + extra_field_length as u64 > u16::MAX as u64 { + let _ = self.abort_file(); + return Err(InvalidArchive( + "Extra data field would be larger than allowed after aligning", + )); + } + let pad = vec![0; pad_length as usize]; + writer.write_all(b"za").map_err(ZipError::from)?; // 0x617a + writer + .write_u16::(pad.len() as u16) + .map_err(ZipError::from)?; + writer.write_all(&pad).map_err(ZipError::from)?; + header_end = writer.stream_position()?; - let header_end = writer.stream_position()?; + // Update extra field length in local file header. + writer.seek(SeekFrom::Start(file.header_start + 28))?; + writer.write_u16::(pad_length as u16 + extra_field_length)?; + writer.seek(SeekFrom::Start(header_end))?; + debug_assert_eq!(header_end % align, 0); + } + } + if let Some(keys) = options.encrypt_with { + let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { + writer: mem::replace(&mut self.inner, Closed).unwrap(), + buffer: vec![], + keys, + }; + let crypto_header = [0u8; 12]; + + zipwriter.write_all(&crypto_header)?; + header_end = zipwriter.writer.stream_position()?; + self.inner = Storer(MaybeEncrypted::Encrypted(zipwriter)); + } self.stats.start = header_end; *file.data_start.get_mut() = header_end; - + self.writing_to_file = true; self.stats.bytes_written = 0; self.stats.hasher = Hasher::new(); - - self.files.push(file); - } - if let Some(keys) = options.encrypt_with { - let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { writer: core::mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(), buffer: vec![], keys }; - let mut crypto_header = [0u8; 12]; - - zipwriter.write_all(&crypto_header)?; - self.inner = GenericZipWriter::Storer(MaybeEncrypted::Encrypted(zipwriter)); } Ok(()) } + fn insert_file_data(&mut self, file: ZipFileData) -> ZipResult { + let name = &file.file_name; + if self.files_by_name.contains_key(name) { + return Err(InvalidArchive("Duplicate filename")); + } + let name = name.to_owned(); + self.files.push(file); + let index = self.files.len() - 1; + self.files_by_name.insert(name, index); + Ok(index) + } + fn finish_file(&mut self) -> ZipResult<()> { - if self.writing_to_extra_field { - // Implicitly calling [`ZipWriter::end_extra_data`] for empty files. - self.end_extra_data()?; - } - self.inner.switch_to(CompressionMethod::Stored, None)?; - match core::mem::replace(&mut self.inner, GenericZipWriter::Closed) { - GenericZipWriter::Storer(MaybeEncrypted::Encrypted(writer)) => { - let crc32 = self.stats.hasher.clone().finalize(); - self.inner = GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?)) - } - GenericZipWriter::Storer(w) => self.inner = GenericZipWriter::Storer(w), - _ => unreachable!() + if !self.writing_to_file { + return Ok(()); } + + let make_plain_writer = self.inner.prepare_next_writer( + Stored, + None, + #[cfg(feature = "deflate-zopfli")] + None, + )?; + self.inner.switch_to(make_plain_writer)?; + self.switch_to_non_encrypting_writer()?; let writer = self.inner.get_plain(); if !self.writing_raw { @@ -444,33 +792,97 @@ impl ZipWriter { file.uncompressed_size = self.stats.bytes_written; let file_end = writer.stream_position()?; + debug_assert!(file_end >= self.stats.start); file.compressed_size = file_end - self.stats.start; update_local_file_header(writer, file)?; - writer.seek(io::SeekFrom::Start(file_end))?; + writer.seek(SeekFrom::Start(file_end))?; + } + if self.flush_on_finish_file { + if let Err(e) = writer.flush() { + self.abort_file()?; + return Err(e.into()); + } } self.writing_to_file = false; - self.writing_raw = false; Ok(()) } - /// Create a file in the archive and start writing its' contents. + fn switch_to_non_encrypting_writer(&mut self) -> Result<(), ZipError> { + match mem::replace(&mut self.inner, Closed) { + Storer(MaybeEncrypted::Encrypted(writer)) => { + let crc32 = self.stats.hasher.clone().finalize(); + self.inner = Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?)) + } + Storer(MaybeEncrypted::Unencrypted(w)) => { + self.inner = Storer(MaybeEncrypted::Unencrypted(w)) + } + _ => unreachable!(), + } + Ok(()) + } + + /// Removes the file currently being written from the archive if there is one, or else removes + /// the file most recently written. + pub fn abort_file(&mut self) -> ZipResult<()> { + let last_file = self.files.pop().ok_or(ZipError::FileNotFound)?; + self.files_by_name.remove(&last_file.file_name); + let make_plain_writer = self.inner.prepare_next_writer( + Stored, + None, + #[cfg(feature = "deflate-zopfli")] + None, + )?; + self.inner.switch_to(make_plain_writer)?; + self.switch_to_non_encrypting_writer()?; + // Make sure this is the last file, and that no shallow copies of it remain; otherwise we'd + // overwrite a valid file and corrupt the archive + if self + .files + .iter() + .all(|file| file.data_start.load() < last_file.data_start.load()) + { + self.inner + .get_plain() + .seek(SeekFrom::Start(last_file.header_start))?; + } + self.writing_to_file = false; + Ok(()) + } + + /// Create a file in the archive and start writing its' contents. The file must not have the + /// same name as a file already in the archive. /// - /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] + /// The data should be written using the [`Write`] implementation on this [`ZipWriter`] pub fn start_file(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> where S: Into, { + Self::normalize_options(&mut options); + let make_new_self = self.inner.prepare_next_writer( + options.compression_method, + options.compression_level, + #[cfg(feature = "deflate-zopfli")] + options.zopfli_buffer_size, + )?; + self.start_entry(name, options, None)?; + if let Err(e) = self.inner.switch_to(make_new_self) { + self.abort_file().unwrap(); + return Err(e); + } + self.writing_raw = false; + Ok(()) + } + + fn normalize_options(options: &mut FileOptions) { if options.permissions.is_none() { options.permissions = Some(0o644); } - *options.permissions.as_mut().unwrap() |= 0o100000; - self.start_entry(name, options, None)?; - self.inner - .switch_to(options.compression_method, options.compression_level)?; - self.writing_to_file = true; - Ok(()) + if !options.last_modified_time.is_valid() { + options.last_modified_time = FileOptions::default().last_modified_time; + } + *options.permissions.as_mut().unwrap() |= ffi::S_IFREG; } /// Starts a file, taking a Path as argument. @@ -489,171 +901,6 @@ impl ZipWriter { self.start_file(path_to_string(path), options) } - /// Create an aligned file in the archive and start writing its' contents. - /// - /// Returns the number of padding bytes required to align the file. - /// - /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] - pub fn start_file_aligned( - &mut self, - name: S, - options: FileOptions, - align: u16, - ) -> Result - where - S: Into, - { - let data_start = self.start_file_with_extra_data(name, options)?; - let align = align as u64; - if align > 1 && data_start % align != 0 { - let pad_length = (align - (data_start + 4) % align) % align; - let pad = vec![0; pad_length as usize]; - self.write_all(b"za").map_err(ZipError::from)?; // 0x617a - self.write_u16::(pad.len() as u16) - .map_err(ZipError::from)?; - self.write_all(&pad).map_err(ZipError::from)?; - assert_eq!(self.end_local_start_central_extra_data()? % align, 0); - } - let extra_data_end = self.end_extra_data()?; - Ok(extra_data_end - data_start) - } - - /// Create a file in the archive and start writing its extra data first. - /// - /// Finish writing extra data and start writing file data with [`ZipWriter::end_extra_data`]. - /// Optionally, distinguish local from central extra data with - /// [`ZipWriter::end_local_start_central_extra_data`]. - /// - /// Returns the preliminary starting offset of the file data without any extra data allowing to - /// align the file data by calculating a pad length to be prepended as part of the extra data. - /// - /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] - /// - /// ``` - /// use byteorder::{LittleEndian, WriteBytesExt}; - /// use zip::{ZipArchive, ZipWriter, result::ZipResult}; - /// use zip::{write::FileOptions, CompressionMethod}; - /// use std::io::{Write, Cursor}; - /// - /// # fn main() -> ZipResult<()> { - /// let mut archive = Cursor::new(Vec::new()); - /// - /// { - /// let mut zip = ZipWriter::new(&mut archive); - /// let options = FileOptions::default() - /// .compression_method(CompressionMethod::Stored); - /// - /// zip.start_file_with_extra_data("identical_extra_data.txt", options)?; - /// let extra_data = b"local and central extra data"; - /// zip.write_u16::(0xbeef)?; - /// zip.write_u16::(extra_data.len() as u16)?; - /// zip.write_all(extra_data)?; - /// zip.end_extra_data()?; - /// zip.write_all(b"file data")?; - /// - /// let data_start = zip.start_file_with_extra_data("different_extra_data.txt", options)?; - /// let extra_data = b"local extra data"; - /// zip.write_u16::(0xbeef)?; - /// zip.write_u16::(extra_data.len() as u16)?; - /// zip.write_all(extra_data)?; - /// let data_start = data_start as usize + 4 + extra_data.len() + 4; - /// let align = 64; - /// let pad_length = (align - data_start % align) % align; - /// assert_eq!(pad_length, 19); - /// zip.write_u16::(0xdead)?; - /// zip.write_u16::(pad_length as u16)?; - /// zip.write_all(&vec![0; pad_length])?; - /// let data_start = zip.end_local_start_central_extra_data()?; - /// assert_eq!(data_start as usize % align, 0); - /// let extra_data = b"central extra data"; - /// zip.write_u16::(0xbeef)?; - /// zip.write_u16::(extra_data.len() as u16)?; - /// zip.write_all(extra_data)?; - /// zip.end_extra_data()?; - /// zip.write_all(b"file data")?; - /// - /// zip.finish()?; - /// } - /// - /// let mut zip = ZipArchive::new(archive)?; - /// assert_eq!(&zip.by_index(0)?.extra_data()[4..], b"local and central extra data"); - /// assert_eq!(&zip.by_index(1)?.extra_data()[4..], b"central extra data"); - /// # Ok(()) - /// # } - /// ``` - pub fn start_file_with_extra_data( - &mut self, - name: S, - mut options: FileOptions, - ) -> ZipResult - where - S: Into, - { - if options.permissions.is_none() { - options.permissions = Some(0o644); - } - *options.permissions.as_mut().unwrap() |= 0o100000; - self.start_entry(name, options, None)?; - self.writing_to_file = true; - self.writing_to_extra_field = true; - Ok(self.files.last().unwrap().data_start.load()) - } - - /// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`]. - /// - /// Returns the final starting offset of the file data. - pub fn end_local_start_central_extra_data(&mut self) -> ZipResult { - let data_start = self.end_extra_data()?; - self.files.last_mut().unwrap().extra_field.clear(); - self.writing_to_extra_field = true; - self.writing_to_central_extra_field_only = true; - Ok(data_start) - } - - /// End extra data and start file data. Requires [`ZipWriter::start_file_with_extra_data`]. - /// - /// Returns the final starting offset of the file data. - pub fn end_extra_data(&mut self) -> ZipResult { - // Require `start_file_with_extra_data()`. Ensures `file` is some. - if !self.writing_to_extra_field { - return Err(ZipError::Io(io::Error::new( - io::ErrorKind::Other, - "Not writing to extra field", - ))); - } - let file = self.files.last_mut().unwrap(); - - validate_extra_data(file)?; - - let data_start = file.data_start.get_mut(); - - if !self.writing_to_central_extra_field_only { - let writer = self.inner.get_plain(); - - // Append extra data to local file header and keep it for central file header. - writer.write_all(&file.extra_field)?; - - // Update final `data_start`. - let header_end = *data_start + file.extra_field.len() as u64; - self.stats.start = header_end; - *data_start = header_end; - - // Update extra field length in local file header. - let extra_field_length = - if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16; - writer.seek(io::SeekFrom::Start(file.header_start + 28))?; - writer.write_u16::(extra_field_length)?; - writer.seek(io::SeekFrom::Start(header_end))?; - - self.inner - .switch_to(file.compression_method, file.compression_level)?; - } - - self.writing_to_extra_field = false; - self.writing_to_central_extra_field_only = false; - Ok(*data_start) - } - /// Add a new file using the already compressed data from a ZIP file being read and renames it, this /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again. /// Any `ZipFile` metadata is copied and not checked, for example the file CRC. @@ -661,12 +908,12 @@ impl ZipWriter { /// ```no_run /// use std::fs::File; /// use std::io::{Read, Seek, Write}; - /// use zip::{ZipArchive, ZipWriter}; + /// use zip_next::{ZipArchive, ZipWriter}; /// /// fn copy_rename( /// src: &mut ZipArchive, /// dst: &mut ZipWriter, - /// ) -> zip::result::ZipResult<()> + /// ) -> zip_next::result::ZipResult<()> /// where /// R: Read + Seek, /// W: Write + Seek, @@ -691,6 +938,7 @@ impl ZipWriter { if let Some(perms) = file.unix_mode() { options = options.unix_permissions(perms); } + Self::normalize_options(&mut options); let raw_values = ZipRawValues { crc32: file.crc32(), @@ -714,9 +962,9 @@ impl ZipWriter { /// ```no_run /// use std::fs::File; /// use std::io::{Read, Seek, Write}; - /// use zip::{ZipArchive, ZipWriter}; + /// use zip_next::{ZipArchive, ZipWriter}; /// - /// fn copy(src: &mut ZipArchive, dst: &mut ZipWriter) -> zip::result::ZipResult<()> + /// fn copy(src: &mut ZipArchive, dst: &mut ZipWriter) -> zip_next::result::ZipResult<()> /// where /// R: Read + Seek, /// W: Write + Seek, @@ -746,7 +994,7 @@ impl ZipWriter { options.permissions = Some(0o755); } *options.permissions.as_mut().unwrap() |= 0o40000; - options.compression_method = CompressionMethod::Stored; + options.compression_method = Stored; let name_as_string = name.into(); // Append a slash to the filename if it does not end with it. @@ -757,6 +1005,7 @@ impl ZipWriter { self.start_entry(name_with_slash, options, None)?; self.writing_to_file = false; + self.switch_to_non_encrypting_writer()?; Ok(()) } @@ -782,7 +1031,7 @@ impl ZipWriter { /// Note that the zipfile will also be finished on drop. pub fn finish(&mut self) -> ZipResult { self.finalize()?; - let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed); + let inner = mem::replace(&mut self.inner, Closed); Ok(inner.unwrap()) } @@ -814,12 +1063,15 @@ impl ZipWriter { *options.permissions.as_mut().unwrap() |= 0o120000; // The symlink target is stored as file content. And compressing the target path // likely wastes space. So always store. - options.compression_method = CompressionMethod::Stored; + options.compression_method = Stored; self.start_entry(name, options, None)?; self.writing_to_file = true; - self.write_all(target.into().as_bytes())?; - self.writing_to_file = false; + if let Err(e) = self.write_all(target.into().as_bytes()) { + self.abort_file().unwrap(); + return Err(e.into()); + } + self.finish_file()?; Ok(()) } @@ -828,136 +1080,199 @@ impl ZipWriter { self.finish_file()?; { + let central_start = self.write_central_and_footer()?; let writer = self.inner.get_plain(); - - let central_start = writer.stream_position()?; - for file in self.files.iter() { - write_central_directory_header(writer, file)?; + let footer_end = writer.stream_position()?; + let file_end = writer.seek(SeekFrom::End(0))?; + if footer_end < file_end { + // Data from an aborted file is past the end of the footer, so rewrite the footer at + // the actual end. + let central_and_footer_size = footer_end - central_start; + writer.seek(SeekFrom::End(-(central_and_footer_size as i64)))?; + self.write_central_and_footer()?; } - let central_size = writer.stream_position()? - central_start; - - if self.files.len() > spec::ZIP64_ENTRY_THR - || central_size.max(central_start) > spec::ZIP64_BYTES_THR - { - let zip64_footer = spec::Zip64CentralDirectoryEnd { - version_made_by: DEFAULT_VERSION as u16, - version_needed_to_extract: DEFAULT_VERSION as u16, - disk_number: 0, - disk_with_central_directory: 0, - number_of_files_on_this_disk: self.files.len() as u64, - number_of_files: self.files.len() as u64, - central_directory_size: central_size, - central_directory_offset: central_start, - }; - - zip64_footer.write(writer)?; - - let zip64_footer = spec::Zip64CentralDirectoryEndLocator { - disk_with_central_directory: 0, - end_of_central_directory_offset: central_start + central_size, - number_of_disks: 1, - }; - - zip64_footer.write(writer)?; - } - - let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16; - let footer = spec::CentralDirectoryEnd { - disk_number: 0, - disk_with_central_directory: 0, - zip_file_comment: self.comment.clone(), - number_of_files_on_this_disk: number_of_files, - number_of_files, - central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32, - central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32, - }; - - footer.write(writer)?; } Ok(()) } + + fn write_central_and_footer(&mut self) -> Result { + let writer = self.inner.get_plain(); + + let central_start = writer.stream_position()?; + for file in self.files.iter() { + write_central_directory_header(writer, file)?; + } + let central_size = writer.stream_position()? - central_start; + + if self.files.len() > spec::ZIP64_ENTRY_THR + || central_size.max(central_start) > spec::ZIP64_BYTES_THR + { + let zip64_footer = spec::Zip64CentralDirectoryEnd { + version_made_by: DEFAULT_VERSION as u16, + version_needed_to_extract: DEFAULT_VERSION as u16, + disk_number: 0, + disk_with_central_directory: 0, + number_of_files_on_this_disk: self.files.len() as u64, + number_of_files: self.files.len() as u64, + central_directory_size: central_size, + central_directory_offset: central_start, + }; + + zip64_footer.write(writer)?; + + let zip64_footer = spec::Zip64CentralDirectoryEndLocator { + disk_with_central_directory: 0, + end_of_central_directory_offset: central_start + central_size, + number_of_disks: 1, + }; + + zip64_footer.write(writer)?; + } + + let number_of_files = self.files.len().min(spec::ZIP64_ENTRY_THR) as u16; + let footer = spec::CentralDirectoryEnd { + disk_number: 0, + disk_with_central_directory: 0, + zip_file_comment: self.comment.clone(), + number_of_files_on_this_disk: number_of_files, + number_of_files, + central_directory_size: central_size.min(spec::ZIP64_BYTES_THR) as u32, + central_directory_offset: central_start.min(spec::ZIP64_BYTES_THR) as u32, + }; + + footer.write(writer)?; + Ok(central_start) + } + + fn index_by_name(&self, name: &str) -> ZipResult { + Ok(*self.files_by_name.get(name).ok_or(ZipError::FileNotFound)?) + } + + /// Adds another entry to the central directory referring to the same content as an existing + /// entry. The file's local-file header will still refer to it by its original name, so + /// unzipping the file will technically be unspecified behavior. [ZipArchive] ignores the + /// filename in the local-file header and treat the central directory as authoritative. However, + /// some other software (e.g. Minecraft) will refuse to extract a file copied this way. + pub fn shallow_copy_file(&mut self, src_name: &str, dest_name: &str) -> ZipResult<()> { + self.finish_file()?; + let src_index = self.index_by_name(src_name)?; + let mut dest_data = self.files[src_index].to_owned(); + dest_data.file_name = dest_name.into(); + self.insert_file_data(dest_data)?; + Ok(()) + } } -impl Drop for ZipWriter { +impl Drop for ZipWriter { fn drop(&mut self) { if !self.inner.is_closed() { if let Err(e) = self.finalize() { - let _ = write!(io::stderr(), "ZipWriter drop failed: {e:?}"); + let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e); } } } } -impl GenericZipWriter { - fn switch_to( - &mut self, +type SwitchWriterFunction = Box) -> GenericZipWriter>; + +impl GenericZipWriter { + fn prepare_next_writer( + &self, compression: CompressionMethod, compression_level: Option, - ) -> ZipResult<()> { - match self.current_compression() { - Some(method) if method == compression => return Ok(()), - None => { - return Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", - ) - .into()) - } - _ => {} + #[cfg(feature = "deflate-zopfli")] zopfli_buffer_size: Option, + ) -> ZipResult> { + if let Closed = self { + return Err( + io::Error::new(io::ErrorKind::BrokenPipe, "ZipWriter was already closed").into(), + ); } - let bare = match mem::replace(self, GenericZipWriter::Closed) { - GenericZipWriter::Storer(w) => w, - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - GenericZipWriter::Deflater(w) => w.finish()?, - #[cfg(feature = "bzip2")] - GenericZipWriter::Bzip2(w) => w.finish()?, - #[cfg(feature = "zstd")] - GenericZipWriter::Zstd(w) => w.finish()?, - GenericZipWriter::Closed => { - return Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", - ) - .into()) - } - }; - - *self = { + { #[allow(deprecated)] + #[allow(unreachable_code)] match compression { - CompressionMethod::Stored => { + Stored => { if compression_level.is_some() { - return Err(ZipError::UnsupportedArchive( + Err(ZipError::UnsupportedArchive( "Unsupported compression level", - )); + )) + } else { + Ok(Box::new(|bare| Storer(bare))) } - - GenericZipWriter::Storer(bare) } #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" ))] - CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new( - bare, - flate2::Compression::new( - clamp_opt( - compression_level - .unwrap_or(flate2::Compression::default().level() as i32), - deflate_compression_level_range(), - ) - .ok_or(ZipError::UnsupportedArchive( - "Unsupported compression level", - ))? as u32, - ), - )), + CompressionMethod::Deflated => { + let default = if cfg!(feature = "deflate") + || cfg!(feature = "deflate-miniz") + || cfg!(feature = "deflate-zlib") + || cfg!(feature = "deflate-zlib-ng") + { + Compression::default().level() as i32 + } else { + 24 + }; + + let level = clamp_opt( + compression_level.unwrap_or(default), + deflate_compression_level_range(), + ) + .ok_or(ZipError::UnsupportedArchive( + "Unsupported compression level", + ))? as u32; + + #[cfg(feature = "deflate-zopfli")] + { + let best_non_zopfli = Compression::best().level(); + if level > best_non_zopfli { + let options = Options { + iteration_count: NonZeroU8::try_from( + (level - best_non_zopfli) as u8, + ) + .unwrap(), + ..Default::default() + }; + return Ok(Box::new(move |bare| match zopfli_buffer_size { + Some(size) => GenericZipWriter::BufferedZopfliDeflater( + BufWriter::with_capacity( + size, + zopfli::DeflateEncoder::new( + options, + Default::default(), + bare, + ), + ), + ), + None => GenericZipWriter::ZopfliDeflater( + zopfli::DeflateEncoder::new(options, Default::default(), bare), + ), + })); + } + } + + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + ))] + { + return Ok(Box::new(move |bare| { + GenericZipWriter::Deflater(DeflateEncoder::new( + bare, + Compression::new(level), + )) + })); + } + unreachable!() + } #[cfg(feature = "deflate64")] CompressionMethod::Deflate64 => { return Err(ZipError::UnsupportedArchive( @@ -965,95 +1280,110 @@ impl GenericZipWriter { )); } #[cfg(feature = "bzip2")] - CompressionMethod::Bzip2 => GenericZipWriter::Bzip2(BzEncoder::new( - bare, - bzip2::Compression::new( - clamp_opt( - compression_level - .unwrap_or(bzip2::Compression::default().level() as i32), - bzip2_compression_level_range(), - ) - .ok_or(ZipError::UnsupportedArchive( - "Unsupported compression level", - ))? as u32, - ), - )), - CompressionMethod::AES => { - return Err(ZipError::UnsupportedArchive( - "AES compression is not supported for writing", - )) - } - #[cfg(feature = "zstd")] - CompressionMethod::Zstd => GenericZipWriter::Zstd( - ZstdEncoder::new( - bare, - clamp_opt( - compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL), - zstd::compression_level_range(), - ) - .ok_or(ZipError::UnsupportedArchive( - "Unsupported compression level", - ))?, + CompressionMethod::Bzip2 => { + let level = clamp_opt( + compression_level.unwrap_or(bzip2::Compression::default().level() as i32), + bzip2_compression_level_range(), ) - .unwrap(), - ), + .ok_or(ZipError::UnsupportedArchive( + "Unsupported compression level", + ))? as u32; + Ok(Box::new(move |bare| { + GenericZipWriter::Bzip2(BzEncoder::new( + bare, + bzip2::Compression::new(level), + )) + })) + } + CompressionMethod::AES => Err(ZipError::UnsupportedArchive( + "AES compression is not supported for writing", + )), + #[cfg(feature = "zstd")] + CompressionMethod::Zstd => { + let level = clamp_opt( + compression_level.unwrap_or(zstd::DEFAULT_COMPRESSION_LEVEL), + zstd::compression_level_range(), + ) + .ok_or(ZipError::UnsupportedArchive( + "Unsupported compression level", + ))?; + Ok(Box::new(move |bare| { + GenericZipWriter::Zstd(ZstdEncoder::new(bare, level).unwrap()) + })) + } CompressionMethod::Unsupported(..) => { - return Err(ZipError::UnsupportedArchive("Unsupported compression")) + Err(ZipError::UnsupportedArchive("Unsupported compression")) } } - }; + } + } + fn switch_to(&mut self, make_new_self: SwitchWriterFunction) -> ZipResult<()> { + let bare = match mem::replace(self, Closed) { + Storer(w) => w, + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib", + feature = "deflate-zlib-ng" + ))] + GenericZipWriter::Deflater(w) => w.finish()?, + #[cfg(feature = "deflate-zopfli")] + GenericZipWriter::ZopfliDeflater(w) => w.finish()?, + #[cfg(feature = "deflate-zopfli")] + GenericZipWriter::BufferedZopfliDeflater(w) => w.into_inner()?.finish()?, + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(w) => w.finish()?, + #[cfg(feature = "zstd")] + GenericZipWriter::Zstd(w) => w.finish()?, + Closed => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ZipWriter was already closed", + ) + .into()); + } + }; + *self = (make_new_self)(bare); Ok(()) } fn ref_mut(&mut self) -> Option<&mut dyn Write> { - match *self { - GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write), + match self { + Storer(ref mut w) => Some(w as &mut dyn Write), #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng" ))] GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write), + #[cfg(feature = "deflate-zopfli")] + GenericZipWriter::ZopfliDeflater(w) => Some(w as &mut dyn Write), + #[cfg(feature = "deflate-zopfli")] + GenericZipWriter::BufferedZopfliDeflater(w) => Some(w as &mut dyn Write), #[cfg(feature = "bzip2")] GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write), #[cfg(feature = "zstd")] GenericZipWriter::Zstd(ref mut w) => Some(w as &mut dyn Write), - GenericZipWriter::Closed => None, + Closed => None, } } - fn is_closed(&self) -> bool { + const fn is_closed(&self) -> bool { matches!(*self, GenericZipWriter::Closed) } fn get_plain(&mut self) -> &mut W { match *self { - GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w, + Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w, _ => panic!("Should have switched to stored and unencrypted beforehand"), } } - fn current_compression(&self) -> Option { - match *self { - GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated), - #[cfg(feature = "bzip2")] - GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2), - #[cfg(feature = "zstd")] - GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd), - GenericZipWriter::Closed => None, - } - } - fn unwrap(self) -> W { match self { - GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(w)) => w, + Storer(MaybeEncrypted::Unencrypted(w)) => w, _ => panic!("Should have switched to stored and unencrypted beforehand"), } } @@ -1062,17 +1392,34 @@ impl GenericZipWriter { #[cfg(any( feature = "deflate", feature = "deflate-miniz", - feature = "deflate-zlib" + feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli" ))] fn deflate_compression_level_range() -> std::ops::RangeInclusive { - let min = flate2::Compression::none().level() as i32; - let max = flate2::Compression::best().level() as i32; + let min = if cfg!(feature = "deflate") + || cfg!(feature = "deflate-miniz") + || cfg!(feature = "deflate-zlib") + || cfg!(feature = "deflate-zlib-ng") + { + Compression::none().level() as i32 + } else { + Compression::best().level() as i32 + 1 + }; + + let max = Compression::best().level() as i32 + + if cfg!(feature = "deflate-zopfli") { + u8::MAX as i32 + } else { + 0 + }; + min..=max } #[cfg(feature = "bzip2")] fn bzip2_compression_level_range() -> std::ops::RangeInclusive { - let min = bzip2::Compression::none().level() as i32; + let min = bzip2::Compression::fast().level() as i32; let max = bzip2::Compression::best().level() as i32; min..=max } @@ -1081,6 +1428,8 @@ fn bzip2_compression_level_range() -> std::ops::RangeInclusive { feature = "deflate", feature = "deflate-miniz", feature = "deflate-zlib", + feature = "deflate-zlib-ng", + feature = "deflate-zopfli", feature = "bzip2", feature = "zstd" ))] @@ -1092,55 +1441,9 @@ fn clamp_opt(value: T, range: std::ops::RangeInclusive) -> Opt } } -fn write_local_file_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { - // local file header signature - writer.write_u32::(spec::LOCAL_FILE_HEADER_SIGNATURE)?; - // version needed to extract - writer.write_u16::(file.version_needed())?; - // general purpose bit flag - let flag = if !file.file_name.is_ascii() { - 1u16 << 11 - } else { - 0 - } | if file.encrypted { 1u16 << 0 } else { 0 }; - writer.write_u16::(flag)?; - // Compression method - #[allow(deprecated)] - writer.write_u16::(file.compression_method.to_u16())?; - // last mod file time and last mod file date - writer.write_u16::(file.last_modified_time.timepart())?; - writer.write_u16::(file.last_modified_time.datepart())?; - // crc-32 - writer.write_u32::(file.crc32)?; - // compressed size and uncompressed size - if file.large_file { - writer.write_u32::(spec::ZIP64_BYTES_THR as u32)?; - writer.write_u32::(spec::ZIP64_BYTES_THR as u32)?; - } else { - writer.write_u32::(file.compressed_size as u32)?; - writer.write_u32::(file.uncompressed_size as u32)?; - } - // file name length - writer.write_u16::(file.file_name.as_bytes().len() as u16)?; - // extra field length - let extra_field_length = if file.large_file { 20 } else { 0 } + file.extra_field.len() as u16; - writer.write_u16::(extra_field_length)?; - // file name - writer.write_all(file.file_name.as_bytes())?; - // zip64 extra field - if file.large_file { - write_local_zip64_extra_field(writer, file)?; - } - - Ok(()) -} - -fn update_local_file_header( - writer: &mut T, - file: &ZipFileData, -) -> ZipResult<()> { +fn update_local_file_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { const CRC32_OFFSET: u64 = 14; - writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?; + writer.seek(SeekFrom::Start(file.header_start + CRC32_OFFSET))?; writer.write_u32::(file.crc32)?; if file.large_file { update_local_zip64_extra_field(writer, file)?; @@ -1172,7 +1475,7 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) writer.write_u16::(version_made_by)?; // version needed to extract writer.write_u16::(file.version_needed())?; - // general puprose bit flag + // general purpose bit flag let flag = if !file.file_name.is_ascii() { 1u16 << 11 } else { @@ -1194,12 +1497,16 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) // file name length writer.write_u16::(file.file_name.as_bytes().len() as u16)?; // extra field length - writer.write_u16::(zip64_extra_field_length + file.extra_field.len() as u16)?; + writer.write_u16::( + zip64_extra_field_length + + file.extra_field.len() as u16 + + file.central_extra_field.len() as u16, + )?; // file comment length writer.write_u16::(0)?; // disk number start writer.write_u16::(0)?; - // internal file attribytes + // internal file attributes writer.write_u16::(0)?; // external file attributes writer.write_u32::(file.external_attributes)?; @@ -1211,61 +1518,41 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?; // extra field writer.write_all(&file.extra_field)?; + writer.write_all(&file.central_extra_field)?; // file comment // Ok(()) } -fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> { - let mut data = file.extra_field.as_slice(); - - if data.len() > spec::ZIP64_ENTRY_THR { +fn validate_extra_data(header_id: u16, data: &[u8]) -> ZipResult<()> { + if data.len() > u16::MAX as usize { return Err(ZipError::Io(io::Error::new( - io::ErrorKind::InvalidData, - "Extra data exceeds extra field", + io::ErrorKind::Other, + "Extra-data field can't exceed u16::MAX bytes", + ))); + } + if header_id == 0x0001 { + return Err(ZipError::Io(io::Error::new( + io::ErrorKind::Other, + "No custom ZIP64 extra data allowed", ))); } - while !data.is_empty() { - let left = data.len(); - if left < 4 { - return Err(ZipError::Io(io::Error::new( - io::ErrorKind::Other, - "Incomplete extra data header", - ))); - } - let kind = data.read_u16::()?; - let size = data.read_u16::()? as usize; - let left = left - 4; - - if kind == 0x0001 { - return Err(ZipError::Io(io::Error::new( - io::ErrorKind::Other, - "No custom ZIP64 extra data allowed", - ))); - } - - #[cfg(not(feature = "unreserved"))] + #[cfg(not(feature = "unreserved"))] + { + if header_id <= 31 + || EXTRA_FIELD_MAPPING + .iter() + .any(|&mapped| mapped == header_id) { - if kind <= 31 || EXTRA_FIELD_MAPPING.iter().any(|&mapped| mapped == kind) { - return Err(ZipError::Io(io::Error::new( - io::ErrorKind::Other, - format!( - "Extra data header ID {kind:#06} requires crate feature \"unreserved\"", - ), - ))); - } - } - - if size > left { return Err(ZipError::Io(io::Error::new( io::ErrorKind::Other, - "Extra data size exceeds extra field", + format!( + "Extra data header ID {header_id:#06} requires crate feature \"unreserved\"", + ), ))); } - - data = &data[size..]; } Ok(()) @@ -1283,12 +1570,12 @@ fn write_local_zip64_extra_field(writer: &mut T, file: &ZipFileData) - Ok(()) } -fn update_local_zip64_extra_field( +fn update_local_zip64_extra_field( writer: &mut T, file: &ZipFileData, ) -> ZipResult<()> { let zip64_extra_field = file.header_start + 30 + file.file_name.as_bytes().len() as u64; - writer.seek(io::SeekFrom::Start(zip64_extra_field + 4))?; + writer.seek(SeekFrom::Start(zip64_extra_field + 4))?; writer.write_u64::(file.uncompressed_size)?; writer.write_u64::(file.compressed_size)?; // Excluded fields: @@ -1351,9 +1638,12 @@ fn path_to_string(path: &std::path::Path) -> String { mod test { use super::{FileOptions, ZipWriter}; use crate::compression::CompressionMethod; + use crate::result::ZipResult; use crate::types::DateTime; + use crate::ZipArchive; use std::io; - use std::io::Write; + use std::io::{Read, Write}; + use std::sync::Arc; #[test] fn write_empty_zip() { @@ -1473,6 +1763,11 @@ mod test { permissions: Some(33188), large_file: false, encrypt_with: None, + extra_data: Arc::new(vec![]), + central_extra_data: Arc::new(vec![]), + alignment: 1, + #[cfg(feature = "deflate-zopfli")] + zopfli_buffer_size: None, }; writer.start_file("mimetype", options).unwrap(); writer @@ -1486,19 +1781,303 @@ mod test { assert_eq!(result.get_ref(), &v); } + #[cfg(test)] + const RT_TEST_TEXT: &str = "And I can't stop thinking about the moments that I lost to you\ + And I can't stop thinking of things I used to do\ + And I can't stop making bad decisions\ + And I can't stop eating stuff you make me chew\ + I put on a smile like you wanna see\ + Another day goes by that I long to be like you"; + #[cfg(test)] + const RT_TEST_FILENAME: &str = "subfolder/sub-subfolder/can't_stop.txt"; + #[cfg(test)] + const SECOND_FILENAME: &str = "different_name.xyz"; + #[cfg(test)] + const THIRD_FILENAME: &str = "third_name.xyz"; + #[test] - fn path_to_string() { - let mut path = std::path::PathBuf::new(); - #[cfg(windows)] - path.push(r"C:\"); - #[cfg(unix)] - path.push("/"); - path.push("windows"); - path.push(".."); - path.push("."); - path.push("system32"); - let path_str = super::path_to_string(&path); - assert_eq!(path_str, "windows/system32"); + fn test_shallow_copy() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + let options = FileOptions { + compression_method: CompressionMethod::default(), + compression_level: None, + last_modified_time: DateTime::default(), + permissions: Some(33188), + large_file: false, + encrypt_with: None, + extra_data: Arc::new(vec![]), + central_extra_data: Arc::new(vec![]), + alignment: 0, + #[cfg(feature = "deflate-zopfli")] + zopfli_buffer_size: None, + }; + writer.start_file(RT_TEST_FILENAME, options).unwrap(); + writer.write_all(RT_TEST_TEXT.as_ref()).unwrap(); + writer + .shallow_copy_file(RT_TEST_FILENAME, SECOND_FILENAME) + .unwrap(); + writer + .shallow_copy_file(RT_TEST_FILENAME, SECOND_FILENAME) + .expect_err("Duplicate filename"); + let zip = writer.finish().unwrap(); + let mut writer = ZipWriter::new_append(zip).unwrap(); + writer + .shallow_copy_file(SECOND_FILENAME, SECOND_FILENAME) + .expect_err("Duplicate filename"); + let zip = writer.finish().unwrap(); + let mut reader = ZipArchive::new(zip).unwrap(); + let mut file_names: Vec<&str> = reader.file_names().collect(); + file_names.sort(); + let mut expected_file_names = vec![RT_TEST_FILENAME, SECOND_FILENAME]; + expected_file_names.sort(); + assert_eq!(file_names, expected_file_names); + let mut first_file_content = String::new(); + reader + .by_name(RT_TEST_FILENAME) + .unwrap() + .read_to_string(&mut first_file_content) + .unwrap(); + assert_eq!(first_file_content, RT_TEST_TEXT); + let mut second_file_content = String::new(); + reader + .by_name(SECOND_FILENAME) + .unwrap() + .read_to_string(&mut second_file_content) + .unwrap(); + assert_eq!(second_file_content, RT_TEST_TEXT); + } + + #[test] + fn test_deep_copy() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + let options = FileOptions { + compression_method: CompressionMethod::default(), + compression_level: None, + last_modified_time: DateTime::default(), + permissions: Some(33188), + large_file: false, + encrypt_with: None, + extra_data: Arc::new(vec![]), + central_extra_data: Arc::new(vec![]), + alignment: 0, + #[cfg(feature = "deflate-zopfli")] + zopfli_buffer_size: None, + }; + writer.start_file(RT_TEST_FILENAME, options).unwrap(); + writer.write_all(RT_TEST_TEXT.as_ref()).unwrap(); + writer + .deep_copy_file(RT_TEST_FILENAME, SECOND_FILENAME) + .unwrap(); + let zip = writer.finish().unwrap(); + let mut writer = ZipWriter::new_append(zip).unwrap(); + writer + .deep_copy_file(RT_TEST_FILENAME, THIRD_FILENAME) + .unwrap(); + let zip = writer.finish().unwrap(); + let mut reader = ZipArchive::new(zip).unwrap(); + let mut file_names: Vec<&str> = reader.file_names().collect(); + file_names.sort(); + let mut expected_file_names = vec![RT_TEST_FILENAME, SECOND_FILENAME, THIRD_FILENAME]; + expected_file_names.sort(); + assert_eq!(file_names, expected_file_names); + let mut first_file_content = String::new(); + reader + .by_name(RT_TEST_FILENAME) + .unwrap() + .read_to_string(&mut first_file_content) + .unwrap(); + assert_eq!(first_file_content, RT_TEST_TEXT); + let mut second_file_content = String::new(); + reader + .by_name(SECOND_FILENAME) + .unwrap() + .read_to_string(&mut second_file_content) + .unwrap(); + assert_eq!(second_file_content, RT_TEST_TEXT); + } + + #[test] + fn duplicate_filenames() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file("foo/bar/test", FileOptions::default()) + .unwrap(); + writer + .write_all("The quick brown 🦊 jumps over the lazy 🐕".as_bytes()) + .unwrap(); + writer + .start_file("foo/bar/test", FileOptions::default()) + .expect_err("Expected duplicate filename not to be allowed"); + } + + #[test] + fn test_filename_looks_like_zip64_locator() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file( + "PK\u{6}\u{7}\0\0\0\u{11}\0\0\0\0\0\0\0\0\0\0\0\0", + FileOptions::default(), + ) + .unwrap(); + let zip = writer.finish().unwrap(); + let _ = ZipArchive::new(zip).unwrap(); + } + + #[test] + fn test_filename_looks_like_zip64_locator_2() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file( + "PK\u{6}\u{6}\0\0\0\0\0\0\0\0\0\0PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", + FileOptions::default(), + ) + .unwrap(); + let zip = writer.finish().unwrap(); + println!("{:02x?}", zip.get_ref()); + let _ = ZipArchive::new(zip).unwrap(); + } + + #[test] + fn test_filename_looks_like_zip64_locator_2a() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file( + "PK\u{6}\u{6}PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", + FileOptions::default(), + ) + .unwrap(); + let zip = writer.finish().unwrap(); + println!("{:02x?}", zip.get_ref()); + let _ = ZipArchive::new(zip).unwrap(); + } + + #[test] + fn test_filename_looks_like_zip64_locator_3() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file("\0PK\u{6}\u{6}", FileOptions::default()) + .unwrap(); + writer + .start_file( + "\0\u{4}\0\0PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{3}", + FileOptions::default(), + ) + .unwrap(); + let zip = writer.finish().unwrap(); + println!("{:02x?}", zip.get_ref()); + let _ = ZipArchive::new(zip).unwrap(); + } + + #[test] + fn test_filename_looks_like_zip64_locator_4() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file("PK\u{6}\u{6}", FileOptions::default()) + .unwrap(); + writer + .start_file("\0\0\0\0\0\0", FileOptions::default()) + .unwrap(); + writer.start_file("\0", FileOptions::default()).unwrap(); + writer.start_file("", FileOptions::default()).unwrap(); + writer.start_file("\0\0", FileOptions::default()).unwrap(); + writer + .start_file( + "\0\0\0PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", + FileOptions::default(), + ) + .unwrap(); + let zip = writer.finish().unwrap(); + println!("{:02x?}", zip.get_ref()); + let _ = ZipArchive::new(zip).unwrap(); + } + + #[test] + fn test_filename_looks_like_zip64_locator_5() -> ZipResult<()> { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .add_directory("", FileOptions::default().with_alignment(21)) + .unwrap(); + let mut writer = ZipWriter::new_append(writer.finish().unwrap()).unwrap(); + writer.shallow_copy_file("/", "").unwrap(); + writer.shallow_copy_file("", "\0").unwrap(); + writer.shallow_copy_file("\0", "PK\u{6}\u{6}").unwrap(); + let mut writer = ZipWriter::new_append(writer.finish().unwrap()).unwrap(); + writer + .start_file("\0\0\0\0\0\0", FileOptions::default()) + .unwrap(); + let mut writer = ZipWriter::new_append(writer.finish().unwrap()).unwrap(); + writer + .start_file( + "#PK\u{6}\u{7}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", + FileOptions::default(), + ) + .unwrap(); + let zip = writer.finish().unwrap(); + println!("{:02x?}", zip.get_ref()); + let _ = ZipArchive::new(zip).unwrap(); + Ok(()) + } + + #[test] + fn remove_shallow_copy_keeps_original() -> ZipResult<()> { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file("original", FileOptions::default()) + .unwrap(); + writer.write_all(RT_TEST_TEXT.as_bytes()).unwrap(); + writer + .shallow_copy_file("original", "shallow_copy") + .unwrap(); + writer.abort_file().unwrap(); + let mut zip = ZipArchive::new(writer.finish().unwrap()).unwrap(); + let mut file = zip.by_name("original").unwrap(); + let mut contents = Vec::new(); + file.read_to_end(&mut contents).unwrap(); + assert_eq!(RT_TEST_TEXT.as_bytes(), contents); + Ok(()) + } + + #[test] + fn remove_encrypted_file() -> ZipResult<()> { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + let first_file_options = FileOptions::default() + .with_alignment(65535) + .with_deprecated_encryption(b"Password"); + writer.start_file("", first_file_options).unwrap(); + writer.abort_file().unwrap(); + let zip = writer.finish().unwrap(); + let mut writer = ZipWriter::new(zip); + writer.start_file("", FileOptions::default()).unwrap(); + Ok(()) + } + + #[test] + fn remove_encrypted_aligned_symlink() -> ZipResult<()> { + let mut options = FileOptions::default(); + options = options.with_deprecated_encryption(b"Password"); + options.alignment = 65535; + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer.add_symlink("", "s\t\0\0ggggg\0\0", options).unwrap(); + writer.abort_file().unwrap(); + let zip = writer.finish().unwrap(); + println!("{:0>2x?}", zip.get_ref()); + let mut writer = ZipWriter::new_append(zip).unwrap(); + writer.start_file("", FileOptions::default()).unwrap(); + Ok(()) + } + + #[cfg(feature = "deflate-zopfli")] + #[test] + fn zopfli_empty_write() -> ZipResult<()> { + let mut options = FileOptions::default(); + options = options + .compression_method(CompressionMethod::default()) + .compression_level(Some(264)); + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer.start_file("", options).unwrap(); + writer.write_all(&[]).unwrap(); + writer.write_all(&[]).unwrap(); + Ok(()) } } diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs index c3696e4d..43ef1048 100644 --- a/src/zipcrypto.rs +++ b/src/zipcrypto.rs @@ -3,18 +3,40 @@ //! The following paper was used to implement the ZipCrypto algorithm: //! [https://courses.cs.ut.ee/MTAT.07.022/2015_fall/uploads/Main/dmitri-report-f15-16.pdf](https://courses.cs.ut.ee/MTAT.07.022/2015_fall/uploads/Main/dmitri-report-f15-16.pdf) +use std::fmt::{Debug, Formatter}; +use std::hash::Hash; use std::num::Wrapping; /// A container to hold the current key state -#[derive(Clone, Copy)] +#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))] +#[derive(Clone, Copy, Hash, Ord, PartialOrd, Eq, PartialEq)] pub(crate) struct ZipCryptoKeys { key_0: Wrapping, key_1: Wrapping, key_2: Wrapping, } +impl Debug for ZipCryptoKeys { + #[allow(unreachable_code)] + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + #[cfg(not(any(test, fuzzing)))] + { + use std::collections::hash_map::DefaultHasher; + use std::hash::Hasher; + let mut t = DefaultHasher::new(); + self.hash(&mut t); + return f.write_fmt(format_args!("ZipCryptoKeys(hash {})", t.finish())); + } + #[cfg(any(test, fuzzing))] + return f.write_fmt(format_args!( + "ZipCryptoKeys({:#10x},{:#10x},{:#10x})", + self.key_0, self.key_1, self.key_2 + )); + } +} + impl ZipCryptoKeys { - fn new() -> ZipCryptoKeys { + const fn new() -> ZipCryptoKeys { ZipCryptoKeys { key_0: Wrapping(0x12345678), key_1: Wrapping(0x23456789), @@ -123,12 +145,14 @@ impl ZipCryptoReader { Ok(Some(ZipCryptoReaderValid { reader: self })) } } +#[allow(unused)] pub(crate) struct ZipCryptoWriter { pub(crate) writer: W, pub(crate) buffer: Vec, pub(crate) keys: ZipCryptoKeys, } impl ZipCryptoWriter { + #[allow(unused)] pub(crate) fn finish(mut self, crc32: u32) -> std::io::Result { self.buffer[11] = (crc32 >> 24) as u8; for byte in self.buffer.iter_mut() { diff --git a/tests/aes_encryption.rs b/tests/aes_encryption.rs index 4b393ebf..eaad6888 100644 --- a/tests/aes_encryption.rs +++ b/tests/aes_encryption.rs @@ -1,7 +1,7 @@ #![cfg(feature = "aes-crypto")] use std::io::{self, Read}; -use zip::ZipArchive; +use zip_next::ZipArchive; const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet"; diff --git a/tests/data/zip64_magic_in_filename_1.zip b/tests/data/zip64_magic_in_filename_1.zip new file mode 100644 index 00000000..18b9494a Binary files /dev/null and b/tests/data/zip64_magic_in_filename_1.zip differ diff --git a/tests/data/zip64_magic_in_filename_2.zip b/tests/data/zip64_magic_in_filename_2.zip new file mode 100644 index 00000000..5628acd4 Binary files /dev/null and b/tests/data/zip64_magic_in_filename_2.zip differ diff --git a/tests/data/zip64_magic_in_filename_3.zip b/tests/data/zip64_magic_in_filename_3.zip new file mode 100644 index 00000000..4398604c Binary files /dev/null and b/tests/data/zip64_magic_in_filename_3.zip differ diff --git a/tests/data/zip64_magic_in_filename_4.zip b/tests/data/zip64_magic_in_filename_4.zip new file mode 100644 index 00000000..9726a982 Binary files /dev/null and b/tests/data/zip64_magic_in_filename_4.zip differ diff --git a/tests/data/zip64_magic_in_filename_5.zip b/tests/data/zip64_magic_in_filename_5.zip new file mode 100644 index 00000000..bf6a95db Binary files /dev/null and b/tests/data/zip64_magic_in_filename_5.zip differ diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs index 53bea2a4..4a836464 100644 --- a/tests/end_to_end.rs +++ b/tests/end_to_end.rs @@ -3,8 +3,9 @@ use std::collections::HashSet; use std::io::prelude::*; use std::io::{Cursor, Seek}; use std::iter::FromIterator; -use zip::write::FileOptions; -use zip::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; +use zip_next::result::ZipResult; +use zip_next::write::FileOptions; +use zip_next::{CompressionMethod, ZipWriter, SUPPORTED_COMPRESSION_METHODS}; // This test asserts that after creating a zip file, then reading its contents back out, // the extracted data will *always* be exactly the same as the original data. @@ -17,10 +18,11 @@ fn end_to_end() { let file = &mut Cursor::new(Vec::new()); println!("Writing file with {method} compression"); - write_test_archive(file, method).expect("Couldn't write test zip archive"); + write_test_archive(file, method, true); println!("Checking file contents"); check_archive_file(file, ENTRY_NAME, Some(method), LOREM_IPSUM); + check_archive_file(file, INTERNAL_COPY_ENTRY_NAME, Some(method), LOREM_IPSUM); } } @@ -33,13 +35,13 @@ fn copy() { continue } let src_file = &mut Cursor::new(Vec::new()); - write_test_archive(src_file, method).expect("Couldn't write to test file"); + write_test_archive(src_file, method, false); let mut tgt_file = &mut Cursor::new(Vec::new()); { - let mut src_archive = zip::ZipArchive::new(src_file).unwrap(); - let mut zip = zip::ZipWriter::new(&mut tgt_file); + let mut src_archive = zip_next::ZipArchive::new(src_file).unwrap(); + let mut zip = ZipWriter::new(&mut tgt_file); { let file = src_archive @@ -59,7 +61,7 @@ fn copy() { } } - let mut tgt_archive = zip::ZipArchive::new(tgt_file).unwrap(); + let mut tgt_archive = zip_next::ZipArchive::new(tgt_file).unwrap(); check_archive_file_contents(&mut tgt_archive, ENTRY_NAME, LOREM_IPSUM); check_archive_file_contents(&mut tgt_archive, COPY_ENTRY_NAME, LOREM_IPSUM); @@ -74,59 +76,68 @@ fn append() { if method == CompressionMethod::DEFLATE64 { continue } - let mut file = &mut Cursor::new(Vec::new()); - write_test_archive(file, method).expect("Couldn't write to test file"); + for shallow_copy in &[false, true] { + println!("Writing file with {method} compression, shallow_copy {shallow_copy}"); + let mut file = &mut Cursor::new(Vec::new()); + write_test_archive(file, method, *shallow_copy); - { - let mut zip = zip::ZipWriter::new_append(&mut file).unwrap(); - zip.start_file( - COPY_ENTRY_NAME, - FileOptions::default().compression_method(method), - ) - .unwrap(); - zip.write_all(LOREM_IPSUM).unwrap(); - zip.finish().unwrap(); + { + let mut zip = ZipWriter::new_append(&mut file).unwrap(); + zip.start_file( + COPY_ENTRY_NAME, + FileOptions::default() + .compression_method(method) + .unix_permissions(0o755), + ) + .unwrap(); + zip.write_all(LOREM_IPSUM).unwrap(); + zip.finish().unwrap(); + } + + let mut zip = zip_next::ZipArchive::new(&mut file).unwrap(); + check_archive_file_contents(&mut zip, ENTRY_NAME, LOREM_IPSUM); + check_archive_file_contents(&mut zip, COPY_ENTRY_NAME, LOREM_IPSUM); + check_archive_file_contents(&mut zip, INTERNAL_COPY_ENTRY_NAME, LOREM_IPSUM); } - - let mut zip = zip::ZipArchive::new(&mut file).unwrap(); - check_archive_file_contents(&mut zip, ENTRY_NAME, LOREM_IPSUM); - check_archive_file_contents(&mut zip, COPY_ENTRY_NAME, LOREM_IPSUM); } } // Write a test zip archive to buffer. -fn write_test_archive( - file: &mut Cursor>, - method: CompressionMethod, -) -> zip::result::ZipResult<()> { - let mut zip = zip::ZipWriter::new(file); +fn write_test_archive(file: &mut Cursor>, method: CompressionMethod, shallow_copy: bool) { + let mut zip = ZipWriter::new(file); - zip.add_directory("test/", Default::default())?; + zip.add_directory("test/", Default::default()).unwrap(); - let options = FileOptions::default() + let mut options = FileOptions::default() .compression_method(method) .unix_permissions(0o755); - zip.start_file("test/☃.txt", options)?; - zip.write_all(b"Hello, World!\n")?; + zip.start_file(ENTRY_NAME, options.clone()).unwrap(); + zip.write_all(LOREM_IPSUM).unwrap(); - zip.start_file_with_extra_data("test_with_extra_data/🐢.txt", options)?; - zip.write_u16::(0xbeef)?; - zip.write_u16::(EXTRA_DATA.len() as u16)?; - zip.write_all(EXTRA_DATA)?; - zip.end_extra_data()?; - zip.write_all(b"Hello, World! Again.\n")?; + if shallow_copy { + zip.shallow_copy_file(ENTRY_NAME, INTERNAL_COPY_ENTRY_NAME) + .unwrap(); + } else { + zip.deep_copy_file(ENTRY_NAME, INTERNAL_COPY_ENTRY_NAME) + .unwrap(); + } - zip.start_file(ENTRY_NAME, options)?; - zip.write_all(LOREM_IPSUM)?; + zip.start_file("test/☃.txt", options.clone()).unwrap(); + zip.write_all(b"Hello, World!\n").unwrap(); - zip.finish()?; - Ok(()) + options.add_extra_data(0xbeef, EXTRA_DATA, false).unwrap(); + + zip.start_file("test_with_extra_data/🐢.txt", options) + .unwrap(); + zip.write_all(b"Hello, World! Again.\n").unwrap(); + + zip.finish().unwrap(); } // Load an archive from buffer and check for test data. -fn check_test_archive(zip_file: R) -> zip::result::ZipResult> { - let mut archive = zip::ZipArchive::new(zip_file).unwrap(); +fn check_test_archive(zip_file: R) -> ZipResult> { + let mut archive = zip_next::ZipArchive::new(zip_file).unwrap(); // Check archive contains expected file names. { @@ -135,6 +146,7 @@ fn check_test_archive(zip_file: R) -> zip::result::ZipResult>(); @@ -156,9 +168,9 @@ fn check_test_archive(zip_file: R) -> zip::result::ZipResult( - archive: &mut zip::ZipArchive, + archive: &mut zip_next::ZipArchive, name: &str, -) -> zip::result::ZipResult { +) -> ZipResult { let mut file = archive.by_name(name)?; let mut contents = String::new(); @@ -192,10 +204,13 @@ fn check_archive_file( // Check a file in the archive contains the given data. fn check_archive_file_contents( - archive: &mut zip::ZipArchive, + archive: &mut zip_next::ZipArchive, name: &str, expected: &[u8], ) { + let file_permissions: u32 = archive.by_name(name).unwrap().unix_mode().unwrap(); + assert_eq!(file_permissions, 0o100755); + let file_contents: String = read_archive_file(archive, name).unwrap(); assert_eq!(file_contents.as_bytes(), expected); } @@ -212,3 +227,5 @@ const EXTRA_DATA: &[u8] = b"Extra Data"; const ENTRY_NAME: &str = "test/lorem_ipsum.txt"; const COPY_ENTRY_NAME: &str = "test/lorem_ipsum_renamed.txt"; + +const INTERNAL_COPY_ENTRY_NAME: &str = "test/lorem_ipsum_copied.txt"; diff --git a/tests/invalid_date.rs b/tests/invalid_date.rs index 3f24e251..b684bf6a 100644 --- a/tests/invalid_date.rs +++ b/tests/invalid_date.rs @@ -1,5 +1,5 @@ use std::io::Cursor; -use zip::read::ZipArchive; +use zip_next::read::ZipArchive; const BUF: &[u8] = &[ 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/tests/issue_234.rs b/tests/issue_234.rs index f8c1d2c8..48e5d668 100644 --- a/tests/issue_234.rs +++ b/tests/issue_234.rs @@ -1,4 +1,4 @@ -use zip::result::ZipError; +use zip_next::result::ZipError; const BUF: &[u8] = &[ 0, 80, 75, 1, 2, 127, 120, 0, 3, 3, 75, 80, 232, 3, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 7, 0, 0, 0, @@ -23,7 +23,7 @@ const BUF: &[u8] = &[ #[test] fn invalid_header() { let reader = std::io::Cursor::new(&BUF); - let archive = zip::ZipArchive::new(reader); + let archive = zip_next::ZipArchive::new(reader); match archive { Err(ZipError::InvalidArchive(_)) => {} value => panic!("Unexpected value: {value:?}"), diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index 468ef198..7cbdd1b5 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -190,7 +190,7 @@ impl Read for Zip64File { #[test] fn zip64_large() { let zipfile = Zip64File::new(); - let mut archive = zip::ZipArchive::new(zipfile).unwrap(); + let mut archive = zip_next::ZipArchive::new(zipfile).unwrap(); let mut buf = [0u8; 32]; for i in 0..archive.len() { diff --git a/tests/zip_comment_garbage.rs b/tests/zip_comment_garbage.rs index ef4d9750..73702a08 100644 --- a/tests/zip_comment_garbage.rs +++ b/tests/zip_comment_garbage.rs @@ -18,7 +18,7 @@ // 0000002e use std::io; -use zip::ZipArchive; +use zip_next::ZipArchive; #[test] fn correctly_handle_zip_with_garbage_after_comment() { diff --git a/tests/zip_crypto.rs b/tests/zip_crypto.rs index d831c1e6..45612aac 100644 --- a/tests/zip_crypto.rs +++ b/tests/zip_crypto.rs @@ -22,20 +22,24 @@ use std::io::Read; #[test] fn encrypting_file() { - use zip::unstable::write::FileOptionsExt; use std::io::{Read, Write}; + use zip_next::unstable::write::FileOptionsExt; let mut buf = vec![0; 2048]; - let mut archive = zip::write::ZipWriter::new(std::io::Cursor::new(&mut buf)); - archive.start_file("name", zip::write::FileOptions::default().with_deprecated_encryption(b"password")).unwrap(); + let mut archive = zip_next::write::ZipWriter::new(Cursor::new(&mut buf)); + archive + .start_file( + "name", + zip_next::write::FileOptions::default().with_deprecated_encryption(b"password"), + ) + .unwrap(); archive.write_all(b"test").unwrap(); archive.finish().unwrap(); drop(archive); - let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&mut buf)).unwrap(); + let mut archive = zip_next::ZipArchive::new(Cursor::new(&mut buf)).unwrap(); let mut file = archive.by_index_decrypt(0, b"password").unwrap().unwrap(); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); assert_eq!(buf, b"test"); - } #[test] fn encrypted_file() { @@ -56,7 +60,7 @@ fn encrypted_file() { 0x00, 0x00, ]); - let mut archive = zip::ZipArchive::new(zip_file_bytes).unwrap(); + let mut archive = zip_next::ZipArchive::new(zip_file_bytes).unwrap(); assert_eq!(archive.len(), 1); //Only one file inside archive: `test.txt` @@ -64,8 +68,8 @@ fn encrypted_file() { // No password let file = archive.by_index(0); match file { - Err(zip::result::ZipError::UnsupportedArchive( - zip::result::ZipError::PASSWORD_REQUIRED, + Err(zip_next::result::ZipError::UnsupportedArchive( + zip_next::result::ZipError::PASSWORD_REQUIRED, )) => (), Err(_) => panic!( "Expected PasswordRequired error when opening encrypted file without password" @@ -78,7 +82,7 @@ fn encrypted_file() { // Wrong password let file = archive.by_index_decrypt(0, b"wrong password"); match file { - Ok(Err(zip::result::InvalidPassword)) => (), + Ok(Err(zip_next::result::InvalidPassword)) => (), Err(_) => panic!( "Expected InvalidPassword error when opening encrypted file with wrong password" ),