aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid LeGare <legare@google.com>2022-04-13 22:18:08 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-04-13 22:18:08 +0000
commit3903686638ba21de4c640890140ab8f7f14a1058 (patch)
tree5488828d7446ad70a03375c5837022ef5b4e32e4
parent2095b106a168c52e7e17543d3aef87545666aa4e (diff)
parent1b6000bc1ced90425b09760988f2d0089d93d557 (diff)
downloadzip-3903686638ba21de4c640890140ab8f7f14a1058.tar.gz
Upgrade rust/crates/zip to 0.6.2 am: 132eccb845 am: 1b6000bc1c
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/zip/+/2061160 Change-Id: Ieebcedab103c5c4f870625223e33f999d7a9a70f Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.cargo_vcs_info.json7
-rw-r--r--.github/workflows/ci.yaml23
-rw-r--r--Android.bp8
-rw-r--r--CODE_OF_CONDUCT.md1
-rw-r--r--Cargo.toml61
-rw-r--r--Cargo.toml.orig31
-rw-r--r--METADATA10
-rw-r--r--README.md17
-rw-r--r--benches/read_entry.rs4
-rw-r--r--cargo2android.json3
-rw-r--r--examples/extract.rs5
-rw-r--r--examples/extract_lorem.rs2
-rw-r--r--examples/file_info.rs5
-rw-r--r--examples/stdin_info.rs3
-rw-r--r--examples/write_dir.rs13
-rw-r--r--examples/write_sample.rs4
-rw-r--r--patches/Android.bp.diff14
-rw-r--r--src/aes.rs185
-rw-r--r--src/aes_ctr.rs281
-rw-r--r--src/compression.rs65
-rw-r--r--src/cp437.rs3
-rw-r--r--src/crc32.rs21
-rw-r--r--src/lib.rs29
-rw-r--r--src/read.rs331
-rw-r--r--src/result.rs49
-rw-r--r--src/spec.rs3
-rw-r--r--src/types.rs290
-rw-r--r--src/write.rs355
-rw-r--r--src/zipcrypto.rs8
-rw-r--r--tests/aes_encryption.rs80
-rw-r--r--tests/data/aes_archive.zipbin0 -> 908 bytes
-rw-r--r--tests/end_to_end.rs164
-rw-r--r--tests/issue_234.rs31
33 files changed, 1625 insertions, 481 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
index b1502c1..b50dc02 100644
--- a/.cargo_vcs_info.json
+++ b/.cargo_vcs_info.json
@@ -1,5 +1,6 @@
{
"git": {
- "sha1": "7edf2489d5cff8b80f02ee6fc5febf3efd0a9442"
- }
-}
+ "sha1": "4f7609cec700765525a537747c8f340dd1090aa0"
+ },
+ "path_in_vcs": ""
+} \ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8374189..6f0e4b9 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
- rust: [stable, 1.36.0]
+ rust: [stable, 1.54.0]
steps:
- uses: actions/checkout@master
@@ -39,6 +39,25 @@ jobs:
command: test
args: --all
+ clippy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: nightly
+ override: true
+ components: clippy
+
+ - name: clippy
+ uses: actions-rs/cargo@v1
+ with:
+ command: clippy
+ args: --all-targets --all-features -- -D warnings
+
check_fmt_and_docs:
name: Checking fmt and docs
runs-on: ubuntu-latest
@@ -54,4 +73,4 @@ jobs:
run: cargo fmt --all -- --check
- name: Docs
- run: cargo doc \ No newline at end of file
+ run: cargo doc
diff --git a/Android.bp b/Android.bp
index 8e1f3e7..c0b2349 100644
--- a/Android.bp
+++ b/Android.bp
@@ -23,7 +23,7 @@ rust_library {
host_supported: true,
crate_name: "zip",
cargo_env_compat: true,
- cargo_pkg_version: "0.5.13",
+ cargo_pkg_version: "0.6.2",
srcs: ["src/lib.rs"],
edition: "2018",
features: [
@@ -34,10 +34,14 @@ rust_library {
"libbyteorder",
"libcrc32fast",
"libflate2",
- "libthiserror",
],
apex_available: [
"//apex_available:platform",
"com.android.virt",
],
+ arch: {
+ arm: {
+ rustlibs: ["libcrossbeam_utils"],
+ },
+ },
}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 845634e..2290ec2 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,4 +1,3 @@
-
# Contributor Covenant Code of Conduct
## Our Pledge
diff --git a/Cargo.toml b/Cargo.toml
index e964f60..fe1a031 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,17 +3,16 @@
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
-# to registry (e.g., crates.io) dependencies
+# to registry (e.g., crates.io) dependencies.
#
-# If you believe there's an error in this file please file an
-# issue against the rust-lang/cargo repository. If you're
-# editing this file be aware that the upstream Cargo.toml
-# will likely look very different (and much more reasonable)
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
[package]
edition = "2018"
name = "zip"
-version = "0.5.13"
+version = "0.6.2"
authors = ["Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", "Ryan Levick <ryan.levick@gmail.com>"]
description = "Library to support the reading and writing of zip files.\n"
keywords = ["zip", "archive"]
@@ -23,39 +22,65 @@ repository = "https://github.com/zip-rs/zip.git"
[[bench]]
name = "read_entry"
harness = false
+[dependencies.aes]
+version = "0.7.5"
+optional = true
+
[dependencies.byteorder]
-version = "1.3"
+version = "1.4.3"
[dependencies.bzip2]
-version = "0.4"
+version = "0.4.3"
+optional = true
+
+[dependencies.constant_time_eq]
+version = "0.1.5"
optional = true
[dependencies.crc32fast]
-version = "1.0"
+version = "1.3.2"
[dependencies.flate2]
-version = "1.0.0"
+version = "1.0.22"
optional = true
default-features = false
-[dependencies.thiserror]
-version = "1.0"
+[dependencies.hmac]
+version = "0.12.1"
+features = ["reset"]
+optional = true
+
+[dependencies.pbkdf2]
+version = "0.10.1"
+optional = true
+
+[dependencies.sha1]
+version = "0.10.1"
+optional = true
[dependencies.time]
-version = "0.1"
+version = "0.3.7"
+features = ["formatting", "macros"]
+optional = true
+
+[dependencies.zstd]
+version = "0.10.0"
optional = true
[dev-dependencies.bencher]
-version = "0.1"
+version = "0.1.5"
-[dev-dependencies.rand]
-version = "0.7"
+[dev-dependencies.getrandom]
+version = "0.2.5"
[dev-dependencies.walkdir]
-version = "2"
+version = "2.3.2"
[features]
-default = ["bzip2", "deflate", "time"]
+aes-crypto = ["aes", "constant_time_eq", "hmac", "pbkdf2", "sha1"]
+default = ["aes-crypto", "bzip2", "deflate", "time", "zstd"]
deflate = ["flate2/rust_backend"]
deflate-miniz = ["flate2/default"]
deflate-zlib = ["flate2/zlib"]
unreserved = []
+[target."cfg(any(all(target_arch = \"arm\", target_pointer_width = \"32\"), target_arch = \"mips\", target_arch = \"powerpc\"))".dependencies.crossbeam-utils]
+version = "0.8.8"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
index 2ce7e60..cc87821 100644
--- a/Cargo.toml.orig
+++ b/Cargo.toml.orig
@@ -1,6 +1,6 @@
[package]
name = "zip"
-version = "0.5.13"
+version = "0.6.2"
authors = ["Mathijs van de Nes <git@mathijs.vd-nes.nl>", "Marli Frost <marli@frost.red>", "Ryan Levick <ryan.levick@gmail.com>"]
license = "MIT"
repository = "https://github.com/zip-rs/zip.git"
@@ -11,24 +11,33 @@ Library to support the reading and writing of zip files.
edition = "2018"
[dependencies]
-flate2 = { version = "1.0.0", default-features = false, optional = true }
-time = { version = "0.1", optional = true }
-byteorder = "1.3"
-bzip2 = { version = "0.4", optional = true }
-crc32fast = "1.0"
-thiserror = "1.0"
+aes = { version = "0.7.5", optional = true }
+byteorder = "1.4.3"
+bzip2 = { version = "0.4.3", optional = true }
+constant_time_eq = { version = "0.1.5", optional = true }
+crc32fast = "1.3.2"
+flate2 = { version = "1.0.22", default-features = false, optional = true }
+hmac = { version = "0.12.1", optional = true, features = ["reset"] }
+pbkdf2 = {version = "0.10.1", optional = true }
+sha1 = {version = "0.10.1", optional = true }
+time = { version = "0.3.7", features = ["formatting", "macros" ], optional = true }
+zstd = { version = "0.10.0", 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"
[dev-dependencies]
-bencher = "0.1"
-rand = "0.7"
-walkdir = "2"
+bencher = "0.1.5"
+getrandom = "0.2.5"
+walkdir = "2.3.2"
[features]
+aes-crypto = [ "aes", "constant_time_eq", "hmac", "pbkdf2", "sha1" ]
deflate = ["flate2/rust_backend"]
deflate-miniz = ["flate2/default"]
deflate-zlib = ["flate2/zlib"]
unreserved = []
-default = ["bzip2", "deflate", "time"]
+default = ["aes-crypto", "bzip2", "deflate", "time", "zstd"]
[[bench]]
name = "read_entry"
diff --git a/METADATA b/METADATA
index 33abb6b..c6e83c0 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://static.crates.io/crates/zip/zip-0.5.13.crate"
+ value: "https://static.crates.io/crates/zip/zip-0.6.2.crate"
}
- version: "0.5.13"
+ version: "0.6.2"
license_type: NOTICE
last_upgrade_date {
- year: 2021
- month: 6
- day: 21
+ year: 2022
+ month: 4
+ day: 13
}
}
diff --git a/README.md b/README.md
index e489b98..7db31d4 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,17 @@ zip-rs
[![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)
-[Documentation](https://docs.rs/zip/0.5.13/zip/)
+[Documentation](https://docs.rs/zip/0.6.2/zip/)
+> PSA: This version of the ZIP crate will not gain any new features,
+> and will only be updated if major security issues are found.
Info
----
+
A zip library for rust which supports reading and writing of simple ZIP files.
Supported compression formats:
@@ -17,6 +21,7 @@ Supported compression formats:
* stored (i.e. none)
* deflate
* bzip2
+* zstd
Currently unsupported zip extensions:
@@ -30,28 +35,30 @@ With all default features:
```toml
[dependencies]
-zip = "0.5"
+zip = "0.6.2"
```
Without the default features:
```toml
[dependencies]
-zip = { version = "0.5", default-features = false }
+zip = { version = "0.6.2", default-features = false }
```
The features available are:
-* `deflate`: Enables the deflate compression algorithm, which is the default for zipfiles
+* `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.
* `bzip2`: Enables the BZip2 compression algorithm.
* `time`: Enables features using the [time](https://github.com/rust-lang-deprecated/time) crate.
+* `zstd`: Enables the Zstandard compression algorithm.
All of these are enabled by default.
MSRV
----
-Our current Minimum Supported Rust Version is **1.36.0**. When adding features,
+Our current Minimum Supported Rust Version is **1.54.0**. When adding features,
we will follow these guidelines:
- We will always support the latest four minor Rust versions. This gives you a 6
diff --git a/benches/read_entry.rs b/benches/read_entry.rs
index 25c0b94..af9affe 100644
--- a/benches/read_entry.rs
+++ b/benches/read_entry.rs
@@ -3,7 +3,7 @@ use bencher::{benchmark_group, benchmark_main};
use std::io::{Cursor, Read, Write};
use bencher::Bencher;
-use rand::Rng;
+use getrandom::getrandom;
use zip::{ZipArchive, ZipWriter};
fn generate_random_archive(size: usize) -> Vec<u8> {
@@ -14,7 +14,7 @@ fn generate_random_archive(size: usize) -> Vec<u8> {
writer.start_file("random.dat", options).unwrap();
let mut bytes = vec![0u8; size];
- rand::thread_rng().fill_bytes(&mut bytes);
+ getrandom(&mut bytes).unwrap();
writer.write_all(&bytes).unwrap();
writer.finish().unwrap().into_inner()
diff --git a/cargo2android.json b/cargo2android.json
index 51581bb..42d8c8b 100644
--- a/cargo2android.json
+++ b/cargo2android.json
@@ -5,6 +5,7 @@
],
"dependencies": true,
"device": true,
+ "patch": "patches/Android.bp.diff",
"features": "deflate-zlib",
"run": true
-} \ No newline at end of file
+}
diff --git a/examples/extract.rs b/examples/extract.rs
index 05c5a4a..7b8860c 100644
--- a/examples/extract.rs
+++ b/examples/extract.rs
@@ -30,7 +30,7 @@ fn real_main() -> i32 {
}
}
- if (&*file.name()).ends_with('/') {
+ if (*file.name()).ends_with('/') {
println!("File {} extracted to \"{}\"", i, outpath.display());
fs::create_dir_all(&outpath).unwrap();
} else {
@@ -59,5 +59,6 @@ fn real_main() -> i32 {
}
}
}
- return 0;
+
+ 0
}
diff --git a/examples/extract_lorem.rs b/examples/extract_lorem.rs
index 89e33ef..a34a04f 100644
--- a/examples/extract_lorem.rs
+++ b/examples/extract_lorem.rs
@@ -27,5 +27,5 @@ fn real_main() -> i32 {
file.read_to_string(&mut contents).unwrap();
println!("{}", contents);
- return 0;
+ 0
}
diff --git a/examples/file_info.rs b/examples/file_info.rs
index 315b5c3..64969b6 100644
--- a/examples/file_info.rs
+++ b/examples/file_info.rs
@@ -34,7 +34,7 @@ fn real_main() -> i32 {
}
}
- if (&*file.name()).ends_with('/') {
+ if (*file.name()).ends_with('/') {
println!(
"Entry {} is a directory with name \"{}\"",
i,
@@ -49,5 +49,6 @@ fn real_main() -> i32 {
);
}
}
- return 0;
+
+ 0
}
diff --git a/examples/stdin_info.rs b/examples/stdin_info.rs
index 606944c..10d7aa8 100644
--- a/examples/stdin_info.rs
+++ b/examples/stdin_info.rs
@@ -30,5 +30,6 @@ fn real_main() -> i32 {
}
}
}
- return 0;
+
+ 0
}
diff --git a/examples/write_dir.rs b/examples/write_dir.rs
index 793bd6b..8cc561f 100644
--- a/examples/write_dir.rs
+++ b/examples/write_dir.rs
@@ -32,6 +32,11 @@ const METHOD_BZIP2: Option<zip::CompressionMethod> = Some(zip::CompressionMethod
#[cfg(not(feature = "bzip2"))]
const METHOD_BZIP2: Option<zip::CompressionMethod> = None;
+#[cfg(feature = "zstd")]
+const METHOD_ZSTD: Option<zip::CompressionMethod> = Some(zip::CompressionMethod::Zstd);
+#[cfg(not(feature = "zstd"))]
+const METHOD_ZSTD: Option<zip::CompressionMethod> = None;
+
fn real_main() -> i32 {
let args: Vec<_> = std::env::args().collect();
if args.len() < 3 {
@@ -44,7 +49,7 @@ fn real_main() -> i32 {
let src_dir = &*args[1];
let dst_file = &*args[2];
- for &method in [METHOD_STORED, METHOD_DEFLATED, METHOD_BZIP2].iter() {
+ for &method in [METHOD_STORED, METHOD_DEFLATED, METHOD_BZIP2, METHOD_ZSTD].iter() {
if method.is_none() {
continue;
}
@@ -54,7 +59,7 @@ fn real_main() -> i32 {
}
}
- return 0;
+ 0
}
fn zip_dir<T>(
@@ -87,7 +92,7 @@ where
f.read_to_end(&mut buffer)?;
zip.write_all(&*buffer)?;
buffer.clear();
- } else if name.as_os_str().len() != 0 {
+ } else if !name.as_os_str().is_empty() {
// Only if not root! Avoids path spec / warning
// and mapname conversion failed error on unzip
println!("adding dir {:?} as {:?} ...", path, name);
@@ -111,7 +116,7 @@ fn doit(
let path = Path::new(dst_file);
let file = File::create(&path).unwrap();
- let walkdir = WalkDir::new(src_dir.to_string());
+ let walkdir = WalkDir::new(src_dir);
let it = walkdir.into_iter();
zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method)?;
diff --git a/examples/write_sample.rs b/examples/write_sample.rs
index 4ef5ce3..b574950 100644
--- a/examples/write_sample.rs
+++ b/examples/write_sample.rs
@@ -18,7 +18,7 @@ fn real_main() -> i32 {
Err(e) => println!("Error: {:?}", e),
}
- return 0;
+ 0
}
fn doit(filename: &str) -> zip::result::ZipResult<()> {
@@ -42,7 +42,7 @@ fn doit(filename: &str) -> zip::result::ZipResult<()> {
Ok(())
}
-const LOREM_IPSUM : &'static [u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
+const LOREM_IPSUM : &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
molestie. Proin blandit ornare dui, a tempor nisl accumsan in. Praesent a consequat felis. Morbi metus diam, auctor in auctor vel, feugiat id odio. Curabitur ex ex,
dictum quis auctor quis, suscipit id lorem. Aliquam vestibulum dolor nec enim vehicula, porta tristique augue tincidunt. Vivamus ut gravida est. Sed pellentesque, dolor
vitae tristique consectetur, neque lectus pulvinar dui, sed feugiat purus diam id lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
diff --git a/patches/Android.bp.diff b/patches/Android.bp.diff
new file mode 100644
index 0000000..c0037cc
--- /dev/null
+++ b/patches/Android.bp.diff
@@ -0,0 +1,14 @@
+diff --git a/Android.bp b/Android.bp
+index 2373253..c0b2349 100644
+--- a/Android.bp
++++ b/Android.bp
+@@ -39,4 +39,9 @@ rust_library {
+ "//apex_available:platform",
+ "com.android.virt",
+ ],
++ arch: {
++ arm: {
++ rustlibs: ["libcrossbeam_utils"],
++ },
++ },
+ }
diff --git a/src/aes.rs b/src/aes.rs
new file mode 100644
index 0000000..8997705
--- /dev/null
+++ b/src/aes.rs
@@ -0,0 +1,185 @@
+//! Implementation of the AES decryption for zip files.
+//!
+//! This was implemented according to the [WinZip specification](https://www.winzip.com/win/en/aes_info.html).
+//! Note that using CRC with AES depends on the used encryption specification, AE-1 or AE-2.
+//! If the file is marked as encrypted with AE-2 the CRC field is ignored, even if it isn't set to 0.
+
+use crate::aes_ctr;
+use crate::types::AesMode;
+use constant_time_eq::constant_time_eq;
+use hmac::{Hmac, Mac};
+use sha1::Sha1;
+use std::io::{self, Read};
+
+/// The length of the password verifcation value in bytes
+const PWD_VERIFY_LENGTH: usize = 2;
+/// The length of the authentication code in bytes
+const AUTH_CODE_LENGTH: usize = 10;
+/// The number of iterations used with PBKDF2
+const ITERATION_COUNT: u32 = 1000;
+
+/// Create a AesCipher depending on the used `AesMode` and the given `key`.
+///
+/// # Panics
+///
+/// This panics if `key` doesn't have the correct size for the chosen aes mode.
+fn cipher_from_mode(aes_mode: AesMode, key: &[u8]) -> Box<dyn aes_ctr::AesCipher> {
+ match aes_mode {
+ AesMode::Aes128 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes128>::new(key))
+ as Box<dyn aes_ctr::AesCipher>,
+ AesMode::Aes192 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes192>::new(key))
+ as Box<dyn aes_ctr::AesCipher>,
+ AesMode::Aes256 => Box::new(aes_ctr::AesCtrZipKeyStream::<aes_ctr::Aes256>::new(key))
+ as Box<dyn aes_ctr::AesCipher>,
+ }
+}
+
+// An aes encrypted file starts with a salt, whose length depends on the used aes mode
+// followed by a 2 byte password verification value
+// then the variable length encrypted data
+// and lastly a 10 byte authentication code
+pub struct AesReader<R> {
+ reader: R,
+ aes_mode: AesMode,
+ data_length: u64,
+}
+
+impl<R: Read> AesReader<R> {
+ pub fn new(reader: R, aes_mode: AesMode, compressed_size: u64) -> AesReader<R> {
+ let data_length = compressed_size
+ - (PWD_VERIFY_LENGTH + AUTH_CODE_LENGTH + aes_mode.salt_length()) as u64;
+
+ Self {
+ reader,
+ aes_mode,
+ data_length,
+ }
+ }
+
+ /// Read the AES header bytes and validate the password.
+ ///
+ /// Even if the validation succeeds, there is still a 1 in 65536 chance that an incorrect
+ /// password was provided.
+ /// It isn't possible to check the authentication code in this step. This will be done after
+ /// reading and decrypting the file.
+ ///
+ /// # Returns
+ ///
+ /// If the password verification failed `Ok(None)` will be returned to match the validate
+ /// method of ZipCryptoReader.
+ pub fn validate(mut self, password: &[u8]) -> io::Result<Option<AesReaderValid<R>>> {
+ let salt_length = self.aes_mode.salt_length();
+ let key_length = self.aes_mode.key_length();
+
+ let mut salt = vec![0; salt_length];
+ self.reader.read_exact(&mut salt)?;
+
+ // next are 2 bytes used for password verification
+ let mut pwd_verification_value = vec![0; PWD_VERIFY_LENGTH];
+ self.reader.read_exact(&mut pwd_verification_value)?;
+
+ // derive a key from the password and salt
+ // the length depends on the aes key length
+ let derived_key_len = 2 * key_length + PWD_VERIFY_LENGTH;
+ let mut derived_key: Vec<u8> = vec![0; derived_key_len];
+
+ // use PBKDF2 with HMAC-Sha1 to derive the key
+ pbkdf2::pbkdf2::<Hmac<Sha1>>(password, &salt, ITERATION_COUNT, &mut derived_key);
+ 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..];
+
+ // the last 2 bytes should equal the password verification value
+ if pwd_verification_value != pwd_verify {
+ // wrong password
+ return Ok(None);
+ }
+
+ let cipher = cipher_from_mode(self.aes_mode, decrypt_key);
+ let hmac = Hmac::<Sha1>::new_from_slice(hmac_key).unwrap();
+
+ Ok(Some(AesReaderValid {
+ reader: self.reader,
+ data_remaining: self.data_length,
+ cipher,
+ hmac,
+ finalized: false,
+ }))
+ }
+}
+
+/// A reader for aes encrypted files, which has already passed the first password check.
+///
+/// There is a 1 in 65536 chance that an invalid password passes that check.
+/// After the data has been read and decrypted an HMAC will be checked and provide a final means
+/// to check if either the password is invalid or if the data has been changed.
+pub struct AesReaderValid<R: Read> {
+ reader: R,
+ data_remaining: u64,
+ cipher: Box<dyn aes_ctr::AesCipher>,
+ hmac: Hmac<Sha1>,
+ finalized: bool,
+}
+
+impl<R: Read> Read for AesReaderValid<R> {
+ /// This implementation does not fulfill all requirements set in the trait documentation.
+ ///
+ /// ```txt
+ /// "If an error is returned then it must be guaranteed that no bytes were read."
+ /// ```
+ ///
+ /// Whether this applies to errors that occur while reading the encrypted data depends on the
+ /// underlying reader. If the error occurs while verifying the HMAC, the reader might become
+ /// practically unusable, since its position after the error is not known.
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ if self.data_remaining == 0 {
+ return Ok(0);
+ }
+
+ // get the number of bytes to read, compare as u64 to make sure we can read more than
+ // 2^32 bytes even on 32 bit systems.
+ let bytes_to_read = self.data_remaining.min(buf.len() as u64) as usize;
+ let read = self.reader.read(&mut buf[0..bytes_to_read])?;
+ self.data_remaining -= read as u64;
+
+ // Update the hmac with the encrypted data
+ self.hmac.update(&buf[0..read]);
+
+ // decrypt the data
+ self.cipher.crypt_in_place(&mut buf[0..read]);
+
+ // if there is no data left to read, check the integrity of the data
+ if self.data_remaining == 0 {
+ assert!(
+ !self.finalized,
+ "Tried to use an already finalized HMAC. This is a bug!"
+ );
+ self.finalized = true;
+
+ // Zip uses HMAC-Sha1-80, which only uses the first half of the hash
+ // see https://www.winzip.com/win/en/aes_info.html#auth-faq
+ let mut read_auth_code = [0; AUTH_CODE_LENGTH];
+ self.reader.read_exact(&mut read_auth_code)?;
+ let computed_auth_code = &self.hmac.finalize_reset().into_bytes()[0..AUTH_CODE_LENGTH];
+
+ // 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,
+ "Invalid authentication code, this could be due to an invalid password or errors in the data"
+ )
+ );
+ }
+ }
+
+ Ok(read)
+ }
+}
+
+impl<R: Read> AesReaderValid<R> {
+ /// Consumes this decoder, returning the underlying reader.
+ pub fn into_inner(self) -> R {
+ self.reader
+ }
+}
diff --git a/src/aes_ctr.rs b/src/aes_ctr.rs
new file mode 100644
index 0000000..0f34335
--- /dev/null
+++ b/src/aes_ctr.rs
@@ -0,0 +1,281 @@
+//! A counter mode (CTR) for AES to work with the encryption used in zip files.
+//!
+//! This was implemented since the zip specification requires the mode to not use a nonce and uses a
+//! different byte order (little endian) than NIST (big endian).
+//! See [AesCtrZipKeyStream](./struct.AesCtrZipKeyStream.html) for more information.
+
+use aes::cipher::generic_array::GenericArray;
+use aes::{BlockEncrypt, NewBlockCipher};
+use byteorder::WriteBytesExt;
+use std::{any, fmt};
+
+/// Internal block size of an AES cipher.
+const AES_BLOCK_SIZE: usize = 16;
+
+/// AES-128.
+#[derive(Debug)]
+pub struct Aes128;
+/// AES-192
+#[derive(Debug)]
+pub struct Aes192;
+/// AES-256.
+#[derive(Debug)]
+pub struct Aes256;
+
+/// An AES cipher kind.
+pub trait AesKind {
+ /// Key type.
+ type Key: AsRef<[u8]>;
+ /// Cipher used to decrypt.
+ type Cipher;
+}
+
+impl AesKind for Aes128 {
+ type Key = [u8; 16];
+ type Cipher = aes::Aes128;
+}
+
+impl AesKind for Aes192 {
+ type Key = [u8; 24];
+ type Cipher = aes::Aes192;
+}
+
+impl AesKind for Aes256 {
+ type Key = [u8; 32];
+ type Cipher = aes::Aes256;
+}
+
+/// An AES-CTR key stream generator.
+///
+/// Implements the slightly non-standard AES-CTR variant used by WinZip AES encryption.
+///
+/// Typical AES-CTR implementations combine a nonce with a 64 bit counter. WinZIP AES instead uses
+/// no nonce and also uses a different byte order (little endian) than NIST (big endian).
+///
+/// The stream implements the `Read` trait; encryption or decryption is performed by XOR-ing the
+/// bytes from the key stream with the ciphertext/plaintext.
+pub struct AesCtrZipKeyStream<C: AesKind> {
+ /// Current AES counter.
+ counter: u128,
+ /// AES cipher instance.
+ cipher: C::Cipher,
+ /// Stores the currently available keystream bytes.
+ buffer: [u8; AES_BLOCK_SIZE],
+ /// Number of bytes already used up from `buffer`.
+ pos: usize,
+}
+
+impl<C> fmt::Debug for AesCtrZipKeyStream<C>
+where
+ C: AesKind,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "AesCtrZipKeyStream<{}>(counter: {})",
+ any::type_name::<C>(),
+ self.counter
+ )
+ }
+}
+
+impl<C> AesCtrZipKeyStream<C>
+where
+ C: AesKind,
+ C::Cipher: NewBlockCipher,
+{
+ /// Creates a new zip variant AES-CTR key stream.
+ ///
+ /// # Panics
+ ///
+ /// This panics if `key` doesn't have the correct size for cipher `C`.
+ pub fn new(key: &[u8]) -> AesCtrZipKeyStream<C> {
+ AesCtrZipKeyStream {
+ counter: 1,
+ cipher: C::Cipher::new(GenericArray::from_slice(key)),
+ buffer: [0u8; AES_BLOCK_SIZE],
+ pos: AES_BLOCK_SIZE,
+ }
+ }
+}
+
+impl<C> AesCipher for AesCtrZipKeyStream<C>
+where
+ C: AesKind,
+ C::Cipher: BlockEncrypt,
+{
+ /// Decrypt or encrypt `target`.
+ #[inline]
+ fn crypt_in_place(&mut self, mut target: &mut [u8]) {
+ while !target.is_empty() {
+ if self.pos == AES_BLOCK_SIZE {
+ // Note: AES block size is always 16 bytes, same as u128.
+ self.buffer
+ .as_mut()
+ .write_u128::<byteorder::LittleEndian>(self.counter)
+ .expect("did not expect u128 le conversion to fail");
+ self.cipher
+ .encrypt_block(GenericArray::from_mut_slice(&mut self.buffer));
+ self.counter += 1;
+ self.pos = 0;
+ }
+
+ let target_len = target.len().min(AES_BLOCK_SIZE - self.pos);
+
+ xor(
+ &mut target[0..target_len],
+ &self.buffer[self.pos..(self.pos + target_len)],
+ );
+ target = &mut target[target_len..];
+ self.pos += target_len;
+ }
+ }
+}
+
+/// This trait allows using generic AES ciphers with different key sizes.
+pub trait AesCipher {
+ fn crypt_in_place(&mut self, target: &mut [u8]);
+}
+
+/// XORs a slice in place with another slice.
+#[inline]
+fn xor(dest: &mut [u8], src: &[u8]) {
+ assert_eq!(dest.len(), src.len());
+
+ for (lhs, rhs) in dest.iter_mut().zip(src.iter()) {
+ *lhs ^= *rhs;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{Aes128, Aes192, Aes256, AesCipher, AesCtrZipKeyStream, AesKind};
+ use aes::{BlockEncrypt, NewBlockCipher};
+
+ /// Checks whether `crypt_in_place` produces the correct plaintext after one use and yields the
+ /// cipertext again after applying it again.
+ fn roundtrip<Aes>(key: &[u8], ciphertext: &mut [u8], expected_plaintext: &[u8])
+ where
+ Aes: AesKind,
+ Aes::Cipher: NewBlockCipher + BlockEncrypt,
+ {
+ let mut key_stream = AesCtrZipKeyStream::<Aes>::new(key);
+
+ let mut plaintext: Vec<u8> = ciphertext.to_vec();
+ key_stream.crypt_in_place(plaintext.as_mut_slice());
+ assert_eq!(plaintext, expected_plaintext.to_vec());
+
+ // Round-tripping should yield the ciphertext again.
+ let mut key_stream = AesCtrZipKeyStream::<Aes>::new(key);
+ key_stream.crypt_in_place(&mut plaintext);
+ assert_eq!(plaintext, ciphertext.to_vec());
+ }
+
+ #[test]
+ #[should_panic]
+ fn new_with_wrong_key_size() {
+ AesCtrZipKeyStream::<Aes128>::new(&[1, 2, 3, 4, 5]);
+ }
+
+ // The data used in these tests was generated with p7zip without any compression.
+ // It's not possible to recreate the exact same data, since a random salt is used for encryption.
+ // `7z a -phelloworld -mem=AES256 -mx=0 aes256_40byte.zip 40byte_data.txt`
+ #[test]
+ fn crypt_aes_256_0_byte() {
+ let mut ciphertext = [];
+ let expected_plaintext = &[];
+ let key = [
+ 0x0b, 0xec, 0x2e, 0xf2, 0x46, 0xf0, 0x7e, 0x35, 0x16, 0x54, 0xe0, 0x98, 0x10, 0xb3,
+ 0x18, 0x55, 0x24, 0xa3, 0x9e, 0x0e, 0x40, 0xe7, 0x92, 0xad, 0xb2, 0x8a, 0x48, 0xf4,
+ 0x5c, 0xd0, 0xc0, 0x54,
+ ];
+
+ roundtrip::<Aes256>(&key, &mut ciphertext, expected_plaintext);
+ }
+
+ #[test]
+ fn crypt_aes_128_5_byte() {
+ let mut ciphertext = [0x98, 0xa9, 0x8c, 0x26, 0x0e];
+ let expected_plaintext = b"asdf\n";
+ let key = [
+ 0xe0, 0x25, 0x7b, 0x57, 0x97, 0x6a, 0xa4, 0x23, 0xab, 0x94, 0xaa, 0x44, 0xfd, 0x47,
+ 0x4f, 0xa5,
+ ];
+
+ roundtrip::<Aes128>(&key, &mut ciphertext, expected_plaintext);
+ }
+
+ #[test]
+ fn crypt_aes_192_5_byte() {
+ let mut ciphertext = [0x36, 0x55, 0x5c, 0x61, 0x3c];
+ let expected_plaintext = b"asdf\n";
+ let key = [
+ 0xe4, 0x4a, 0x88, 0x52, 0x8f, 0xf7, 0x0b, 0x81, 0x7b, 0x75, 0xf1, 0x74, 0x21, 0x37,
+ 0x8c, 0x90, 0xad, 0xbe, 0x4a, 0x65, 0xa8, 0x96, 0x0e, 0xcc,
+ ];
+
+ roundtrip::<Aes192>(&key, &mut ciphertext, expected_plaintext);
+ }
+
+ #[test]
+ fn crypt_aes_256_5_byte() {
+ let mut ciphertext = [0xc2, 0x47, 0xc0, 0xdc, 0x56];
+ let expected_plaintext = b"asdf\n";
+ let key = [
+ 0x79, 0x5e, 0x17, 0xf2, 0xc6, 0x3d, 0x28, 0x9b, 0x4b, 0x4b, 0xbb, 0xa9, 0xba, 0xc9,
+ 0xa5, 0xee, 0x3a, 0x4f, 0x0f, 0x4b, 0x29, 0xbd, 0xe9, 0xb8, 0x41, 0x9c, 0x41, 0xa5,
+ 0x15, 0xb2, 0x86, 0xab,
+ ];
+
+ roundtrip::<Aes256>(&key, &mut ciphertext, expected_plaintext);
+ }
+
+ #[test]
+ fn crypt_aes_128_40_byte() {
+ let mut ciphertext = [
+ 0xcf, 0x72, 0x6b, 0xa1, 0xb2, 0x0f, 0xdf, 0xaa, 0x10, 0xad, 0x9c, 0x7f, 0x6d, 0x1c,
+ 0x8d, 0xb5, 0x16, 0x7e, 0xbb, 0x11, 0x69, 0x52, 0x8c, 0x89, 0x80, 0x32, 0xaa, 0x76,
+ 0xa6, 0x18, 0x31, 0x98, 0xee, 0xdd, 0x22, 0x68, 0xb7, 0xe6, 0x77, 0xd2,
+ ];
+ let expected_plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
+ let key = [
+ 0x43, 0x2b, 0x6d, 0xbe, 0x05, 0x76, 0x6c, 0x9e, 0xde, 0xca, 0x3b, 0xf8, 0xaf, 0x5d,
+ 0x81, 0xb6,
+ ];
+
+ roundtrip::<Aes128>(&key, &mut ciphertext, expected_plaintext);
+ }
+
+ #[test]
+ fn crypt_aes_192_40_byte() {
+ let mut ciphertext = [
+ 0xa6, 0xfc, 0x52, 0x79, 0x2c, 0x6c, 0xfe, 0x68, 0xb1, 0xa8, 0xb3, 0x07, 0x52, 0x8b,
+ 0x82, 0xa6, 0x87, 0x9c, 0x72, 0x42, 0x3a, 0xf8, 0xc6, 0xa9, 0xc9, 0xfb, 0x61, 0x19,
+ 0x37, 0xb9, 0x56, 0x62, 0xf4, 0xfc, 0x5e, 0x7a, 0xdd, 0x55, 0x0a, 0x48,
+ ];
+ let expected_plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
+ let key = [
+ 0xac, 0x92, 0x41, 0xba, 0xde, 0xd9, 0x02, 0xfe, 0x40, 0x92, 0x20, 0xf6, 0x56, 0x03,
+ 0xfe, 0xae, 0x1b, 0xba, 0x01, 0x97, 0x97, 0x79, 0xbb, 0xa6,
+ ];
+
+ roundtrip::<Aes192>(&key, &mut ciphertext, expected_plaintext);
+ }
+
+ #[test]
+ fn crypt_aes_256_40_byte() {
+ let mut ciphertext = [
+ 0xa9, 0x99, 0xbd, 0xea, 0x82, 0x9b, 0x8f, 0x2f, 0xb7, 0x52, 0x2f, 0x6b, 0xd8, 0xf6,
+ 0xab, 0x0e, 0x24, 0x51, 0x9e, 0x18, 0x0f, 0xc0, 0x8f, 0x54, 0x15, 0x80, 0xae, 0xbc,
+ 0xa0, 0x5c, 0x8a, 0x11, 0x8d, 0x14, 0x7e, 0xc5, 0xb4, 0xae, 0xd3, 0x37,
+ ];
+ let expected_plaintext = b"Lorem ipsum dolor sit amet, consectetur\n";
+ let key = [
+ 0x64, 0x7c, 0x7a, 0xde, 0xf0, 0xf2, 0x61, 0x49, 0x1c, 0xf1, 0xf1, 0xe3, 0x37, 0xfc,
+ 0xe1, 0x4d, 0x4a, 0x77, 0xd4, 0xeb, 0x9e, 0x3d, 0x75, 0xce, 0x9a, 0x3e, 0x10, 0x50,
+ 0xc2, 0x07, 0x36, 0xb6,
+ ];
+
+ roundtrip::<Aes256>(&key, &mut ciphertext, expected_plaintext);
+ }
+}
diff --git a/src/compression.rs b/src/compression.rs
index 5fdde07..abd8b53 100644
--- a/src/compression.rs
+++ b/src/compression.rs
@@ -9,8 +9,9 @@ use std::fmt;
/// contents to be read without context.
///
/// When creating ZIP files, you may choose the method to use with
-/// [`zip::write::FileOptions::compression_method`]
+/// [`crate::write::FileOptions::compression_method`]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+#[non_exhaustive]
pub enum CompressionMethod {
/// Store the file as is
Stored,
@@ -24,6 +25,15 @@ pub enum CompressionMethod {
/// Compress the file using BZIP2
#[cfg(feature = "bzip2")]
Bzip2,
+ /// Encrypted using AES.
+ ///
+ /// The actual compression method has to be taken from the AES extra data field
+ /// or from `ZipFileData`.
+ #[cfg(feature = "aes-crypto")]
+ Aes,
+ /// Compress the file using ZStandard
+ #[cfg(feature = "zstd")]
+ Zstd,
/// Unsupported compression method
#[deprecated(since = "0.5.7", note = "use the constants instead")]
Unsupported(u16),
@@ -60,12 +70,19 @@ impl CompressionMethod {
pub const IBM_ZOS_CMPSC: Self = CompressionMethod::Unsupported(16);
pub const IBM_TERSE: Self = CompressionMethod::Unsupported(18);
pub const ZSTD_DEPRECATED: Self = CompressionMethod::Unsupported(20);
+ #[cfg(feature = "zstd")]
+ pub const ZSTD: Self = CompressionMethod::Zstd;
+ #[cfg(not(feature = "zstd"))]
pub const ZSTD: Self = CompressionMethod::Unsupported(93);
pub const MP3: Self = CompressionMethod::Unsupported(94);
pub const XZ: Self = CompressionMethod::Unsupported(95);
pub const JPEG: Self = CompressionMethod::Unsupported(96);
pub const WAVPACK: Self = CompressionMethod::Unsupported(97);
pub const PPMD: Self = CompressionMethod::Unsupported(98);
+ #[cfg(feature = "aes-crypto")]
+ pub const AES: Self = CompressionMethod::Aes;
+ #[cfg(not(feature = "aes-crypto"))]
+ pub const AES: Self = CompressionMethod::Unsupported(99);
}
impl CompressionMethod {
/// Converts an u16 to its corresponding CompressionMethod
@@ -85,6 +102,10 @@ impl CompressionMethod {
8 => CompressionMethod::Deflated,
#[cfg(feature = "bzip2")]
12 => CompressionMethod::Bzip2,
+ #[cfg(feature = "zstd")]
+ 93 => CompressionMethod::Zstd,
+ #[cfg(feature = "aes-crypto")]
+ 99 => CompressionMethod::Aes,
v => CompressionMethod::Unsupported(v),
}
@@ -107,6 +128,11 @@ impl CompressionMethod {
CompressionMethod::Deflated => 8,
#[cfg(feature = "bzip2")]
CompressionMethod::Bzip2 => 12,
+ #[cfg(feature = "aes-crypto")]
+ CompressionMethod::Aes => 99,
+ #[cfg(feature = "zstd")]
+ CompressionMethod::Zstd => 93,
+
CompressionMethod::Unsupported(v) => v,
}
}
@@ -119,13 +145,28 @@ impl fmt::Display for CompressionMethod {
}
}
+/// The compression methods which have been implemented.
+pub const SUPPORTED_COMPRESSION_METHODS: &[CompressionMethod] = &[
+ CompressionMethod::Stored,
+ #[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+ ))]
+ CompressionMethod::Deflated,
+ #[cfg(feature = "bzip2")]
+ CompressionMethod::Bzip2,
+ #[cfg(feature = "zstd")]
+ CompressionMethod::Zstd,
+];
+
#[cfg(test)]
mod test {
- use super::CompressionMethod;
+ use super::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
#[test]
fn from_eq_to() {
- for v in 0..(::std::u16::MAX as u32 + 1) {
+ for v in 0..(u16::MAX as u32 + 1) {
#[allow(deprecated)]
let from = CompressionMethod::from_u16(v as u16);
#[allow(deprecated)]
@@ -134,20 +175,6 @@ mod test {
}
}
- fn methods() -> Vec<CompressionMethod> {
- let mut methods = Vec::new();
- methods.push(CompressionMethod::Stored);
- #[cfg(any(
- feature = "deflate",
- feature = "deflate-miniz",
- feature = "deflate-zlib"
- ))]
- methods.push(CompressionMethod::Deflated);
- #[cfg(feature = "bzip2")]
- methods.push(CompressionMethod::Bzip2);
- methods
- }
-
#[test]
fn to_eq_from() {
fn check_match(method: CompressionMethod) {
@@ -160,7 +187,7 @@ mod test {
assert_eq!(to, back);
}
- for method in methods() {
+ for &method in SUPPORTED_COMPRESSION_METHODS {
check_match(method);
}
}
@@ -173,7 +200,7 @@ mod test {
assert_eq!(debug_str, display_str);
}
- for method in methods() {
+ for &method in SUPPORTED_COMPRESSION_METHODS {
check_match(method);
}
}
diff --git a/src/cp437.rs b/src/cp437.rs
index f994814..4dba9af 100644
--- a/src/cp437.rs
+++ b/src/cp437.rs
@@ -6,7 +6,8 @@ pub trait FromCp437 {
type Target;
/// Function that does the conversion from cp437.
- /// Gennerally allocations will be avoided if all data falls into the ASCII range.
+ /// Generally allocations will be avoided if all data falls into the ASCII range.
+ #[allow(clippy::wrong_self_convention)]
fn from_cp437(self) -> Self::Target;
}
diff --git a/src/crc32.rs b/src/crc32.rs
index b351aa0..ebace89 100644
--- a/src/crc32.rs
+++ b/src/crc32.rs
@@ -10,15 +10,20 @@ pub struct Crc32Reader<R> {
inner: R,
hasher: Hasher,
check: u32,
+ /// Signals if `inner` stores aes encrypted data.
+ /// AE-2 encrypted data doesn't use crc and sets the value to 0.
+ ae2_encrypted: bool,
}
impl<R> Crc32Reader<R> {
- /// Get a new Crc32Reader which check the inner reader against checksum.
- pub fn new(inner: R, checksum: u32) -> Crc32Reader<R> {
+ /// Get a new Crc32Reader which checks the inner reader against checksum.
+ /// The check is disabled if `ae2_encrypted == true`.
+ pub(crate) fn new(inner: R, checksum: u32, ae2_encrypted: bool) -> Crc32Reader<R> {
Crc32Reader {
inner,
hasher: Hasher::new(),
check: checksum,
+ ae2_encrypted,
}
}
@@ -33,8 +38,10 @@ impl<R> Crc32Reader<R> {
impl<R: Read> Read for Crc32Reader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+ let invalid_check = !buf.is_empty() && !self.check_matches() && !self.ae2_encrypted;
+
let count = match self.inner.read(buf) {
- Ok(0) if !buf.is_empty() && !self.check_matches() => {
+ Ok(0) if invalid_check => {
return Err(io::Error::new(io::ErrorKind::Other, "Invalid checksum"))
}
Ok(n) => n,
@@ -55,10 +62,10 @@ mod test {
let data: &[u8] = b"";
let mut buf = [0; 1];
- let mut reader = Crc32Reader::new(data, 0);
+ let mut reader = Crc32Reader::new(data, 0, false);
assert_eq!(reader.read(&mut buf).unwrap(), 0);
- let mut reader = Crc32Reader::new(data, 1);
+ let mut reader = Crc32Reader::new(data, 1, false);
assert!(reader
.read(&mut buf)
.unwrap_err()
@@ -71,7 +78,7 @@ mod test {
let data: &[u8] = b"1234";
let mut buf = [0; 1];
- let mut reader = Crc32Reader::new(data, 0x9be3e0a3);
+ let mut reader = Crc32Reader::new(data, 0x9be3e0a3, false);
assert_eq!(reader.read(&mut buf).unwrap(), 1);
assert_eq!(reader.read(&mut buf).unwrap(), 1);
assert_eq!(reader.read(&mut buf).unwrap(), 1);
@@ -86,7 +93,7 @@ mod test {
let data: &[u8] = b"1234";
let mut buf = [0; 5];
- let mut reader = Crc32Reader::new(data, 0x9be3e0a3);
+ let mut reader = Crc32Reader::new(data, 0x9be3e0a3, false);
assert_eq!(reader.read(&mut buf[..0]).unwrap(), 0);
assert_eq!(reader.read(&mut buf).unwrap(), 4);
}
diff --git a/src/lib.rs b/src/lib.rs
index 3b39ab4..0fee99c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,15 +1,38 @@
-//! An ergonomic API for reading and writing ZIP files.
+//! A library for reading and writing ZIP archives.
+//! ZIP is a format designed for cross-platform file "archiving".
+//! That is, storing a collection of files in a single datastream
+//! to make them easier to share between computers.
+//! Additionally, ZIP is able to compress and encrypt files in its
+//! archives.
//!
//! The current implementation is based on [PKWARE's APPNOTE.TXT v6.3.9](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT)
-// TODO(#184): Decide on the crate's bias: Do we prioritise permissiveness/correctness/speed/ergonomics?
+//!
+//! ---
+//!
+//! [`zip`](`crate`) has support for the most common ZIP archives found in common use.
+//! However, in special cases,
+//! there are some zip archives that are difficult to read or write.
+//!
+//! This is a list of supported features:
+//!
+//! | | Reading | Writing |
+//! | ------- | ------ | ------- |
+//! | Deflate | ✅ [->](`crate::ZipArchive::by_name`) | ✅ [->](`crate::write::FileOptions::compression_method`) |
+//!
+//!
+//!
#![warn(missing_docs)]
-pub use crate::compression::CompressionMethod;
+pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS};
pub use crate::read::ZipArchive;
pub use crate::types::DateTime;
pub use crate::write::ZipWriter;
+#[cfg(feature = "aes-crypto")]
+mod aes;
+#[cfg(feature = "aes-crypto")]
+mod aes_ctr;
mod compression;
mod cp437;
mod crc32;
diff --git a/src/read.rs b/src/read.rs
index 97bccd2..c619f24 100644
--- a/src/read.rs
+++ b/src/read.rs
@@ -1,18 +1,20 @@
//! Types for reading ZIP archives
+#[cfg(feature = "aes-crypto")]
+use crate::aes::{AesReader, AesReaderValid};
use crate::compression::CompressionMethod;
+use crate::cp437::FromCp437;
use crate::crc32::Crc32Reader;
use crate::result::{InvalidPassword, ZipError, ZipResult};
use crate::spec;
+use crate::types::{AesMode, AesVendorVersion, AtomicU64, DateTime, System, ZipFileData};
use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator};
+use byteorder::{LittleEndian, ReadBytesExt};
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::{self, prelude::*};
use std::path::{Component, Path};
-
-use crate::cp437::FromCp437;
-use crate::types::{DateTime, System, ZipFileData};
-use byteorder::{LittleEndian, ReadBytesExt};
+use std::sync::Arc;
#[cfg(any(
feature = "deflate",
@@ -24,39 +26,62 @@ use flate2::read::DeflateDecoder;
#[cfg(feature = "bzip2")]
use bzip2::read::BzDecoder;
+#[cfg(feature = "zstd")]
+use zstd::stream::read::Decoder as ZstdDecoder;
+
mod ffi {
pub const S_IFDIR: u32 = 0o0040000;
pub const S_IFREG: u32 = 0o0100000;
}
-/// ZIP archive reader
-///
-/// ```no_run
-/// use std::io::prelude::*;
-/// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> {
-/// let mut zip = zip::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());
-/// }
-///
-/// Ok(())
-/// }
-/// ```
-#[derive(Clone, Debug)]
-pub struct ZipArchive<R> {
- reader: R,
- files: Vec<ZipFileData>,
- names_map: HashMap<String, usize>,
- offset: u64,
- comment: Vec<u8>,
+// Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely
+pub(crate) mod zip_archive {
+ /// Extract immutable data from `ZipArchive` to make it cheap to clone
+ #[derive(Debug)]
+ pub(crate) struct Shared {
+ pub(super) files: Vec<super::ZipFileData>,
+ pub(super) names_map: super::HashMap<String, usize>,
+ pub(super) offset: u64,
+ pub(super) comment: Vec<u8>,
+ }
+
+ /// ZIP archive reader
+ ///
+ /// At the moment, this type is cheap to clone if this is the case for the
+ /// reader it uses. However, this is not guaranteed by this crate and it may
+ /// change in the future.
+ ///
+ /// ```no_run
+ /// use std::io::prelude::*;
+ /// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> {
+ /// let mut zip = zip::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());
+ /// }
+ ///
+ /// Ok(())
+ /// }
+ /// ```
+ #[derive(Clone, Debug)]
+ pub struct ZipArchive<R> {
+ pub(super) reader: R,
+ pub(super) shared: super::Arc<Shared>,
+ }
}
+pub use zip_archive::ZipArchive;
+#[allow(clippy::large_enum_variant)]
enum CryptoReader<'a> {
Plaintext(io::Take<&'a mut dyn Read>),
ZipCrypto(ZipCryptoReaderValid<io::Take<&'a mut dyn Read>>),
+ #[cfg(feature = "aes-crypto")]
+ Aes {
+ reader: AesReaderValid<io::Take<&'a mut dyn Read>>,
+ vendor_version: AesVendorVersion,
+ },
}
impl<'a> Read for CryptoReader<'a> {
@@ -64,6 +89,8 @@ impl<'a> Read for CryptoReader<'a> {
match self {
CryptoReader::Plaintext(r) => r.read(buf),
CryptoReader::ZipCrypto(r) => r.read(buf),
+ #[cfg(feature = "aes-crypto")]
+ CryptoReader::Aes { reader: r, .. } => r.read(buf),
}
}
}
@@ -74,8 +101,24 @@ impl<'a> CryptoReader<'a> {
match self {
CryptoReader::Plaintext(r) => r,
CryptoReader::ZipCrypto(r) => r.into_inner(),
+ #[cfg(feature = "aes-crypto")]
+ CryptoReader::Aes { reader: r, .. } => r.into_inner(),
}
}
+
+ /// Returns `true` if the data is encrypted using AE2.
+ pub fn is_ae2_encrypted(&self) -> bool {
+ #[cfg(feature = "aes-crypto")]
+ return matches!(
+ self,
+ CryptoReader::Aes {
+ vendor_version: AesVendorVersion::Ae2,
+ ..
+ }
+ );
+ #[cfg(not(feature = "aes-crypto"))]
+ false
+ }
}
enum ZipFileReader<'a> {
@@ -90,6 +133,8 @@ enum ZipFileReader<'a> {
Deflated(Crc32Reader<flate2::read::DeflateDecoder<CryptoReader<'a>>>),
#[cfg(feature = "bzip2")]
Bzip2(Crc32Reader<BzDecoder<CryptoReader<'a>>>),
+ #[cfg(feature = "zstd")]
+ Zstd(Crc32Reader<ZstdDecoder<'a, io::BufReader<CryptoReader<'a>>>>),
}
impl<'a> Read for ZipFileReader<'a> {
@@ -106,6 +151,8 @@ impl<'a> Read for ZipFileReader<'a> {
ZipFileReader::Deflated(r) => r.read(buf),
#[cfg(feature = "bzip2")]
ZipFileReader::Bzip2(r) => r.read(buf),
+ #[cfg(feature = "zstd")]
+ ZipFileReader::Zstd(r) => r.read(buf),
}
}
}
@@ -125,6 +172,8 @@ impl<'a> ZipFileReader<'a> {
ZipFileReader::Deflated(r) => r.into_inner().into_inner().into_inner(),
#[cfg(feature = "bzip2")]
ZipFileReader::Bzip2(r) => r.into_inner().into_inner().into_inner(),
+ #[cfg(feature = "zstd")]
+ ZipFileReader::Zstd(r) => r.into_inner().finish().into_inner().into_inner(),
}
}
}
@@ -137,7 +186,7 @@ pub struct ZipFile<'a> {
}
fn find_content<'a>(
- data: &mut ZipFileData,
+ data: &ZipFileData,
reader: &'a mut (impl Read + Seek),
) -> ZipResult<io::Take<&'a mut dyn Read>> {
// Parse local header
@@ -151,12 +200,14 @@ fn find_content<'a>(
let file_name_length = reader.read_u16::<LittleEndian>()? as u64;
let extra_field_length = reader.read_u16::<LittleEndian>()? as u64;
let magic_and_header = 4 + 22 + 2 + 2;
- data.data_start = data.header_start + magic_and_header + file_name_length + extra_field_length;
+ let data_start = data.header_start + magic_and_header + file_name_length + extra_field_length;
+ data.data_start.store(data_start);
- reader.seek(io::SeekFrom::Start(data.data_start))?;
+ reader.seek(io::SeekFrom::Start(data_start))?;
Ok((reader as &mut dyn Read).take(data.compressed_size))
}
+#[allow(clippy::too_many_arguments)]
fn make_crypto_reader<'a>(
compression_method: crate::compression::CompressionMethod,
crc32: u32,
@@ -164,6 +215,8 @@ fn make_crypto_reader<'a>(
using_data_descriptor: bool,
reader: io::Take<&'a mut dyn io::Read>,
password: Option<&[u8]>,
+ aes_info: Option<(AesMode, AesVendorVersion)>,
+ #[cfg(feature = "aes-crypto")] compressed_size: u64,
) -> ZipResult<Result<CryptoReader<'a>, InvalidPassword>> {
#[allow(deprecated)]
{
@@ -172,9 +225,24 @@ fn make_crypto_reader<'a>(
}
}
- let reader = match password {
- None => CryptoReader::Plaintext(reader),
- Some(password) => {
+ let reader = match (password, aes_info) {
+ #[cfg(not(feature = "aes-crypto"))]
+ (Some(_), Some(_)) => {
+ return Err(ZipError::UnsupportedArchive(
+ "AES encrypted files cannot be decrypted without the aes-crypto feature.",
+ ))
+ }
+ #[cfg(feature = "aes-crypto")]
+ (Some(password), Some((aes_mode, vendor_version))) => {
+ match AesReader::new(reader, aes_mode, compressed_size).validate(password)? {
+ None => return Ok(Err(InvalidPassword)),
+ Some(r) => CryptoReader::Aes {
+ reader: r,
+ vendor_version,
+ },
+ }
+ }
+ (Some(password), None) => {
let validator = if using_data_descriptor {
ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart())
} else {
@@ -185,17 +253,23 @@ fn make_crypto_reader<'a>(
Some(r) => CryptoReader::ZipCrypto(r),
}
}
+ (None, Some(_)) => return Ok(Err(InvalidPassword)),
+ (None, None) => CryptoReader::Plaintext(reader),
};
Ok(Ok(reader))
}
-fn make_reader<'a>(
+fn make_reader(
compression_method: CompressionMethod,
crc32: u32,
- reader: CryptoReader<'a>,
-) -> ZipFileReader<'a> {
+ reader: CryptoReader,
+) -> ZipFileReader {
+ let ae2_encrypted = reader.is_ae2_encrypted();
+
match compression_method {
- CompressionMethod::Stored => ZipFileReader::Stored(Crc32Reader::new(reader, crc32)),
+ CompressionMethod::Stored => {
+ ZipFileReader::Stored(Crc32Reader::new(reader, crc32, ae2_encrypted))
+ }
#[cfg(any(
feature = "deflate",
feature = "deflate-miniz",
@@ -203,12 +277,17 @@ fn make_reader<'a>(
))]
CompressionMethod::Deflated => {
let deflate_reader = DeflateDecoder::new(reader);
- ZipFileReader::Deflated(Crc32Reader::new(deflate_reader, crc32))
+ ZipFileReader::Deflated(Crc32Reader::new(deflate_reader, crc32, ae2_encrypted))
}
#[cfg(feature = "bzip2")]
CompressionMethod::Bzip2 => {
let bzip2_reader = BzDecoder::new(reader);
- ZipFileReader::Bzip2(Crc32Reader::new(bzip2_reader, crc32))
+ ZipFileReader::Bzip2(Crc32Reader::new(bzip2_reader, crc32, ae2_encrypted))
+ }
+ #[cfg(feature = "zstd")]
+ CompressionMethod::Zstd => {
+ let zstd_reader = ZstdDecoder::new(reader).unwrap();
+ ZipFileReader::Zstd(Crc32Reader::new(zstd_reader, crc32, ae2_encrypted))
}
_ => panic!("Compression method not supported"),
}
@@ -303,7 +382,7 @@ impl<R: Read + io::Seek> ZipArchive<R> {
let directory_start = footer
.central_directory_offset
.checked_add(archive_offset)
- .ok_or_else(|| {
+ .ok_or({
ZipError::InvalidArchive("Invalid central directory size or offset")
})?;
@@ -332,7 +411,7 @@ impl<R: Read + io::Seek> ZipArchive<R> {
let mut files = Vec::new();
let mut names_map = HashMap::new();
- if let Err(_) = reader.seek(io::SeekFrom::Start(directory_start)) {
+ if reader.seek(io::SeekFrom::Start(directory_start)).is_err() {
return Err(ZipError::InvalidArchive(
"Could not seek to start of central directory",
));
@@ -344,13 +423,14 @@ impl<R: Read + io::Seek> ZipArchive<R> {
files.push(file);
}
- Ok(ZipArchive {
- reader,
+ let shared = Arc::new(zip_archive::Shared {
files,
names_map,
offset: archive_offset,
comment: footer.zip_file_comment,
- })
+ });
+
+ Ok(ZipArchive { reader, shared })
}
/// Extract a Zip archive into a directory, overwriting files if they
/// already exist. Paths are sanitized with [`ZipFile::enclosed_name`].
@@ -393,7 +473,7 @@ impl<R: Read + io::Seek> ZipArchive<R> {
/// Number of files contained in this zip.
pub fn len(&self) -> usize {
- self.files.len()
+ self.shared.files.len()
}
/// Whether this zip archive contains no files
@@ -406,20 +486,32 @@ impl<R: Read + io::Seek> ZipArchive<R> {
/// Normally this value is zero, but if the zip has arbitrary data prepended to it, then this value will be the size
/// of that prepended data.
pub fn offset(&self) -> u64 {
- self.offset
+ self.shared.offset
}
/// Get the comment of the zip archive.
pub fn comment(&self) -> &[u8] {
- &self.comment
+ &self.shared.comment
}
/// Returns an iterator over all the file and directory names in this archive.
pub fn file_names(&self) -> impl Iterator<Item = &str> {
- self.names_map.keys().map(|s| s.as_str())
+ self.shared.names_map.keys().map(|s| s.as_str())
}
/// Search for a file entry by name, decrypt with given password
+ ///
+ /// # Warning
+ ///
+ /// The implementation of the cryptographic algorithms has not
+ /// gone through a correctness review, and you should assume it is insecure:
+ /// passwords used with this API may be compromised.
+ ///
+ /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
+ /// to check for a 1/256 chance that the password is correct.
+ /// 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_name_decrypt<'a>(
&'a mut self,
name: &str,
@@ -438,7 +530,7 @@ impl<R: Read + io::Seek> ZipArchive<R> {
name: &str,
password: Option<&[u8]>,
) -> ZipResult<Result<ZipFile<'a>, InvalidPassword>> {
- let index = match self.names_map.get(name) {
+ let index = match self.shared.names_map.get(name) {
Some(index) => *index,
None => {
return Err(ZipError::FileNotFound);
@@ -448,6 +540,18 @@ impl<R: Read + io::Seek> ZipArchive<R> {
}
/// Get a contained file by index, decrypt with given password
+ ///
+ /// # Warning
+ ///
+ /// The implementation of the cryptographic algorithms has not
+ /// gone through a correctness review, and you should assume it is insecure:
+ /// passwords used with this API may be compromised.
+ ///
+ /// This function sometimes accepts wrong password. This is because the ZIP spec only allows us
+ /// to check for a 1/256 chance that the password is correct.
+ /// 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,
file_number: usize,
@@ -457,17 +561,18 @@ impl<R: Read + io::Seek> ZipArchive<R> {
}
/// Get a contained file by index
- pub fn by_index<'a>(&'a mut self, file_number: usize) -> ZipResult<ZipFile<'a>> {
+ pub fn by_index(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
Ok(self
.by_index_with_optional_password(file_number, None)?
.unwrap())
}
/// Get a contained file by index without decompressing it
- pub fn by_index_raw<'a>(&'a mut self, file_number: usize) -> ZipResult<ZipFile<'a>> {
+ pub fn by_index_raw(&mut self, file_number: usize) -> ZipResult<ZipFile<'_>> {
let reader = &mut self.reader;
- self.files
- .get_mut(file_number)
+ self.shared
+ .files
+ .get(file_number)
.ok_or(ZipError::FileNotFound)
.and_then(move |data| {
Ok(ZipFile {
@@ -483,10 +588,11 @@ impl<R: Read + io::Seek> ZipArchive<R> {
file_number: usize,
mut password: Option<&[u8]>,
) -> ZipResult<Result<ZipFile<'a>, InvalidPassword>> {
- if file_number >= self.files.len() {
- return Err(ZipError::FileNotFound);
- }
- let data = &mut self.files[file_number];
+ let data = self
+ .shared
+ .files
+ .get(file_number)
+ .ok_or(ZipError::FileNotFound)?;
match (password, data.encrypted) {
(None, true) => return Err(ZipError::UnsupportedArchive(ZipError::PASSWORD_REQUIRED)),
@@ -502,6 +608,9 @@ impl<R: Read + io::Seek> ZipArchive<R> {
data.using_data_descriptor,
limit_reader,
password,
+ data.aes_mode,
+ #[cfg(feature = "aes-crypto")]
+ data.compressed_size,
) {
Ok(Ok(crypto_reader)) => Ok(Ok(ZipFile {
crypto_reader: Some(crypto_reader),
@@ -582,6 +691,7 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>(
#[allow(deprecated)]
CompressionMethod::from_u16(compression_method)
},
+ compression_level: None,
last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time),
crc32,
compressed_size: compressed_size as u64,
@@ -592,9 +702,10 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>(
file_comment,
header_start: offset,
central_header_start,
- data_start: 0,
+ data_start: AtomicU64::new(0),
external_attributes: external_file_attributes,
large_file: false,
+ aes_mode: None,
};
match parse_extra_field(&mut result) {
@@ -602,8 +713,18 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>(
Err(e) => return Err(e),
}
+ let aes_enabled = result.compression_method == CompressionMethod::AES;
+ if aes_enabled && result.aes_mode.is_none() {
+ return Err(ZipError::InvalidArchive(
+ "AES encryption without AES extra data field",
+ ));
+ }
+
// Account for shifted zip offsets.
- result.header_start += archive_offset;
+ result.header_start = result
+ .header_start
+ .checked_add(archive_offset)
+ .ok_or(ZipError::InvalidArchive("Archive header is too large"))?;
Ok(result)
}
@@ -615,24 +736,58 @@ fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> {
let kind = reader.read_u16::<LittleEndian>()?;
let len = reader.read_u16::<LittleEndian>()?;
let mut len_left = len as i64;
- // Zip64 extended information extra field
- if kind == 0x0001 {
- if file.uncompressed_size == 0xFFFFFFFF {
- file.large_file = true;
- file.uncompressed_size = reader.read_u64::<LittleEndian>()?;
- len_left -= 8;
+ match kind {
+ // Zip64 extended information extra field
+ 0x0001 => {
+ if file.uncompressed_size == spec::ZIP64_BYTES_THR {
+ file.large_file = true;
+ file.uncompressed_size = reader.read_u64::<LittleEndian>()?;
+ len_left -= 8;
+ }
+ if file.compressed_size == spec::ZIP64_BYTES_THR {
+ file.large_file = true;
+ file.compressed_size = reader.read_u64::<LittleEndian>()?;
+ len_left -= 8;
+ }
+ if file.header_start == spec::ZIP64_BYTES_THR {
+ file.header_start = reader.read_u64::<LittleEndian>()?;
+ len_left -= 8;
+ }
}
- if file.compressed_size == 0xFFFFFFFF {
- file.large_file = true;
- file.compressed_size = reader.read_u64::<LittleEndian>()?;
- len_left -= 8;
+ 0x9901 => {
+ // AES
+ if len != 7 {
+ return Err(ZipError::UnsupportedArchive(
+ "AES extra data field has an unsupported length",
+ ));
+ }
+ let vendor_version = reader.read_u16::<LittleEndian>()?;
+ let vendor_id = reader.read_u16::<LittleEndian>()?;
+ let aes_mode = reader.read_u8()?;
+ let compression_method = reader.read_u16::<LittleEndian>()?;
+
+ if vendor_id != 0x4541 {
+ return Err(ZipError::InvalidArchive("Invalid AES vendor"));
+ }
+ let vendor_version = match vendor_version {
+ 0x0001 => AesVendorVersion::Ae1,
+ 0x0002 => AesVendorVersion::Ae2,
+ _ => return Err(ZipError::InvalidArchive("Invalid AES vendor version")),
+ };
+ match aes_mode {
+ 0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version)),
+ 0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version)),
+ 0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version)),
+ _ => return Err(ZipError::InvalidArchive("Invalid AES encryption strength")),
+ };
+ file.compression_method = {
+ #[allow(deprecated)]
+ CompressionMethod::from_u16(compression_method)
+ };
}
- if file.header_start == 0xFFFFFFFF {
- file.header_start = reader.read_u64::<LittleEndian>()?;
- len_left -= 8;
+ _ => {
+ // Other fields are ignored
}
- // Unparsed fields:
- // u32: disk start number
}
// We could also check for < 0 to check for errors
@@ -822,7 +977,7 @@ impl<'a> ZipFile<'a> {
/// Get the starting offset of the data of the compressed file
pub fn data_start(&self) -> u64 {
- self.data.data_start
+ self.data.data_start.load()
}
/// Get the starting offset of the zip header for this file
@@ -932,6 +1087,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>(
encrypted,
using_data_descriptor,
compression_method,
+ compression_level: None,
last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time),
crc32,
compressed_size: compressed_size as u64,
@@ -943,13 +1099,14 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>(
// header_start and data start are not available, but also don't matter, since seeking is
// not available.
header_start: 0,
- data_start: 0,
+ data_start: AtomicU64::new(0),
central_header_start: 0,
// The external_attributes field is only available in the central directory.
// We set this to zero, which should be valid as the docs state 'If input came
// from standard input, this field is set to zero.'
external_attributes: 0,
large_file: false,
+ aes_mode: None,
};
match parse_extra_field(&mut result) {
@@ -975,6 +1132,9 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>(
result.using_data_descriptor,
limit_reader,
None,
+ None,
+ #[cfg(feature = "aes-crypto")]
+ result.compressed_size,
)?
.unwrap();
@@ -1017,7 +1177,7 @@ mod test {
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();
- assert!(reader.len() == 1);
+ assert_eq!(reader.len(), 1);
}
#[test]
@@ -1028,7 +1188,7 @@ mod test {
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();
- assert!(reader.comment() == b"");
+ assert_eq!(reader.comment(), b"");
assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77);
}
@@ -1041,9 +1201,8 @@ mod test {
v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip"));
let mut reader = io::Cursor::new(v);
loop {
- match read_zipfile_from_stream(&mut reader).unwrap() {
- None => break,
- _ => (),
+ if read_zipfile_from_stream(&mut reader).unwrap().is_none() {
+ break;
}
}
}
@@ -1079,14 +1238,14 @@ mod test {
let mut buf3 = [0; 5];
let mut buf4 = [0; 5];
- file1.read(&mut buf1).unwrap();
- file2.read(&mut buf2).unwrap();
- file1.read(&mut buf3).unwrap();
- file2.read(&mut buf4).unwrap();
+ file1.read_exact(&mut buf1).unwrap();
+ file2.read_exact(&mut buf2).unwrap();
+ file1.read_exact(&mut buf3).unwrap();
+ file2.read_exact(&mut buf4).unwrap();
assert_eq!(buf1, buf2);
assert_eq!(buf3, buf4);
- assert!(buf1 != buf3);
+ assert_ne!(buf1, buf3);
}
#[test]
diff --git a/src/result.rs b/src/result.rs
index 5d5ab45..72a30e4 100644
--- a/src/result.rs
+++ b/src/result.rs
@@ -1,37 +1,66 @@
//! Error types that can be emitted from this library
+use std::error::Error;
+use std::fmt;
use std::io;
-use thiserror::Error;
-
/// Generic result type with ZipError as its error variant
pub type ZipResult<T> = Result<T, ZipError>;
/// The given password is wrong
-#[derive(Error, Debug)]
-#[error("invalid password for file in archive")]
+#[derive(Debug)]
pub struct InvalidPassword;
+impl fmt::Display for InvalidPassword {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ write!(fmt, "invalid password for file in archive")
+ }
+}
+
+impl Error for InvalidPassword {}
+
/// Error type for Zip
-#[derive(Debug, Error)]
+#[derive(Debug)]
pub enum ZipError {
/// An Error caused by I/O
- #[error(transparent)]
- Io(#[from] io::Error),
+ Io(io::Error),
/// This file is probably not a zip archive
- #[error("invalid Zip archive")]
InvalidArchive(&'static str),
/// This archive is not supported
- #[error("unsupported Zip archive")]
UnsupportedArchive(&'static str),
/// The requested file could not be found in the archive
- #[error("specified file not found in archive")]
FileNotFound,
}
+impl From<io::Error> for ZipError {
+ fn from(err: io::Error) -> ZipError {
+ ZipError::Io(err)
+ }
+}
+
+impl fmt::Display for ZipError {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ZipError::Io(err) => write!(fmt, "{}", err),
+ ZipError::InvalidArchive(err) => write!(fmt, "invalid Zip archive: {}", err),
+ ZipError::UnsupportedArchive(err) => write!(fmt, "unsupported Zip archive: {}", err),
+ ZipError::FileNotFound => write!(fmt, "specified file not found in archive"),
+ }
+ }
+}
+
+impl Error for ZipError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ match self {
+ ZipError::Io(err) => Some(err),
+ _ => None,
+ }
+ }
+}
+
impl ZipError {
/// The text used as an error when a password is required and not supplied
///
diff --git a/src/spec.rs b/src/spec.rs
index 4ab3656..3ffcf73 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -9,6 +9,9 @@ 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 const ZIP64_BYTES_THR: u64 = u32::MAX as u64;
+pub const ZIP64_ENTRY_THR: usize = u16::MAX as usize;
+
pub struct CentralDirectoryEnd {
pub disk_number: u16,
pub disk_with_central_directory: u16,
diff --git a/src/types.rs b/src/types.rs
index 026aa15..b65fad4 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,4 +1,48 @@
//! Types that specify what is contained in a ZIP.
+#[cfg(doc)]
+use {crate::read::ZipFile, crate::write::FileOptions};
+
+#[cfg(not(any(
+ all(target_arch = "arm", target_pointer_width = "32"),
+ target_arch = "mips",
+ target_arch = "powerpc"
+)))]
+use std::sync::atomic;
+
+#[cfg(any(
+ all(target_arch = "arm", target_pointer_width = "32"),
+ target_arch = "mips",
+ target_arch = "powerpc"
+))]
+mod atomic {
+ use crossbeam_utils::sync::ShardedLock;
+ pub use std::sync::atomic::Ordering;
+
+ #[derive(Debug, Default)]
+ pub struct AtomicU64 {
+ value: ShardedLock<u64>,
+ }
+
+ impl AtomicU64 {
+ pub fn new(v: u64) -> Self {
+ Self {
+ value: ShardedLock::new(v),
+ }
+ }
+ pub fn get_mut(&mut self) -> &mut u64 {
+ self.value.get_mut().unwrap()
+ }
+ pub fn load(&self, _: Ordering) -> u64 {
+ *self.value.read().unwrap()
+ }
+ pub fn store(&self, value: u64, _: Ordering) {
+ *self.value.write().unwrap() = value;
+ }
+ }
+}
+
+#[cfg(feature = "time")]
+use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum System {
@@ -19,19 +63,23 @@ impl System {
}
}
-/// A DateTime field to be used for storing timestamps in a zip file
+/// Representation of a moment in time.
///
-/// This structure does bounds checking to ensure the date is able to be stored in a zip file.
+/// Zip files use an old format from DOS to store timestamps,
+/// with its own set of peculiarities.
+/// For example, it has a resolution of 2 seconds!
///
-/// When constructed manually from a date and time, it will also check if the input is sensible
-/// (e.g. months are from [1, 12]), but when read from a zip some parts may be out of their normal
-/// bounds (e.g. month 0, or hour 31).
+/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`],
+/// or read from one with [`ZipFile::last_modified`]
///
/// # Warning
///
-/// Some utilities use alternative timestamps to improve the accuracy of their
-/// ZIPs, but we don't parse them yet. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904),
-/// however this API shouldn't be considered complete.
+/// Because there is no timezone associated with the [`DateTime`], they should ideally only
+/// be used for user-facing descriptions. This also means [`DateTime::to_time`] returns an
+/// [`OffsetDateTime`] (which is the equivalent of chrono's `NaiveDateTime`).
+///
+/// Modern zip files store more precise timestamps, which are ignored by [`crate::read::ZipArchive`],
+/// so keep in mind that these timestamps are unreliable. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904).
#[derive(Debug, Clone, Copy)]
pub struct DateTime {
year: u16,
@@ -62,7 +110,7 @@ impl DateTime {
let seconds = (timepart & 0b0000000000011111) << 1;
let minutes = (timepart & 0b0000011111100000) >> 5;
let hours = (timepart & 0b1111100000000000) >> 11;
- let days = (datepart & 0b0000000000011111) >> 0;
+ let days = datepart & 0b0000000000011111;
let months = (datepart & 0b0000000111100000) >> 5;
let years = (datepart & 0b1111111000000000) >> 9;
@@ -85,6 +133,7 @@ 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,
@@ -93,8 +142,7 @@ impl DateTime {
minute: u8,
second: u8,
) -> Result<DateTime, ()> {
- if year >= 1980
- && year <= 2107
+ if (1980..=2107).contains(&year)
&& month >= 1
&& month <= 12
&& day >= 1
@@ -117,30 +165,19 @@ impl DateTime {
}
#[cfg(feature = "time")]
- /// Converts a ::time::Tm object to a DateTime
+ /// Converts a OffsetDateTime object to a DateTime
///
/// Returns `Err` when this object is out of bounds
- pub fn from_time(tm: ::time::Tm) -> Result<DateTime, ()> {
- if tm.tm_year >= 80
- && tm.tm_year <= 207
- && tm.tm_mon >= 0
- && tm.tm_mon <= 11
- && tm.tm_mday >= 1
- && tm.tm_mday <= 31
- && tm.tm_hour >= 0
- && tm.tm_hour <= 23
- && tm.tm_min >= 0
- && tm.tm_min <= 59
- && tm.tm_sec >= 0
- && tm.tm_sec <= 60
- {
+ #[allow(clippy::result_unit_err)]
+ pub fn from_time(dt: OffsetDateTime) -> Result<DateTime, ()> {
+ if dt.year() >= 1980 && dt.year() <= 2107 {
Ok(DateTime {
- year: (tm.tm_year + 1900) as u16,
- month: (tm.tm_mon + 1) as u8,
- day: tm.tm_mday as u8,
- hour: tm.tm_hour as u8,
- minute: tm.tm_min as u8,
- second: tm.tm_sec as u8,
+ year: (dt.year()) as u16,
+ month: (dt.month()) as u8,
+ day: dt.day() as u8,
+ hour: dt.hour() as u8,
+ minute: dt.minute() as u8,
+ second: dt.second() as u8,
})
} else {
Err(())
@@ -158,20 +195,14 @@ impl DateTime {
}
#[cfg(feature = "time")]
- /// Converts the datetime to a Tm structure
- ///
- /// The fields `tm_wday`, `tm_yday`, `tm_utcoff` and `tm_nsec` are set to their defaults.
- pub fn to_time(&self) -> ::time::Tm {
- ::time::Tm {
- tm_sec: self.second as i32,
- tm_min: self.minute as i32,
- tm_hour: self.hour as i32,
- tm_mday: self.day as i32,
- tm_mon: self.month as i32 - 1,
- tm_year: self.year as i32 - 1900,
- tm_isdst: -1,
- ..::time::empty_tm()
- }
+ /// Converts the DateTime to a OffsetDateTime structure
+ pub fn to_time(&self) -> Result<OffsetDateTime, ComponentRange> {
+ use std::convert::TryFrom;
+
+ let date =
+ Date::from_calendar_date(self.year as i32, Month::try_from(self.month)?, self.day)?;
+ let time = Time::from_hms(self.hour, self.minute, self.second)?;
+ Ok(PrimitiveDateTime::new(date, time).assume_utc())
}
/// Get the year. There is no epoch, i.e. 2018 will be returned as 2018.
@@ -180,26 +211,46 @@ impl DateTime {
}
/// Get the month, where 1 = january and 12 = december
+ ///
+ /// # Warning
+ ///
+ /// When read from a zip file, this may not be a reasonable value
pub fn month(&self) -> u8 {
self.month
}
/// Get the day
+ ///
+ /// # Warning
+ ///
+ /// When read from a zip file, this may not be a reasonable value
pub fn day(&self) -> u8 {
self.day
}
/// Get the hour
+ ///
+ /// # Warning
+ ///
+ /// When read from a zip file, this may not be a reasonable value
pub fn hour(&self) -> u8 {
self.hour
}
/// Get the minute
+ ///
+ /// # Warning
+ ///
+ /// When read from a zip file, this may not be a reasonable value
pub fn minute(&self) -> u8 {
self.minute
}
/// Get the second
+ ///
+ /// # Warning
+ ///
+ /// When read from a zip file, this may not be a reasonable value
pub fn second(&self) -> u8 {
self.second
}
@@ -207,6 +258,37 @@ impl DateTime {
pub const DEFAULT_VERSION: u8 = 46;
+/// A type like `AtomicU64` except it implements `Clone` and has predefined
+/// ordering.
+///
+/// It uses `Relaxed` ordering because it is not used for synchronisation.
+#[derive(Debug)]
+pub struct AtomicU64(atomic::AtomicU64);
+
+impl AtomicU64 {
+ pub fn new(v: u64) -> Self {
+ Self(atomic::AtomicU64::new(v))
+ }
+
+ pub fn load(&self) -> u64 {
+ self.0.load(atomic::Ordering::Relaxed)
+ }
+
+ pub fn store(&self, val: u64) {
+ self.0.store(val, atomic::Ordering::Relaxed)
+ }
+
+ pub fn get_mut(&mut self) -> &mut u64 {
+ self.0.get_mut()
+ }
+}
+
+impl Clone for AtomicU64 {
+ fn clone(&self) -> Self {
+ Self(atomic::AtomicU64::new(self.load()))
+ }
+}
+
/// Structure representing a ZIP file.
#[derive(Debug, Clone)]
pub struct ZipFileData {
@@ -220,6 +302,8 @@ pub struct ZipFileData {
pub using_data_descriptor: bool,
/// Compression method used to store the file
pub compression_method: crate::compression::CompressionMethod,
+ /// Compression level to store the file
+ pub compression_level: Option<i32>,
/// Last modified time. This will only have a 2 second precision.
pub last_modified_time: DateTime,
/// CRC32 checksum
@@ -243,11 +327,13 @@ pub struct ZipFileData {
/// Note that when this is not known, it is set to 0
pub central_header_start: u64,
/// Specifies where the compressed data of the file starts
- pub data_start: u64,
+ pub data_start: AtomicU64,
/// External file attributes
pub external_attributes: u32,
/// Reserve local ZIP64 extra field
pub large_file: bool,
+ /// AES mode if applicable
+ pub aes_mode: Option<(AesMode, AesVendorVersion)>,
}
impl ZipFileData {
@@ -271,10 +357,7 @@ impl ZipFileData {
::std::path::Path::new(&filename)
.components()
- .filter(|component| match *component {
- ::std::path::Component::Normal(..) => true,
- _ => false,
- })
+ .filter(|component| matches!(*component, ::std::path::Component::Normal(..)))
.fold(::std::path::PathBuf::new(), |mut path, ref cur| {
path.push(cur.as_os_str());
path
@@ -298,6 +381,39 @@ impl ZipFileData {
}
}
+/// The encryption specification used to encrypt a file with AES.
+///
+/// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2
+/// does not make use of the CRC check.
+#[derive(Copy, Clone, Debug)]
+pub enum AesVendorVersion {
+ Ae1,
+ Ae2,
+}
+
+/// AES variant used.
+#[derive(Copy, Clone, Debug)]
+pub enum AesMode {
+ Aes128,
+ Aes192,
+ Aes256,
+}
+
+#[cfg(feature = "aes-crypto")]
+impl AesMode {
+ pub fn salt_length(&self) -> usize {
+ self.key_length() / 2
+ }
+
+ pub fn key_length(&self) -> usize {
+ match self {
+ Self::Aes128 => 16,
+ Self::Aes192 => 24,
+ Self::Aes256 => 32,
+ }
+ }
+}
+
#[cfg(test)]
mod test {
#[test]
@@ -319,6 +435,7 @@ mod test {
encrypted: false,
using_data_descriptor: false,
compression_method: crate::compression::CompressionMethod::Stored,
+ compression_level: None,
last_modified_time: DateTime::default(),
crc32: 0,
compressed_size: 0,
@@ -328,10 +445,11 @@ mod test {
extra_field: Vec::new(),
file_comment: String::new(),
header_start: 0,
- data_start: 0,
+ data_start: AtomicU64::new(0),
central_header_start: 0,
external_attributes: 0,
large_file: false,
+ aes_mode: None,
};
assert_eq!(
data.file_name_sanitized(),
@@ -340,6 +458,7 @@ mod test {
}
#[test]
+ #[allow(clippy::unusual_byte_groupings)]
fn datetime_default() {
use super::DateTime;
let dt = DateTime::default();
@@ -348,6 +467,7 @@ mod test {
}
#[test]
+ #[allow(clippy::unusual_byte_groupings)]
fn datetime_max() {
use super::DateTime;
let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap();
@@ -375,57 +495,25 @@ mod test {
}
#[cfg(feature = "time")]
+ use time::{format_description::well_known::Rfc3339, OffsetDateTime};
+
+ #[cfg(feature = "time")]
#[test]
fn datetime_from_time_bounds() {
use super::DateTime;
+ use time::macros::datetime;
// 1979-12-31 23:59:59
- assert!(DateTime::from_time(::time::Tm {
- tm_sec: 59,
- tm_min: 59,
- tm_hour: 23,
- tm_mday: 31,
- tm_mon: 11, // tm_mon has number range [0, 11]
- tm_year: 79, // 1979 - 1900 = 79
- ..::time::empty_tm()
- })
- .is_err());
+ assert!(DateTime::from_time(datetime!(1979-12-31 23:59:59 UTC)).is_err());
// 1980-01-01 00:00:00
- assert!(DateTime::from_time(::time::Tm {
- tm_sec: 0,
- tm_min: 0,
- tm_hour: 0,
- tm_mday: 1,
- tm_mon: 0, // tm_mon has number range [0, 11]
- tm_year: 80, // 1980 - 1900 = 80
- ..::time::empty_tm()
- })
- .is_ok());
+ assert!(DateTime::from_time(datetime!(1980-01-01 00:00:00 UTC)).is_ok());
// 2107-12-31 23:59:59
- assert!(DateTime::from_time(::time::Tm {
- tm_sec: 59,
- tm_min: 59,
- tm_hour: 23,
- tm_mday: 31,
- tm_mon: 11, // tm_mon has number range [0, 11]
- tm_year: 207, // 2107 - 1900 = 207
- ..::time::empty_tm()
- })
- .is_ok());
+ assert!(DateTime::from_time(datetime!(2107-12-31 23:59:59 UTC)).is_ok());
// 2108-01-01 00:00:00
- assert!(DateTime::from_time(::time::Tm {
- tm_sec: 0,
- tm_min: 0,
- tm_hour: 0,
- tm_mday: 1,
- tm_mon: 0, // tm_mon has number range [0, 11]
- tm_year: 208, // 2108 - 1900 = 208
- ..::time::empty_tm()
- })
- .is_err());
+ assert!(DateTime::from_time(datetime!(2108-01-01 00:00:00 UTC)).is_err());
}
#[test]
@@ -441,7 +529,7 @@ mod test {
#[cfg(feature = "time")]
assert_eq!(
- format!("{}", dt.to_time().rfc3339()),
+ dt.to_time().unwrap().format(&Rfc3339).unwrap(),
"2018-11-17T10:38:30Z"
);
}
@@ -458,10 +546,7 @@ mod test {
assert_eq!(dt.second(), 62);
#[cfg(feature = "time")]
- assert_eq!(
- format!("{}", dt.to_time().rfc3339()),
- "2107-15-31T31:63:62Z"
- );
+ assert!(dt.to_time().is_err());
let dt = DateTime::from_msdos(0x0000, 0x0000);
assert_eq!(dt.year(), 1980);
@@ -472,10 +557,7 @@ mod test {
assert_eq!(dt.second(), 0);
#[cfg(feature = "time")]
- assert_eq!(
- format!("{}", dt.to_time().rfc3339()),
- "1980-00-00T00:00:00Z"
- );
+ assert!(dt.to_time().is_err());
}
#[cfg(feature = "time")]
@@ -484,8 +566,8 @@ mod test {
use super::DateTime;
// 2020-01-01 00:00:00
- let clock = ::time::Timespec::new(1577836800, 0);
- let tm = ::time::at_utc(clock);
- assert!(DateTime::from_time(tm).is_ok());
+ let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap();
+
+ assert!(DateTime::from_time(clock).is_ok());
}
}
diff --git a/src/write.rs b/src/write.rs
index 05c3666..551b4e3 100644
--- a/src/write.rs
+++ b/src/write.rs
@@ -4,7 +4,7 @@ use crate::compression::CompressionMethod;
use crate::read::{central_header_to_zip_file, ZipArchive, ZipFile};
use crate::result::{ZipError, ZipResult};
use crate::spec;
-use crate::types::{DateTime, System, ZipFileData, DEFAULT_VERSION};
+use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crc32fast::Hasher;
use std::default::Default;
@@ -22,6 +22,12 @@ use flate2::write::DeflateEncoder;
#[cfg(feature = "bzip2")]
use bzip2::write::BzEncoder;
+#[cfg(feature = "time")]
+use time::OffsetDateTime;
+
+#[cfg(feature = "zstd")]
+use zstd::stream::write::Encoder as ZstdEncoder;
+
enum GenericZipWriter<W: Write + io::Seek> {
Closed,
Storer(W),
@@ -33,46 +39,52 @@ enum GenericZipWriter<W: Write + io::Seek> {
Deflater(DeflateEncoder<W>),
#[cfg(feature = "bzip2")]
Bzip2(BzEncoder<W>),
+ #[cfg(feature = "zstd")]
+ Zstd(ZstdEncoder<'static, W>),
}
-
-/// ZIP archive generator
-///
-/// Handles the bookkeeping involved in building an archive, and provides an
-/// API to edit its contents.
-///
-/// ```
-/// # fn doit() -> zip::result::ZipResult<()>
-/// # {
-/// # use zip::ZipWriter;
-/// use std::io::Write;
-/// use zip::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 options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
-/// zip.start_file("hello_world.txt", options)?;
-/// zip.write(b"Hello, World!")?;
-///
-/// // Apply the changes you've made.
-/// // Dropping the `ZipWriter` will have the same effect, but may silently fail
-/// zip.finish()?;
-///
-/// # Ok(())
-/// # }
-/// # doit().unwrap();
-/// ```
-pub struct ZipWriter<W: Write + io::Seek> {
- inner: GenericZipWriter<W>,
- files: Vec<ZipFileData>,
- stats: ZipWriterStats,
- writing_to_file: bool,
- writing_to_extra_field: bool,
- writing_to_central_extra_field_only: bool,
- writing_raw: bool,
- comment: Vec<u8>,
+// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
+pub(crate) mod zip_writer {
+ use super::*;
+ /// ZIP archive generator
+ ///
+ /// Handles the bookkeeping involved in building an archive, and provides an
+ /// API to edit its contents.
+ ///
+ /// ```
+ /// # fn doit() -> zip::result::ZipResult<()>
+ /// # {
+ /// # use zip::ZipWriter;
+ /// use std::io::Write;
+ /// use zip::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 options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
+ /// zip.start_file("hello_world.txt", options)?;
+ /// zip.write(b"Hello, World!")?;
+ ///
+ /// // Apply the changes you've made.
+ /// // Dropping the `ZipWriter` will have the same effect, but may silently fail
+ /// zip.finish()?;
+ ///
+ /// # Ok(())
+ /// # }
+ /// # doit().unwrap();
+ /// ```
+ pub struct ZipWriter<W: Write + io::Seek> {
+ pub(super) inner: GenericZipWriter<W>,
+ pub(super) files: Vec<ZipFileData>,
+ 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<u8>,
+ }
}
+pub use zip_writer::ZipWriter;
#[derive(Default)]
struct ZipWriterStats {
@@ -91,6 +103,7 @@ struct ZipRawValues {
#[derive(Copy, Clone)]
pub struct FileOptions {
compression_method: CompressionMethod,
+ compression_level: Option<i32>,
last_modified_time: DateTime,
permissions: Option<u32>,
large_file: bool,
@@ -112,8 +125,9 @@ impl FileOptions {
feature = "deflate-zlib"
)))]
compression_method: CompressionMethod::Stored,
+ compression_level: None,
#[cfg(feature = "time")]
- last_modified_time: DateTime::from_time(time::now()).unwrap_or_default(),
+ last_modified_time: DateTime::from_time(OffsetDateTime::now_utc()).unwrap_or_default(),
#[cfg(not(feature = "time"))]
last_modified_time: DateTime::default(),
permissions: None,
@@ -125,15 +139,32 @@ impl FileOptions {
///
/// The default is `CompressionMethod::Deflated`. If the deflate compression feature is
/// disabled, `CompressionMethod::Stored` becomes the default.
+ #[must_use]
pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions {
self.compression_method = method;
self
}
+ /// Set the compression level for the new file
+ ///
+ /// `None` value specifies default compression level.
+ ///
+ /// Range of values depends on compression method:
+ /// * `Deflated`: 0 - 9. Default is 6
+ /// * `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<i32>) -> FileOptions {
+ self.compression_level = level;
+ self
+ }
+
/// Set the last modified time
///
/// 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 {
self.last_modified_time = mod_time;
self
@@ -144,6 +175,7 @@ impl FileOptions {
/// The format is represented with unix-style permissions.
/// The default is `0o644`, which represents `rw-r--r--` for files,
/// and `0o755`, which represents `rwxr-xr-x` for directories
+ #[must_use]
pub fn unix_permissions(mut self, mode: u32) -> FileOptions {
self.permissions = Some(mode & 0o777);
self
@@ -154,6 +186,7 @@ impl FileOptions {
/// 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`.
+ #[must_use]
pub fn large_file(mut self, large: bool) -> FileOptions {
self.large_file = large;
self
@@ -182,7 +215,7 @@ impl<W: Write + io::Seek> Write for ZipWriter<W> {
let write_result = w.write(buf);
if let Ok(count) = write_result {
self.stats.update(&buf[0..count]);
- if self.stats.bytes_written > 0xFFFFFFFF
+ 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);
@@ -234,7 +267,10 @@ impl<A: Read + Write + io::Seek> ZipWriter<A> {
let (archive_offset, directory_start, number_of_files) =
ZipArchive::get_directory_counts(&mut readwriter, &footer, cde_start_pos)?;
- if let Err(_) = readwriter.seek(io::SeekFrom::Start(directory_start)) {
+ if readwriter
+ .seek(io::SeekFrom::Start(directory_start))
+ .is_err()
+ {
return Err(ZipError::InvalidArchive(
"Could not seek to start of central directory",
));
@@ -304,7 +340,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
{
self.finish_file()?;
- let raw_values = raw_values.unwrap_or_else(|| ZipRawValues {
+ let raw_values = raw_values.unwrap_or(ZipRawValues {
crc32: 0,
compressed_size: 0,
uncompressed_size: 0,
@@ -321,6 +357,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
encrypted: false,
using_data_descriptor: false,
compression_method: options.compression_method,
+ compression_level: options.compression_level,
last_modified_time: options.last_modified_time,
crc32: raw_values.crc32,
compressed_size: raw_values.compressed_size,
@@ -330,16 +367,17 @@ impl<W: Write + io::Seek> ZipWriter<W> {
extra_field: Vec::new(),
file_comment: String::new(),
header_start,
- data_start: 0,
+ data_start: AtomicU64::new(0),
central_header_start: 0,
external_attributes: permissions << 16,
large_file: options.large_file,
+ aes_mode: None,
};
write_local_file_header(writer, &file)?;
let header_end = writer.seek(io::SeekFrom::Current(0))?;
self.stats.start = header_end;
- file.data_start = header_end;
+ *file.data_start.get_mut() = header_end;
self.stats.bytes_written = 0;
self.stats.hasher = Hasher::new();
@@ -355,7 +393,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
// Implicitly calling [`ZipWriter::end_extra_data`] for empty files.
self.end_extra_data()?;
}
- self.inner.switch_to(CompressionMethod::Stored)?;
+ self.inner.switch_to(CompressionMethod::Stored, None)?;
let writer = self.inner.get_plain();
if !self.writing_raw {
@@ -390,7 +428,8 @@ impl<W: Write + io::Seek> ZipWriter<W> {
}
*options.permissions.as_mut().unwrap() |= 0o100000;
self.start_entry(name, options, None)?;
- self.inner.switch_to(options.compression_method)?;
+ self.inner
+ .switch_to(options.compression_method, options.compression_level)?;
self.writing_to_file = true;
Ok(())
}
@@ -518,7 +557,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
self.start_entry(name, options, None)?;
self.writing_to_file = true;
self.writing_to_extra_field = true;
- Ok(self.files.last().unwrap().data_start)
+ Ok(self.files.last().unwrap().data_start.load())
}
/// End local and start central extra data. Requires [`ZipWriter::start_file_with_extra_data`].
@@ -545,7 +584,9 @@ impl<W: Write + io::Seek> ZipWriter<W> {
}
let file = self.files.last_mut().unwrap();
- validate_extra_data(&file)?;
+ 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();
@@ -554,9 +595,9 @@ impl<W: Write + io::Seek> ZipWriter<W> {
writer.write_all(&file.extra_field)?;
// Update final `data_start`.
- let header_end = file.data_start + file.extra_field.len() as u64;
+ let header_end = *data_start + file.extra_field.len() as u64;
self.stats.start = header_end;
- file.data_start = header_end;
+ *data_start = header_end;
// Update extra field length in local file header.
let extra_field_length =
@@ -565,12 +606,13 @@ impl<W: Write + io::Seek> ZipWriter<W> {
writer.write_u16::<LittleEndian>(extra_field_length)?;
writer.seek(io::SeekFrom::Start(header_end))?;
- self.inner.switch_to(file.compression_method)?;
+ 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(file.data_start)
+ Ok(*data_start)
}
/// Add a new file using the already compressed data from a ZIP file being read and renames it, this
@@ -603,11 +645,12 @@ impl<W: Write + io::Seek> ZipWriter<W> {
where
S: Into<String>,
{
- let options = FileOptions::default()
+ let mut options = FileOptions::default()
+ .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR)
.last_modified_time(file.last_modified())
.compression_method(file.compression());
if let Some(perms) = file.unix_mode() {
- options.unix_permissions(perms);
+ options = options.unix_permissions(perms);
}
let raw_values = ZipRawValues {
@@ -716,7 +759,8 @@ impl<W: Write + io::Seek> ZipWriter<W> {
}
let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start;
- if self.files.len() > 0xFFFF || central_size > 0xFFFFFFFF || central_start > 0xFFFFFFFF
+ 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,
@@ -740,27 +784,15 @@ impl<W: Write + io::Seek> ZipWriter<W> {
zip64_footer.write(writer)?;
}
- let number_of_files = if self.files.len() > 0xFFFF {
- 0xFFFF
- } else {
- self.files.len() as u16
- };
+ 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: if central_size > 0xFFFFFFFF {
- 0xFFFFFFFF
- } else {
- central_size as u32
- },
- central_directory_offset: if central_start > 0xFFFFFFFF {
- 0xFFFFFFFF
- } else {
- central_start as u32
- },
+ 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)?;
@@ -774,14 +806,18 @@ impl<W: Write + io::Seek> Drop for ZipWriter<W> {
fn drop(&mut self) {
if !self.inner.is_closed() {
if let Err(e) = self.finalize() {
- let _ = write!(&mut io::stderr(), "ZipWriter drop failed: {:?}", e);
+ let _ = write!(io::stderr(), "ZipWriter drop failed: {:?}", e);
}
}
}
}
impl<W: Write + io::Seek> GenericZipWriter<W> {
- fn switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()> {
+ fn switch_to(
+ &mut self,
+ compression: CompressionMethod,
+ compression_level: Option<i32>,
+ ) -> ZipResult<()> {
match self.current_compression() {
Some(method) if method == compression => return Ok(()),
None => {
@@ -804,6 +840,8 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
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,
@@ -816,7 +854,15 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
*self = {
#[allow(deprecated)]
match compression {
- CompressionMethod::Stored => GenericZipWriter::Storer(bare),
+ CompressionMethod::Stored => {
+ if compression_level.is_some() {
+ return Err(ZipError::UnsupportedArchive(
+ "Unsupported compression level",
+ ));
+ }
+
+ GenericZipWriter::Storer(bare)
+ }
#[cfg(any(
feature = "deflate",
feature = "deflate-miniz",
@@ -824,12 +870,50 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
))]
CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new(
bare,
- flate2::Compression::default(),
+ 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,
+ ),
)),
#[cfg(feature = "bzip2")]
- CompressionMethod::Bzip2 => {
- GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::default()))
+ 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",
+ ))?,
+ )
+ .unwrap(),
+ ),
CompressionMethod::Unsupported(..) => {
return Err(ZipError::UnsupportedArchive("Unsupported compression"))
}
@@ -850,15 +934,14 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
GenericZipWriter::Deflater(ref mut 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,
}
}
fn is_closed(&self) -> bool {
- match *self {
- GenericZipWriter::Closed => true,
- _ => false,
- }
+ matches!(*self, GenericZipWriter::Closed)
}
fn get_plain(&mut self) -> &mut W {
@@ -879,6 +962,8 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated),
#[cfg(feature = "bzip2")]
GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2),
+ #[cfg(feature = "zstd")]
+ GenericZipWriter::Zstd(..) => Some(CompressionMethod::Zstd),
GenericZipWriter::Closed => None,
}
}
@@ -891,6 +976,39 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
}
}
+#[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib"
+))]
+fn deflate_compression_level_range() -> std::ops::RangeInclusive<i32> {
+ let min = flate2::Compression::none().level() as i32;
+ let max = flate2::Compression::best().level() as i32;
+ min..=max
+}
+
+#[cfg(feature = "bzip2")]
+fn bzip2_compression_level_range() -> std::ops::RangeInclusive<i32> {
+ let min = bzip2::Compression::none().level() as i32;
+ let max = bzip2::Compression::best().level() as i32;
+ min..=max
+}
+
+#[cfg(any(
+ feature = "deflate",
+ feature = "deflate-miniz",
+ feature = "deflate-zlib",
+ feature = "bzip2",
+ feature = "zstd"
+))]
+fn clamp_opt<T: Ord + Copy>(value: T, range: std::ops::RangeInclusive<T>) -> Option<T> {
+ if range.contains(&value) {
+ Some(value)
+ } else {
+ None
+ }
+}
+
fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipResult<()> {
// local file header signature
writer.write_u32::<LittleEndian>(spec::LOCAL_FILE_HEADER_SIGNATURE)?;
@@ -911,18 +1029,14 @@ fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipR
writer.write_u16::<LittleEndian>(file.last_modified_time.datepart())?;
// crc-32
writer.write_u32::<LittleEndian>(file.crc32)?;
- // compressed size
- writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
- 0xFFFFFFFF
- } else {
- file.compressed_size as u32
- })?;
- // uncompressed size
- writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
- 0xFFFFFFFF
+ // compressed size and uncompressed size
+ if file.large_file {
+ writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
+ writer.write_u32::<LittleEndian>(spec::ZIP64_BYTES_THR as u32)?;
} else {
- file.uncompressed_size as u32
- })?;
+ writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
+ writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
+ }
// file name length
writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
// extra field length
@@ -932,7 +1046,7 @@ fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipR
writer.write_all(file.file_name.as_bytes())?;
// zip64 extra field
if file.large_file {
- write_local_zip64_extra_field(writer, &file)?;
+ write_local_zip64_extra_field(writer, file)?;
}
Ok(())
@@ -945,27 +1059,19 @@ fn update_local_file_header<T: Write + io::Seek>(
const CRC32_OFFSET: u64 = 14;
writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?;
writer.write_u32::<LittleEndian>(file.crc32)?;
- writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
- if file.large_file {
- 0xFFFFFFFF
- } else {
- // compressed size can be slightly larger than uncompressed size
+ if file.large_file {
+ update_local_zip64_extra_field(writer, file)?;
+ } else {
+ // check compressed size as well as it can also be slightly larger than uncompressed size
+ if file.compressed_size > spec::ZIP64_BYTES_THR {
return Err(ZipError::Io(io::Error::new(
io::ErrorKind::Other,
"Large file option has not been set",
)));
}
- } else {
- file.compressed_size as u32
- })?;
- writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
- // uncompressed size is checked on write to catch it as soon as possible
- 0xFFFFFFFF
- } else {
- file.uncompressed_size as u32
- })?;
- if file.large_file {
- update_local_zip64_extra_field(writer, file)?;
+ writer.write_u32::<LittleEndian>(file.compressed_size as u32)?;
+ // uncompressed size is already checked on write to catch it as soon as possible
+ writer.write_u32::<LittleEndian>(file.uncompressed_size as u32)?;
}
Ok(())
}
@@ -999,17 +1105,9 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
// crc-32
writer.write_u32::<LittleEndian>(file.crc32)?;
// compressed size
- writer.write_u32::<LittleEndian>(if file.compressed_size > 0xFFFFFFFF {
- 0xFFFFFFFF
- } else {
- file.compressed_size as u32
- })?;
+ writer.write_u32::<LittleEndian>(file.compressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
// uncompressed size
- writer.write_u32::<LittleEndian>(if file.uncompressed_size > 0xFFFFFFFF {
- 0xFFFFFFFF
- } else {
- file.uncompressed_size as u32
- })?;
+ writer.write_u32::<LittleEndian>(file.uncompressed_size.min(spec::ZIP64_BYTES_THR) as u32)?;
// file name length
writer.write_u16::<LittleEndian>(file.file_name.as_bytes().len() as u16)?;
// extra field length
@@ -1023,11 +1121,7 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
// external file attributes
writer.write_u32::<LittleEndian>(file.external_attributes)?;
// relative offset of local header
- writer.write_u32::<LittleEndian>(if file.header_start > 0xFFFFFFFF {
- 0xFFFFFFFF
- } else {
- file.header_start as u32
- })?;
+ writer.write_u32::<LittleEndian>(file.header_start.min(spec::ZIP64_BYTES_THR) as u32)?;
// file name
writer.write_all(file.file_name.as_bytes())?;
// zip64 extra field
@@ -1043,14 +1137,14 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> {
let mut data = file.extra_field.as_slice();
- if data.len() > 0xFFFF {
+ if data.len() > spec::ZIP64_ENTRY_THR {
return Err(ZipError::Io(io::Error::new(
io::ErrorKind::InvalidData,
"Extra data exceeds extra field",
)));
}
- while data.len() > 0 {
+ while !data.is_empty() {
let left = data.len();
if left < 4 {
return Err(ZipError::Io(io::Error::new(
@@ -1126,9 +1220,9 @@ fn write_central_zip64_extra_field<T: Write>(writer: &mut T, file: &ZipFileData)
// only appear if the corresponding Local or Central
// directory record field is set to 0xFFFF or 0xFFFFFFFF.
let mut size = 0;
- let uncompressed_size = file.uncompressed_size > 0xFFFFFFFF;
- let compressed_size = file.compressed_size > 0xFFFFFFFF;
- let header_start = file.header_start > 0xFFFFFFFF;
+ let uncompressed_size = file.uncompressed_size > spec::ZIP64_BYTES_THR;
+ let compressed_size = file.compressed_size > spec::ZIP64_BYTES_THR;
+ let header_start = file.header_start > spec::ZIP64_BYTES_THR;
if uncompressed_size {
size += 8;
}
@@ -1224,13 +1318,14 @@ mod test {
let mut writer = ZipWriter::new(io::Cursor::new(Vec::new()));
let options = FileOptions {
compression_method: CompressionMethod::Stored,
+ compression_level: None,
last_modified_time: DateTime::default(),
permissions: Some(33188),
large_file: false,
};
writer.start_file("mimetype", options).unwrap();
writer
- .write(b"application/vnd.oasis.opendocument.text")
+ .write_all(b"application/vnd.oasis.opendocument.text")
.unwrap();
let result = writer.finish().unwrap();
diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs
index 3196ea3..91d4039 100644
--- a/src/zipcrypto.rs
+++ b/src/zipcrypto.rs
@@ -47,7 +47,7 @@ impl ZipCryptoKeys {
}
fn crc32(crc: Wrapping<u32>, input: u8) -> Wrapping<u32> {
- return (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize]);
+ (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize])
}
}
@@ -71,7 +71,7 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
/// password byte sequence that is unrepresentable in UTF-8.
pub fn new(file: R, password: &[u8]) -> ZipCryptoReader<R> {
let mut result = ZipCryptoReader {
- file: file,
+ file,
keys: ZipCryptoKeys::new(),
};
@@ -129,11 +129,11 @@ pub struct ZipCryptoReaderValid<R> {
}
impl<R: std::io::Read> std::io::Read for ZipCryptoReaderValid<R> {
- fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result<usize> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
// Note: There might be potential for optimization. Inspiration can be found at:
// https://github.com/kornelski/7z/blob/master/CPP/7zip/Crypto/ZipCrypto.cpp
- let result = self.reader.file.read(&mut buf);
+ let result = self.reader.file.read(buf);
for byte in buf.iter_mut() {
*byte = self.reader.keys.decrypt_byte(*byte);
}
diff --git a/tests/aes_encryption.rs b/tests/aes_encryption.rs
new file mode 100644
index 0000000..4b393eb
--- /dev/null
+++ b/tests/aes_encryption.rs
@@ -0,0 +1,80 @@
+#![cfg(feature = "aes-crypto")]
+
+use std::io::{self, Read};
+use zip::ZipArchive;
+
+const SECRET_CONTENT: &str = "Lorem ipsum dolor sit amet";
+
+const PASSWORD: &[u8] = b"helloworld";
+
+#[test]
+fn aes256_encrypted_uncompressed_file() {
+ let mut v = Vec::new();
+ v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+ let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+ let mut file = archive
+ .by_name_decrypt("secret_data_256_uncompressed", PASSWORD)
+ .expect("couldn't find file in archive")
+ .expect("invalid password");
+ assert_eq!("secret_data_256_uncompressed", file.name());
+
+ let mut content = String::new();
+ file.read_to_string(&mut content)
+ .expect("couldn't read encrypted file");
+ assert_eq!(SECRET_CONTENT, content);
+}
+
+#[test]
+fn aes256_encrypted_file() {
+ let mut v = Vec::new();
+ v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+ let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+ let mut file = archive
+ .by_name_decrypt("secret_data_256", PASSWORD)
+ .expect("couldn't find file in archive")
+ .expect("invalid password");
+ assert_eq!("secret_data_256", file.name());
+
+ let mut content = String::new();
+ file.read_to_string(&mut content)
+ .expect("couldn't read encrypted and compressed file");
+ assert_eq!(SECRET_CONTENT, content);
+}
+
+#[test]
+fn aes192_encrypted_file() {
+ let mut v = Vec::new();
+ v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+ let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+ let mut file = archive
+ .by_name_decrypt("secret_data_192", PASSWORD)
+ .expect("couldn't find file in archive")
+ .expect("invalid password");
+ assert_eq!("secret_data_192", file.name());
+
+ let mut content = String::new();
+ file.read_to_string(&mut content)
+ .expect("couldn't read encrypted file");
+ assert_eq!(SECRET_CONTENT, content);
+}
+
+#[test]
+fn aes128_encrypted_file() {
+ let mut v = Vec::new();
+ v.extend_from_slice(include_bytes!("data/aes_archive.zip"));
+ let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file");
+
+ let mut file = archive
+ .by_name_decrypt("secret_data_128", PASSWORD)
+ .expect("couldn't find file in archive")
+ .expect("invalid password");
+ assert_eq!("secret_data_128", file.name());
+
+ let mut content = String::new();
+ file.read_to_string(&mut content)
+ .expect("couldn't read encrypted file");
+ assert_eq!(SECRET_CONTENT, content);
+}
diff --git a/tests/data/aes_archive.zip b/tests/data/aes_archive.zip
new file mode 100644
index 0000000..4cf1fd2
--- /dev/null
+++ b/tests/data/aes_archive.zip
Binary files differ
diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs
index baebd28..25d0c54 100644
--- a/tests/end_to_end.rs
+++ b/tests/end_to_end.rs
@@ -4,106 +4,135 @@ use std::io::prelude::*;
use std::io::{Cursor, Seek};
use std::iter::FromIterator;
use zip::write::FileOptions;
-use zip::CompressionMethod;
+use zip::{CompressionMethod, 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.
#[test]
fn end_to_end() {
- let file = &mut Cursor::new(Vec::new());
+ for &method in SUPPORTED_COMPRESSION_METHODS {
+ let file = &mut Cursor::new(Vec::new());
- write_to_zip(file).expect("file written");
+ println!("Writing file with {} compression", method);
+ write_test_archive(file, method).expect("Couldn't write test zip archive");
- check_zip_contents(file, ENTRY_NAME);
+ println!("Checking file contents");
+ check_archive_file(file, ENTRY_NAME, Some(method), LOREM_IPSUM);
+ }
}
// This test asserts that after copying a `ZipFile` to a new `ZipWriter`, then reading its
// contents back out, the extracted data will *always* be exactly the same as the original data.
#[test]
fn copy() {
- let src_file = &mut Cursor::new(Vec::new());
- write_to_zip(src_file).expect("file written");
-
- let mut tgt_file = &mut Cursor::new(Vec::new());
+ for &method in SUPPORTED_COMPRESSION_METHODS {
+ let src_file = &mut Cursor::new(Vec::new());
+ write_test_archive(src_file, method).expect("Couldn't write to test file");
- {
- let mut src_archive = zip::ZipArchive::new(src_file).unwrap();
- let mut zip = zip::ZipWriter::new(&mut tgt_file);
+ let mut tgt_file = &mut Cursor::new(Vec::new());
{
- let file = src_archive.by_name(ENTRY_NAME).expect("file found");
- zip.raw_copy_file(file).unwrap();
- }
+ let mut src_archive = zip::ZipArchive::new(src_file).unwrap();
+ let mut zip = zip::ZipWriter::new(&mut tgt_file);
- {
- let file = src_archive.by_name(ENTRY_NAME).expect("file found");
- zip.raw_copy_file_rename(file, COPY_ENTRY_NAME).unwrap();
+ {
+ let file = src_archive
+ .by_name(ENTRY_NAME)
+ .expect("Missing expected file");
+
+ zip.raw_copy_file(file).expect("Couldn't copy file");
+ }
+
+ {
+ let file = src_archive
+ .by_name(ENTRY_NAME)
+ .expect("Missing expected file");
+
+ zip.raw_copy_file_rename(file, COPY_ENTRY_NAME)
+ .expect("Couldn't copy and rename file");
+ }
}
- }
- let mut tgt_archive = zip::ZipArchive::new(tgt_file).unwrap();
+ let mut tgt_archive = zip::ZipArchive::new(tgt_file).unwrap();
- check_zip_file_contents(&mut tgt_archive, ENTRY_NAME);
- check_zip_file_contents(&mut tgt_archive, COPY_ENTRY_NAME);
+ check_archive_file_contents(&mut tgt_archive, ENTRY_NAME, LOREM_IPSUM);
+ check_archive_file_contents(&mut tgt_archive, COPY_ENTRY_NAME, LOREM_IPSUM);
+ }
}
// This test asserts that after appending to a `ZipWriter`, then reading its contents back out,
// both the prior data and the appended data will be exactly the same as their originals.
#[test]
fn append() {
- let mut file = &mut Cursor::new(Vec::new());
- write_to_zip(file).expect("file written");
+ for &method in SUPPORTED_COMPRESSION_METHODS {
+ let mut file = &mut Cursor::new(Vec::new());
+ write_test_archive(file, method).expect("Couldn't write to test file");
- {
- let mut zip = zip::ZipWriter::new_append(&mut file).unwrap();
- zip.start_file(COPY_ENTRY_NAME, Default::default()).unwrap();
- zip.write_all(LOREM_IPSUM).unwrap();
- zip.finish().unwrap();
- }
+ {
+ 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 = zip::ZipArchive::new(&mut file).unwrap();
- check_zip_file_contents(&mut zip, ENTRY_NAME);
- check_zip_file_contents(&mut zip, COPY_ENTRY_NAME);
+ 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);
+ }
}
-fn write_to_zip(file: &mut Cursor<Vec<u8>>) -> zip::result::ZipResult<()> {
+// Write a test zip archive to buffer.
+fn write_test_archive(
+ file: &mut Cursor<Vec<u8>>,
+ method: CompressionMethod,
+) -> zip::result::ZipResult<()> {
let mut zip = zip::ZipWriter::new(file);
zip.add_directory("test/", Default::default())?;
let options = FileOptions::default()
- .compression_method(CompressionMethod::Stored)
+ .compression_method(method)
.unix_permissions(0o755);
+
zip.start_file("test/☃.txt", options)?;
zip.write_all(b"Hello, World!\n")?;
- zip.start_file_with_extra_data("test_with_extra_data/🐢.txt", Default::default())?;
+ zip.start_file_with_extra_data("test_with_extra_data/🐢.txt", options)?;
zip.write_u16::<LittleEndian>(0xbeef)?;
zip.write_u16::<LittleEndian>(EXTRA_DATA.len() as u16)?;
zip.write_all(EXTRA_DATA)?;
zip.end_extra_data()?;
zip.write_all(b"Hello, World! Again.\n")?;
- zip.start_file(ENTRY_NAME, Default::default())?;
+ zip.start_file(ENTRY_NAME, options)?;
zip.write_all(LOREM_IPSUM)?;
zip.finish()?;
Ok(())
}
-fn read_zip<R: Read + Seek>(zip_file: R) -> zip::result::ZipResult<zip::ZipArchive<R>> {
+// Load an archive from buffer and check for test data.
+fn check_test_archive<R: Read + Seek>(zip_file: R) -> zip::result::ZipResult<zip::ZipArchive<R>> {
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
- let expected_file_names = [
- "test/",
- "test/☃.txt",
- "test_with_extra_data/🐢.txt",
- ENTRY_NAME,
- ];
- let expected_file_names = HashSet::from_iter(expected_file_names.iter().map(|&v| v));
- let file_names = archive.file_names().collect::<HashSet<_>>();
- assert_eq!(file_names, expected_file_names);
+ // Check archive contains expected file names.
+ {
+ let expected_file_names = [
+ "test/",
+ "test/☃.txt",
+ "test_with_extra_data/🐢.txt",
+ ENTRY_NAME,
+ ];
+ let expected_file_names = HashSet::from_iter(expected_file_names.iter().copied());
+ let file_names = archive.file_names().collect::<HashSet<_>>();
+ assert_eq!(file_names, expected_file_names);
+ }
+ // Check an archive file for extra data field contents.
{
let file_with_extra_data = archive.by_name("test_with_extra_data/🐢.txt")?;
let mut extra_data = Vec::new();
@@ -116,7 +145,8 @@ fn read_zip<R: Read + Seek>(zip_file: R) -> zip::result::ZipResult<zip::ZipArchi
Ok(archive)
}
-fn read_zip_file<R: Read + Seek>(
+// Read a file in the archive as a string.
+fn read_archive_file<R: Read + Seek>(
archive: &mut zip::ZipArchive<R>,
name: &str,
) -> zip::result::ZipResult<String> {
@@ -124,27 +154,51 @@ fn read_zip_file<R: Read + Seek>(
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
+
Ok(contents)
}
-fn check_zip_contents(zip_file: &mut Cursor<Vec<u8>>, name: &str) {
- let mut archive = read_zip(zip_file).unwrap();
- check_zip_file_contents(&mut archive, name);
+// Check a file in the archive contains expected data and properties.
+fn check_archive_file(
+ zip_file: &mut Cursor<Vec<u8>>,
+ name: &str,
+ expected_method: Option<CompressionMethod>,
+ expected_data: &[u8],
+) {
+ let mut archive = check_test_archive(zip_file).unwrap();
+
+ if let Some(expected_method) = expected_method {
+ // Check the file's compression method.
+ let file = archive.by_name(name).unwrap();
+ let real_method = file.compression();
+
+ assert_eq!(
+ expected_method, real_method,
+ "File does not have expected compression method"
+ );
+ }
+
+ check_archive_file_contents(&mut archive, name, expected_data);
}
-fn check_zip_file_contents<R: Read + Seek>(archive: &mut zip::ZipArchive<R>, name: &str) {
- let file_contents: String = read_zip_file(archive, name).unwrap();
- assert!(file_contents.as_bytes() == LOREM_IPSUM);
+// Check a file in the archive contains the given data.
+fn check_archive_file_contents<R: Read + Seek>(
+ archive: &mut zip::ZipArchive<R>,
+ name: &str,
+ expected: &[u8],
+) {
+ let file_contents: String = read_archive_file(archive, name).unwrap();
+ assert_eq!(file_contents.as_bytes(), expected);
}
-const LOREM_IPSUM : &'static [u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
+const LOREM_IPSUM : &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet
molestie. Proin blandit ornare dui, a tempor nisl accumsan in. Praesent a consequat felis. Morbi metus diam, auctor in auctor vel, feugiat id odio. Curabitur ex ex,
dictum quis auctor quis, suscipit id lorem. Aliquam vestibulum dolor nec enim vehicula, porta tristique augue tincidunt. Vivamus ut gravida est. Sed pellentesque, dolor
vitae tristique consectetur, neque lectus pulvinar dui, sed feugiat purus diam id lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per
inceptos himenaeos. Maecenas feugiat velit in ex ultrices scelerisque id id neque.
";
-const EXTRA_DATA: &'static [u8] = b"Extra Data";
+const EXTRA_DATA: &[u8] = b"Extra Data";
const ENTRY_NAME: &str = "test/lorem_ipsum.txt";
diff --git a/tests/issue_234.rs b/tests/issue_234.rs
new file mode 100644
index 0000000..bd01d1d
--- /dev/null
+++ b/tests/issue_234.rs
@@ -0,0 +1,31 @@
+use zip::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,
+ 0, 65, 0, 1, 0, 0, 0, 4, 0, 0, 224, 255, 0, 255, 255, 255, 255, 255, 255, 20, 39, 221, 221,
+ 221, 221, 221, 221, 205, 221, 221, 221, 42, 221, 221, 221, 221, 221, 221, 221, 221, 38, 34, 34,
+ 219, 80, 75, 5, 6, 0, 0, 0, 0, 5, 96, 0, 1, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 234, 236, 124,
+ 221, 221, 37, 221, 221, 221, 221, 221, 129, 4, 0, 0, 221, 221, 80, 75, 1, 2, 127, 120, 0, 4, 0,
+ 0, 2, 127, 120, 0, 79, 75, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0,
+ 234, 0, 0, 0, 3, 8, 4, 232, 3, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 7, 0, 0, 0, 0, 3, 0,
+ 221, 209, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 58, 58, 42, 75, 9, 2, 127,
+ 120, 0, 99, 99, 99, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0, 213, 213, 213, 213, 213, 213, 213,
+ 213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0, 213, 213,
+ 213, 213, 213, 213, 213, 213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236, 234, 0, 0, 0, 3, 8,
+ 0, 0, 0, 12, 0, 0, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 0, 0, 58, 58, 58, 42, 175, 221, 253, 221,
+ 221, 221, 221, 221, 80, 75, 9, 2, 127, 120, 0, 99, 99, 99, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0,
+ 213, 213, 213, 213, 213, 213, 213, 213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236, 221, 221,
+ 221, 221, 221, 80, 75, 9, 2, 127, 120, 0, 99, 99, 99, 99, 99, 99, 94, 7, 0, 0, 0, 0, 0, 0, 213,
+ 213, 213, 213, 213, 213, 213, 213, 213, 7, 0, 0, 211, 211, 211, 211, 124, 236,
+];
+
+#[test]
+fn invalid_header() {
+ let reader = std::io::Cursor::new(&BUF);
+ let archive = zip::ZipArchive::new(reader);
+ match archive {
+ Err(ZipError::InvalidArchive(_)) => {}
+ value => panic!("Unexpected value: {:?}", value),
+ }
+}