diff options
-rw-r--r-- | .cargo_vcs_info.json | 2 | ||||
-rw-r--r-- | .github/workflows/ci.yaml | 18 | ||||
-rw-r--r-- | Android.bp | 5 | ||||
-rw-r--r-- | CHANGELOG.md | 9 | ||||
-rw-r--r-- | Cargo.toml | 57 | ||||
-rw-r--r-- | Cargo.toml.orig | 15 | ||||
-rw-r--r-- | METADATA | 14 | ||||
-rw-r--r-- | README.md | 29 | ||||
-rw-r--r-- | TEST_MAPPING | 26 | ||||
-rw-r--r-- | benches/read_metadata.rs | 38 | ||||
-rw-r--r-- | examples/extract.rs | 6 | ||||
-rw-r--r-- | examples/extract_lorem.rs | 4 | ||||
-rw-r--r-- | examples/file_info.rs | 4 | ||||
-rw-r--r-- | examples/stdin_info.rs | 4 | ||||
-rw-r--r-- | examples/write_dir.rs | 12 | ||||
-rw-r--r-- | examples/write_sample.rs | 6 | ||||
-rw-r--r-- | patches/Android.bp.diff | 4 | ||||
-rw-r--r-- | src/compression.rs | 6 | ||||
-rw-r--r-- | src/read.rs | 68 | ||||
-rw-r--r-- | src/result.rs | 21 | ||||
-rw-r--r-- | src/spec.rs | 16 | ||||
-rw-r--r-- | src/types.rs | 91 | ||||
-rw-r--r-- | src/write.rs | 180 | ||||
-rw-r--r-- | tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip | bin | 0 -> 124 bytes | |||
-rw-r--r-- | tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip | bin | 0 -> 212 bytes | |||
-rw-r--r-- | tests/end_to_end.rs | 2 | ||||
-rw-r--r-- | tests/zip64_large.rs | 2 |
27 files changed, 488 insertions, 151 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index b50dc02..535489c 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "4f7609cec700765525a537747c8f340dd1090aa0" + "sha1": "f7dcc666b75256e766295589a5ac5dc5a9617c39" }, "path_in_vcs": "" }
\ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6f0e4b9..35d4a6e 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.54.0] + rust: [stable, 1.59.0] steps: - uses: actions/checkout@master @@ -74,3 +74,19 @@ jobs: - name: Docs run: cargo doc + + fuzz: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + + - run: cargo install cargo-fuzz + - name: compile fuzz + run: | + cargo fuzz build fuzz_read @@ -20,10 +20,11 @@ license { rust_library { name: "libzip", + // has rustc warnings host_supported: true, crate_name: "zip", cargo_env_compat: true, - cargo_pkg_version: "0.6.2", + cargo_pkg_version: "0.6.4", srcs: ["src/lib.rs"], edition: "2018", features: [ @@ -39,6 +40,8 @@ rust_library { "//apex_available:platform", "com.android.virt", ], + product_available: true, + vendor_available: true, arch: { arm: { rustlibs: ["libcrossbeam_utils"], diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cd79e39 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## [0.6.4] + +### Changed + + - [#333](https://github.com/zip-rs/zip/pull/333): disabled the default features of the `time` dependency, and also `formatting` and `macros`, as they were enabled by mistake. + - Deprecated [`DateTime::from_time`](https://docs.rs/zip/0.6/zip/struct.DateTime.html#method.from_time) in favor of [`DateTime::try_from`](https://docs.rs/zip/0.6/zip/struct.DateTime.html#impl-TryFrom-for-DateTime) +
\ No newline at end of file @@ -12,16 +12,31 @@ [package] edition = "2018" name = "zip" -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"] +version = "0.6.4" +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. +""" +readme = "README.md" +keywords = [ + "zip", + "archive", +] license = "MIT" repository = "https://github.com/zip-rs/zip.git" [[bench]] name = "read_entry" harness = false + +[[bench]] +name = "read_metadata" +harness = false + [dependencies.aes] version = "0.7.5" optional = true @@ -41,7 +56,7 @@ optional = true version = "1.3.2" [dependencies.flate2] -version = "1.0.22" +version = "1.0.23" optional = true default-features = false @@ -51,7 +66,7 @@ features = ["reset"] optional = true [dependencies.pbkdf2] -version = "0.10.1" +version = "0.11.0" optional = true [dependencies.sha1] @@ -60,27 +75,49 @@ optional = true [dependencies.time] version = "0.3.7" -features = ["formatting", "macros"] +features = ["std"] optional = true +default-features = false [dependencies.zstd] -version = "0.10.0" +version = "0.11.2" optional = true + [dev-dependencies.bencher] version = "0.1.5" [dev-dependencies.getrandom] version = "0.2.5" +[dev-dependencies.time] +version = "0.3.7" +features = [ + "formatting", + "macros", +] + [dev-dependencies.walkdir] version = "2.3.2" [features] -aes-crypto = ["aes", "constant_time_eq", "hmac", "pbkdf2", "sha1"] -default = ["aes-crypto", "bzip2", "deflate", "time", "zstd"] +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 cc87821..caf6a07 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "zip" -version = "0.6.2" +version = "0.6.4" 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" @@ -16,12 +16,12 @@ 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 } +flate2 = { version = "1.0.23", default-features = false, optional = true } hmac = { version = "0.12.1", optional = true, features = ["reset"] } -pbkdf2 = {version = "0.10.1", optional = true } +pbkdf2 = {version = "0.11.0", 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 } +time = { version = "0.3.7", optional = true, default-features = false, features = ["std"] } +zstd = { version = "0.11.2", 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" @@ -30,6 +30,7 @@ crossbeam-utils = "0.8.8" bencher = "0.1.5" getrandom = "0.2.5" walkdir = "2.3.2" +time = { version = "0.3.7", features = ["formatting", "macros"] } [features] aes-crypto = [ "aes", "constant_time_eq", "hmac", "pbkdf2", "sha1" ] @@ -42,3 +43,7 @@ default = ["aes-crypto", "bzip2", "deflate", "time", "zstd"] [[bench]] name = "read_entry" harness = false + +[[bench]] +name = "read_metadata" +harness = false @@ -1,3 +1,7 @@ +# This project was upgraded with external_updater. +# Usage: tools/external_updater/updater.sh update rust/crates/zip +# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md + name: "zip" description: "Library to support the reading and writing of zip files." third_party { @@ -7,13 +11,13 @@ third_party { } url { type: ARCHIVE - value: "https://static.crates.io/crates/zip/zip-0.6.2.crate" + value: "https://static.crates.io/crates/zip/zip-0.6.4.crate" } - version: "0.6.2" + version: "0.6.4" license_type: NOTICE last_upgrade_date { - year: 2022 - month: 4 - day: 13 + year: 2023 + month: 2 + day: 17 } } @@ -5,7 +5,7 @@ zip-rs [![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.6.2/zip/) +[Documentation](https://docs.rs/zip/0.6.3/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. @@ -35,14 +35,14 @@ With all default features: ```toml [dependencies] -zip = "0.6.2" +zip = "0.6.4" ``` Without the default features: ```toml [dependencies] -zip = { version = "0.6.2", default-features = false } +zip = { version = "0.6.4", default-features = false } ``` The features available are: @@ -58,7 +58,7 @@ All of these are enabled by default. MSRV ---- -Our current Minimum Supported Rust Version is **1.54.0**. When adding features, +Our current Minimum Supported Rust Version is **1.59.0**. When adding features, we will follow these guidelines: - We will always support the latest four minor Rust versions. This gives you a 6 @@ -75,3 +75,24 @@ See the [examples directory](examples) for: * How to extract a zip file. * How to extract a single file from a zip. * How to read a zip from the standard input. + +Fuzzing +------- + +Fuzzing support is through [cargo fuzz](https://github.com/rust-fuzz/cargo-fuzz). To install cargo fuzz: + +```bash +cargo install cargo-fuzz +``` + +To list fuzz targets: + +```bash +cargo +nightly fuzz list +``` + +To start fuzzing zip extraction: + +```bash +cargo +nightly fuzz run fuzz_read +``` diff --git a/TEST_MAPPING b/TEST_MAPPING index 7325ef4..bbc8a7f 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -1,37 +1,29 @@ // Generated by update_crate_tests.py for tests that depend on this crate. { - "presubmit": [ + "imports": [ { - "name": "ZipFuseTest" + "path": "packages/modules/Virtualization/apkdmverity" }, { - "name": "libapkverify.integration_test" + "path": "packages/modules/Virtualization/avmd" }, { - "name": "libapkverify.test" + "path": "packages/modules/Virtualization/libs/apexutil" }, { - "name": "microdroid_manager_test" + "path": "packages/modules/Virtualization/libs/apkverify" }, { - "name": "virtualizationservice_device_test" - } - ], - "presubmit-rust": [ - { - "name": "ZipFuseTest" - }, - { - "name": "libapkverify.integration_test" + "path": "packages/modules/Virtualization/microdroid_manager" }, { - "name": "libapkverify.test" + "path": "packages/modules/Virtualization/virtualizationmanager" }, { - "name": "microdroid_manager_test" + "path": "packages/modules/Virtualization/vm" }, { - "name": "virtualizationservice_device_test" + "path": "packages/modules/Virtualization/zipfuse" } ] } diff --git a/benches/read_metadata.rs b/benches/read_metadata.rs new file mode 100644 index 0000000..95334b1 --- /dev/null +++ b/benches/read_metadata.rs @@ -0,0 +1,38 @@ +use bencher::{benchmark_group, benchmark_main}; + +use std::io::{Cursor, Write}; + +use bencher::Bencher; +use zip::{ZipArchive, ZipWriter}; + +const FILE_COUNT: usize = 15_000; +const FILE_SIZE: usize = 1024; + +fn generate_random_archive(count_files: usize, file_size: usize) -> Vec<u8> { + let data = Vec::new(); + let mut writer = ZipWriter::new(Cursor::new(data)); + let options = + zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + + let bytes = vec![0u8; file_size]; + + for i in 0..count_files { + let name = format!("file_deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef_{i}.dat"); + writer.start_file(name, options).unwrap(); + writer.write_all(&bytes).unwrap(); + } + + writer.finish().unwrap().into_inner() +} + +fn read_metadata(bench: &mut Bencher) { + let bytes = generate_random_archive(FILE_COUNT, FILE_SIZE); + + bench.iter(|| { + let archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); + archive.len() + }); +} + +benchmark_group!(benches, read_metadata); +benchmark_main!(benches); diff --git a/examples/extract.rs b/examples/extract.rs index 7b8860c..3080716 100644 --- a/examples/extract.rs +++ b/examples/extract.rs @@ -12,7 +12,7 @@ fn real_main() -> i32 { return 1; } let fname = std::path::Path::new(&*args[1]); - let file = fs::File::open(&fname).unwrap(); + let file = fs::File::open(fname).unwrap(); let mut archive = zip::ZipArchive::new(file).unwrap(); @@ -26,7 +26,7 @@ fn real_main() -> i32 { { let comment = file.comment(); if !comment.is_empty() { - println!("File {} comment: {}", i, comment); + println!("File {i} comment: {comment}"); } } @@ -42,7 +42,7 @@ fn real_main() -> i32 { ); if let Some(p) = outpath.parent() { if !p.exists() { - fs::create_dir_all(&p).unwrap(); + fs::create_dir_all(p).unwrap(); } } let mut outfile = fs::File::create(&outpath).unwrap(); diff --git a/examples/extract_lorem.rs b/examples/extract_lorem.rs index a34a04f..bc50abe 100644 --- a/examples/extract_lorem.rs +++ b/examples/extract_lorem.rs @@ -11,7 +11,7 @@ fn real_main() -> i32 { return 1; } let fname = std::path::Path::new(&*args[1]); - let zipfile = std::fs::File::open(&fname).unwrap(); + let zipfile = std::fs::File::open(fname).unwrap(); let mut archive = zip::ZipArchive::new(zipfile).unwrap(); @@ -25,7 +25,7 @@ fn real_main() -> i32 { let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); - println!("{}", contents); + println!("{contents}"); 0 } diff --git a/examples/file_info.rs b/examples/file_info.rs index 64969b6..6a2adc5 100644 --- a/examples/file_info.rs +++ b/examples/file_info.rs @@ -12,7 +12,7 @@ fn real_main() -> i32 { return 1; } let fname = std::path::Path::new(&*args[1]); - let file = fs::File::open(&fname).unwrap(); + let file = fs::File::open(fname).unwrap(); let reader = BufReader::new(file); let mut archive = zip::ZipArchive::new(reader).unwrap(); @@ -30,7 +30,7 @@ fn real_main() -> i32 { { let comment = file.comment(); if !comment.is_empty() { - println!("Entry {} comment: {}", i, comment); + println!("Entry {i} comment: {comment}"); } } diff --git a/examples/stdin_info.rs b/examples/stdin_info.rs index 10d7aa8..a609916 100644 --- a/examples/stdin_info.rs +++ b/examples/stdin_info.rs @@ -20,12 +20,12 @@ fn real_main() -> i32 { ); match file.read(&mut buf) { Ok(n) => println!("The first {} bytes are: {:?}", n, &buf[0..n]), - Err(e) => println!("Could not read the file: {:?}", e), + Err(e) => println!("Could not read the file: {e:?}"), }; } Ok(None) => break, Err(e) => { - println!("Error encountered while reading zip: {:?}", e); + println!("Error encountered while reading zip: {e:?}"); return 1; } } diff --git a/examples/write_dir.rs b/examples/write_dir.rs index 8cc561f..3b04352 100644 --- a/examples/write_dir.rs +++ b/examples/write_dir.rs @@ -54,8 +54,8 @@ fn real_main() -> i32 { continue; } match doit(src_dir, dst_file, method.unwrap()) { - Ok(_) => println!("done: {} written to {}", src_dir, dst_file), - Err(e) => println!("Error: {:?}", e), + Ok(_) => println!("done: {src_dir} written to {dst_file}"), + Err(e) => println!("Error: {e:?}"), } } @@ -84,18 +84,18 @@ where // Write file or directory explicitly // Some unzip tools unzip files with directory paths correctly, some do not! if path.is_file() { - println!("adding file {:?} as {:?} ...", path, name); + println!("adding file {path:?} as {name:?} ..."); #[allow(deprecated)] zip.start_file_from_path(name, options)?; let mut f = File::open(path)?; f.read_to_end(&mut buffer)?; - zip.write_all(&*buffer)?; + zip.write_all(&buffer)?; buffer.clear(); } 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); + println!("adding dir {path:?} as {name:?} ..."); #[allow(deprecated)] zip.add_directory_from_path(name, options)?; } @@ -114,7 +114,7 @@ fn doit( } let path = Path::new(dst_file); - let file = File::create(&path).unwrap(); + let file = File::create(path).unwrap(); let walkdir = WalkDir::new(src_dir); let it = walkdir.into_iter(); diff --git a/examples/write_sample.rs b/examples/write_sample.rs index b574950..2e45cb1 100644 --- a/examples/write_sample.rs +++ b/examples/write_sample.rs @@ -14,8 +14,8 @@ fn real_main() -> i32 { let filename = &*args[1]; match doit(filename) { - Ok(_) => println!("File written to {}", filename), - Err(e) => println!("Error: {:?}", e), + Ok(_) => println!("File written to {filename}"), + Err(e) => println!("Error: {e:?}"), } 0 @@ -23,7 +23,7 @@ fn real_main() -> i32 { fn doit(filename: &str) -> zip::result::ZipResult<()> { let path = std::path::Path::new(filename); - let file = std::fs::File::create(&path).unwrap(); + let file = std::fs::File::create(path).unwrap(); let mut zip = zip::ZipWriter::new(file); diff --git a/patches/Android.bp.diff b/patches/Android.bp.diff index c0037cc..dfeb034 100644 --- a/patches/Android.bp.diff +++ b/patches/Android.bp.diff @@ -2,10 +2,12 @@ diff --git a/Android.bp b/Android.bp index 2373253..c0b2349 100644 --- a/Android.bp +++ b/Android.bp -@@ -39,4 +39,9 @@ rust_library { +@@ -39,6 +39,11 @@ rust_library { "//apex_available:platform", "com.android.virt", ], + product_available: true, + vendor_available: true, + arch: { + arm: { + rustlibs: ["libcrossbeam_utils"], diff --git a/src/compression.rs b/src/compression.rs index abd8b53..baec939 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -141,7 +141,7 @@ impl CompressionMethod { impl fmt::Display for CompressionMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Just duplicate what the Debug format looks like, i.e, the enum key: - write!(f, "{:?}", self) + write!(f, "{self:?}") } } @@ -195,8 +195,8 @@ mod test { #[test] fn to_display_fmt() { fn check_match(method: CompressionMethod) { - let debug_str = format!("{:?}", method); - let display_str = format!("{}", method); + let debug_str = format!("{method:?}"); + let display_str = format!("{method}"); assert_eq!(debug_str, display_str); } diff --git a/src/read.rs b/src/read.rs index c619f24..dad20c2 100644 --- a/src/read.rs +++ b/src/read.rs @@ -348,7 +348,9 @@ impl<R: Read + io::Seek> ZipArchive<R> { Some(locator64) => { // If we got here, this is indeed a ZIP64 file. - if footer.disk_number as u32 != locator64.disk_with_central_directory { + if !footer.record_too_small() + && footer.disk_number as u32 != locator64.disk_with_central_directory + { return unsupported_zip_error( "Support for multi-disk files is not implemented", ); @@ -401,15 +403,23 @@ impl<R: Read + io::Seek> ZipArchive<R> { pub fn new(mut reader: R) -> ZipResult<ZipArchive<R>> { let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut reader)?; - if footer.disk_number != footer.disk_with_central_directory { + if !footer.record_too_small() && footer.disk_number != footer.disk_with_central_directory { return unsupported_zip_error("Support for multi-disk files is not implemented"); } let (archive_offset, directory_start, number_of_files) = Self::get_directory_counts(&mut reader, &footer, cde_start_pos)?; - let mut files = Vec::new(); - let mut names_map = HashMap::new(); + // If the parsed number of files is greater than the offset then + // something fishy is going on and we shouldn't trust number_of_files. + let file_capacity = if number_of_files > cde_start_pos as usize { + 0 + } else { + number_of_files + }; + + let mut files = Vec::with_capacity(file_capacity); + let mut names_map = HashMap::with_capacity(file_capacity); if reader.seek(io::SeekFrom::Start(directory_start)).is_err() { return Err(ZipError::InvalidArchive( @@ -453,7 +463,7 @@ impl<R: Read + io::Seek> ZipArchive<R> { } else { if let Some(p) = outpath.parent() { if !p.exists() { - fs::create_dir_all(&p)?; + fs::create_dir_all(p)?; } } let mut outfile = fs::File::create(&outpath)?; @@ -639,7 +649,7 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>( reader: &mut R, archive_offset: u64, ) -> ZipResult<ZipFileData> { - let central_header_start = reader.seek(io::SeekFrom::Current(0))?; + let central_header_start = reader.stream_position()?; // Parse central header let signature = reader.read_u32::<LittleEndian>()?; if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE { @@ -673,11 +683,11 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>( reader.read_exact(&mut file_comment_raw)?; let file_name = match is_utf8 { - true => String::from_utf8_lossy(&*file_name_raw).into_owned(), + true => String::from_utf8_lossy(&file_name_raw).into_owned(), false => file_name_raw.clone().from_cp437(), }; let file_comment = match is_utf8 { - true => String::from_utf8_lossy(&*file_comment_raw).into_owned(), + true => String::from_utf8_lossy(&file_comment_raw).into_owned(), false => file_comment_raw.from_cp437(), }; @@ -912,12 +922,12 @@ impl<'a> ZipFile<'a> { self.data.compression_method } - /// Get the size of the file in the archive + /// Get the size of the file, in bytes, in the archive pub fn compressed_size(&self) -> u64 { self.data.compressed_size } - /// Get the size of the file when uncompressed + /// Get the size of the file, in bytes, when uncompressed pub fn size(&self) -> u64 { self.data.uncompressed_size } @@ -949,7 +959,7 @@ impl<'a> ZipFile<'a> { match self.data.system { System::Unix => Some(self.data.external_attributes >> 16), System::Dos => { - // Interpret MSDOS directory bit + // Interpret MS-DOS directory bit let mut mode = if 0x10 == (self.data.external_attributes & 0x10) { ffi::S_IFDIR | 0o0775 } else { @@ -1077,7 +1087,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( reader.read_exact(&mut extra_field)?; let file_name = match is_utf8 { - true => String::from_utf8_lossy(&*file_name_raw).into_owned(), + true => String::from_utf8_lossy(&file_name_raw).into_owned(), false => file_name_raw.clone().from_cp437(), }; @@ -1121,7 +1131,7 @@ pub fn read_zipfile_from_stream<'a, R: io::Read>( return unsupported_zip_error("The file length is not available in the local header"); } - let limit_reader = (reader as &'a mut dyn io::Read).take(result.compressed_size as u64); + let limit_reader = (reader as &'a mut dyn io::Read).take(result.compressed_size); let result_crc32 = result.crc32; let result_compression_method = result.compression_method; @@ -1267,4 +1277,36 @@ mod test { ); } } + + /// test case to ensure we don't preemptively over allocate based on the + /// declared number of files in the CDE of an invalid zip when the number of + /// files declared is more than the alleged offset in the CDE + #[test] + fn invalid_cde_number_of_files_allocation_smaller_offset() { + use super::ZipArchive; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!( + "../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip" + )); + let reader = ZipArchive::new(io::Cursor::new(v)); + assert!(reader.is_err()); + } + + /// test case to ensure we don't preemptively over allocate based on the + /// declared number of files in the CDE of an invalid zip when the number of + /// files declared is less than the alleged offset in the CDE + #[test] + fn invalid_cde_number_of_files_allocation_greater_offset() { + use super::ZipArchive; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!( + "../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip" + )); + let reader = ZipArchive::new(io::Cursor::new(v)); + assert!(reader.is_err()); + } } diff --git a/src/result.rs b/src/result.rs index 72a30e4..00d558c 100644 --- a/src/result.rs +++ b/src/result.rs @@ -44,9 +44,9 @@ impl From<io::Error> for ZipError { 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::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"), } } @@ -81,3 +81,18 @@ impl From<ZipError> for io::Error { io::Error::new(io::ErrorKind::Other, err) } } + +/// Error type for time parsing +#[derive(Debug)] +pub struct DateTimeRangeError; + +impl fmt::Display for DateTimeRangeError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!( + fmt, + "a date could not be represented within the bounds the MS-DOS date range (1980-2107)" + ) + } +} + +impl Error for DateTimeRangeError {} diff --git a/src/spec.rs b/src/spec.rs index 3ffcf73..1d8cb0a 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -23,6 +23,18 @@ pub struct CentralDirectoryEnd { } impl CentralDirectoryEnd { + // Per spec 4.4.1.4 - a CentralDirectoryEnd field might be insufficient to hold the + // required data. In this case the file SHOULD contain a ZIP64 format record + // and the field of this record will be set to -1 + pub(crate) fn record_too_small(&self) -> bool { + self.disk_number == 0xFFFF + || self.disk_with_central_directory == 0xFFFF + || self.number_of_files_on_this_disk == 0xFFFF + || self.number_of_files == 0xFFFF + || self.central_directory_size == 0xFFFFFFFF + || self.central_directory_offset == 0xFFFFFFFF + } + pub fn parse<T: Read>(reader: &mut T) -> ZipResult<CentralDirectoryEnd> { let magic = reader.read_u32::<LittleEndian>()?; if magic != CENTRAL_DIRECTORY_END_SIGNATURE { @@ -64,12 +76,12 @@ impl CentralDirectoryEnd { let mut pos = file_length - HEADER_SIZE; while pos >= search_upper_bound { - reader.seek(io::SeekFrom::Start(pos as u64))?; + reader.seek(io::SeekFrom::Start(pos))?; if reader.read_u32::<LittleEndian>()? == CENTRAL_DIRECTORY_END_SIGNATURE { reader.seek(io::SeekFrom::Current( BYTES_BETWEEN_MAGIC_AND_COMMENT_SIZE as i64, ))?; - let cde_start_pos = reader.seek(io::SeekFrom::Start(pos as u64))?; + let cde_start_pos = reader.seek(io::SeekFrom::Start(pos))?; return CentralDirectoryEnd::parse(reader).map(|cde| (cde, cde_start_pos)); } pos = match pos.checked_sub(1) { diff --git a/src/types.rs b/src/types.rs index b65fad4..ad3a570 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,13 +1,16 @@ //! Types that specify what is contained in a ZIP. -#[cfg(doc)] -use {crate::read::ZipFile, crate::write::FileOptions}; - +#[cfg(feature = "time")] +use std::convert::{TryFrom, TryInto}; #[cfg(not(any( all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc" )))] use std::sync::atomic; +#[cfg(not(feature = "time"))] +use std::time::SystemTime; +#[cfg(doc)] +use {crate::read::ZipFile, crate::write::FileOptions}; #[cfg(any( all(target_arch = "arm", target_pointer_width = "32"), @@ -42,9 +45,11 @@ mod atomic { } #[cfg(feature = "time")] +use crate::result::DateTimeRangeError; +#[cfg(feature = "time")] use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum System { Dos = 0, Unix = 3, @@ -115,7 +120,7 @@ impl DateTime { let years = (datepart & 0b1111111000000000) >> 9; DateTime { - year: (years + 1980) as u16, + year: years + 1980, month: months as u8, day: days as u8, hour: hours as u8, @@ -143,10 +148,8 @@ impl DateTime { second: u8, ) -> Result<DateTime, ()> { if (1980..=2107).contains(&year) - && month >= 1 - && month <= 12 - && day >= 1 - && day <= 31 + && (1..=12).contains(&month) + && (1..=31).contains(&day) && hour <= 23 && minute <= 59 && second <= 60 @@ -169,19 +172,9 @@ impl DateTime { /// /// Returns `Err` when this object is out of bounds #[allow(clippy::result_unit_err)] + #[deprecated(note = "use `DateTime::try_from()`")] pub fn from_time(dt: OffsetDateTime) -> Result<DateTime, ()> { - if dt.year() >= 1980 && dt.year() <= 2107 { - Ok(DateTime { - 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(()) - } + dt.try_into().map_err(|_err| ()) } /// Gets the time portion of this datetime in the msdos representation @@ -197,8 +190,6 @@ impl DateTime { #[cfg(feature = "time")] /// 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)?; @@ -256,6 +247,26 @@ impl DateTime { } } +#[cfg(feature = "time")] +impl TryFrom<OffsetDateTime> for DateTime { + type Error = DateTimeRangeError; + + fn try_from(dt: OffsetDateTime) -> Result<Self, Self::Error> { + if dt.year() >= 1980 && dt.year() <= 2107 { + Ok(DateTime { + year: (dt.year()) as u16, + month: (dt.month()) as u8, + day: dt.day(), + hour: dt.hour(), + minute: dt.minute(), + second: dt.second(), + }) + } else { + Err(DateTimeRangeError) + } + } +} + pub const DEFAULT_VERSION: u8 = 46; /// A type like `AtomicU64` except it implements `Clone` and has predefined @@ -500,20 +511,43 @@ mod test { #[cfg(feature = "time")] #[test] fn datetime_from_time_bounds() { + use std::convert::TryFrom; + + use super::DateTime; + use time::macros::datetime; + + // 1979-12-31 23:59:59 + assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59 UTC)).is_err()); + + // 1980-01-01 00:00:00 + assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00 UTC)).is_ok()); + + // 2107-12-31 23:59:59 + assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59 UTC)).is_ok()); + + // 2108-01-01 00:00:00 + assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00 UTC)).is_err()); + } + + #[cfg(feature = "time")] + #[test] + fn datetime_try_from_bounds() { + use std::convert::TryFrom; + use super::DateTime; use time::macros::datetime; // 1979-12-31 23:59:59 - assert!(DateTime::from_time(datetime!(1979-12-31 23:59:59 UTC)).is_err()); + assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59 UTC)).is_err()); // 1980-01-01 00:00:00 - assert!(DateTime::from_time(datetime!(1980-01-01 00:00:00 UTC)).is_ok()); + assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00 UTC)).is_ok()); // 2107-12-31 23:59:59 - assert!(DateTime::from_time(datetime!(2107-12-31 23:59:59 UTC)).is_ok()); + assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59 UTC)).is_ok()); // 2108-01-01 00:00:00 - assert!(DateTime::from_time(datetime!(2108-01-01 00:00:00 UTC)).is_err()); + assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00 UTC)).is_err()); } #[test] @@ -564,10 +598,11 @@ mod test { #[test] fn time_at_january() { use super::DateTime; + use std::convert::TryFrom; // 2020-01-01 00:00:00 let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap(); - assert!(DateTime::from_time(clock).is_ok()); + assert!(DateTime::try_from(clock).is_ok()); } } diff --git a/src/write.rs b/src/write.rs index 551b4e3..14252b4 100644 --- a/src/write.rs +++ b/src/write.rs @@ -7,6 +7,7 @@ use crate::spec; use crate::types::{AtomicU64, DateTime, System, ZipFileData, DEFAULT_VERSION}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use crc32fast::Hasher; +use std::convert::TryInto; use std::default::Default; use std::io; use std::io::prelude::*; @@ -110,31 +111,6 @@ pub struct FileOptions { } impl FileOptions { - /// Construct a new FileOptions object - pub fn default() -> FileOptions { - FileOptions { - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - compression_method: CompressionMethod::Deflated, - #[cfg(not(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - )))] - compression_method: CompressionMethod::Stored, - compression_level: None, - #[cfg(feature = "time")] - last_modified_time: DateTime::from_time(OffsetDateTime::now_utc()).unwrap_or_default(), - #[cfg(not(feature = "time"))] - last_modified_time: DateTime::default(), - permissions: None, - large_file: false, - } - } - /// Set the compression method for the new file /// /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is @@ -174,7 +150,11 @@ 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 + /// and `0o755`, which represents `rwxr-xr-x` for directories. + /// + /// This method only preserves the file permissions bits (via a `& 0o777`) and discards + /// higher file mode bits. So it cannot be used to denote an entry as a directory, + /// symlink, or other special file type. #[must_use] pub fn unix_permissions(mut self, mode: u32) -> FileOptions { self.permissions = Some(mode & 0o777); @@ -194,8 +174,29 @@ impl FileOptions { } impl Default for FileOptions { + /// Construct a new FileOptions object fn default() -> Self { - Self::default() + Self { + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + compression_method: CompressionMethod::Deflated, + #[cfg(not(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + )))] + compression_method: CompressionMethod::Stored, + compression_level: None, + #[cfg(feature = "time")] + last_modified_time: OffsetDateTime::now_utc().try_into().unwrap_or_default(), + #[cfg(not(feature = "time"))] + last_modified_time: DateTime::default(), + permissions: None, + large_file: false, + } } } @@ -348,7 +349,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { { let writer = self.inner.get_plain(); - let header_start = writer.seek(io::SeekFrom::Current(0))?; + let header_start = writer.stream_position()?; let permissions = options.permissions.unwrap_or(0o100644); let mut file = ZipFileData { @@ -375,7 +376,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { }; write_local_file_header(writer, &file)?; - let header_end = writer.seek(io::SeekFrom::Current(0))?; + let header_end = writer.stream_position()?; self.stats.start = header_end; *file.data_start.get_mut() = header_end; @@ -404,7 +405,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { file.crc32 = self.stats.hasher.clone().finalize(); file.uncompressed_size = self.stats.bytes_written; - let file_end = writer.seek(io::SeekFrom::Current(0))?; + let file_end = writer.stream_position()?; file.compressed_size = file_end - self.stats.start; update_local_file_header(writer, file)?; @@ -723,7 +724,7 @@ impl<W: Write + io::Seek> ZipWriter<W> { /// Add a directory entry, taking a Path as argument. /// - /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal' + /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal' /// Components, such as a starting '/' or '..' and '.'. #[deprecated( since = "0.5.7", @@ -747,17 +748,55 @@ impl<W: Write + io::Seek> ZipWriter<W> { Ok(inner.unwrap()) } + /// Add a symlink entry. + /// + /// The zip archive will contain an entry for path `name` which is a symlink to `target`. + /// + /// No validation or normalization of the paths is performed. For best results, + /// callers should normalize `\` to `/` and ensure symlinks are relative to other + /// paths within the zip archive. + /// + /// WARNING: not all zip implementations preserve symlinks on extract. Some zip + /// implementations may materialize a symlink as a regular file, possibly with the + /// content incorrectly set to the symlink target. For maximum portability, consider + /// storing a regular file instead. + pub fn add_symlink<N, T>( + &mut self, + name: N, + target: T, + mut options: FileOptions, + ) -> ZipResult<()> + where + N: Into<String>, + T: Into<String>, + { + if options.permissions.is_none() { + options.permissions = Some(0o777); + } + *options.permissions.as_mut().unwrap() |= 0o120000; + // The symlink target is stored as file content. And compressing the target path + // likely wastes space. So always store. + options.compression_method = CompressionMethod::Stored; + + self.start_entry(name, options, None)?; + self.writing_to_file = true; + self.write_all(target.into().as_bytes())?; + self.writing_to_file = false; + + Ok(()) + } + fn finalize(&mut self) -> ZipResult<()> { self.finish_file()?; { let writer = self.inner.get_plain(); - let central_start = writer.seek(io::SeekFrom::Current(0))?; + let central_start = writer.stream_position()?; for file in self.files.iter() { write_central_directory_header(writer, file)?; } - let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start; + let central_size = writer.stream_position()? - central_start; if self.files.len() > spec::ZIP64_ENTRY_THR || central_size.max(central_start) > spec::ZIP64_BYTES_THR @@ -806,7 +845,7 @@ 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!(io::stderr(), "ZipWriter drop failed: {:?}", e); + let _ = write!(io::stderr(), "ZipWriter drop failed: {e:?}"); } } } @@ -1169,8 +1208,7 @@ fn validate_extra_data(file: &ZipFileData) -> ZipResult<()> { return Err(ZipError::Io(io::Error::new( io::ErrorKind::Other, format!( - "Extra data header ID {:#06} requires crate feature \"unreserved\"", - kind, + "Extra data header ID {kind:#06} requires crate feature \"unreserved\"", ), ))); } @@ -1259,7 +1297,7 @@ fn path_to_string(path: &std::path::Path) -> String { if !path_str.is_empty() { path_str.push('/'); } - path_str.push_str(&*os_str.to_string_lossy()); + path_str.push_str(&os_str.to_string_lossy()); } } path_str @@ -1286,6 +1324,13 @@ mod test { } #[test] + fn unix_permissions_bitmask() { + // unix_permissions() throws away upper bits. + let options = FileOptions::default().unix_permissions(0o120777); + assert_eq!(options.permissions, Some(0o777)); + } + + #[test] fn write_zip_dir() { let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); writer @@ -1314,6 +1359,67 @@ mod test { } #[test] + fn write_symlink_simple() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .add_symlink( + "name", + "target", + FileOptions::default().last_modified_time( + DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(), + ), + ) + .unwrap(); + assert!(writer + .write(b"writing to a symlink is not allowed and will not write any data") + .is_err()); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 112); + assert_eq!( + *result.get_ref(), + &[ + 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, + 6, 0, 0, 0, 4, 0, 0, 0, 110, 97, 109, 101, 116, 97, 114, 103, 101, 116, 80, 75, 1, + 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 252, 47, 111, 70, 6, 0, 0, 0, 6, 0, + 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 161, 0, 0, 0, 0, 110, 97, 109, 101, + 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 50, 0, 0, 0, 40, 0, 0, 0, 0, 0 + ] as &[u8], + ); + } + + #[test] + fn write_symlink_wonky_paths() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .add_symlink( + "directory\\link", + "/absolute/symlink\\with\\mixed/slashes", + FileOptions::default().last_modified_time( + DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(), + ), + ) + .unwrap(); + assert!(writer + .write(b"writing to a symlink is not allowed and will not write any data") + .is_err()); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 162); + assert_eq!( + *result.get_ref(), + &[ + 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, 41, 81, 245, 36, 0, 0, 0, + 36, 0, 0, 0, 14, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, + 110, 107, 47, 97, 98, 115, 111, 108, 117, 116, 101, 47, 115, 121, 109, 108, 105, + 110, 107, 92, 119, 105, 116, 104, 92, 109, 105, 120, 101, 100, 47, 115, 108, 97, + 115, 104, 101, 115, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 95, + 41, 81, 245, 36, 0, 0, 0, 36, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, + 161, 0, 0, 0, 0, 100, 105, 114, 101, 99, 116, 111, 114, 121, 92, 108, 105, 110, + 107, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 60, 0, 0, 0, 80, 0, 0, 0, 0, 0 + ] as &[u8], + ); + } + + #[test] fn write_mimetype_zip() { let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); let options = FileOptions { diff --git a/tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip b/tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip Binary files differnew file mode 100644 index 0000000..a428ca7 --- /dev/null +++ b/tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip diff --git a/tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip b/tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip Binary files differnew file mode 100644 index 0000000..2cc9007 --- /dev/null +++ b/tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs index 25d0c54..09e7ce4 100644 --- a/tests/end_to_end.rs +++ b/tests/end_to_end.rs @@ -13,7 +13,7 @@ fn end_to_end() { for &method in SUPPORTED_COMPRESSION_METHODS { let file = &mut Cursor::new(Vec::new()); - println!("Writing file with {} compression", method); + println!("Writing file with {method} compression"); write_test_archive(file, method).expect("Couldn't write test zip archive"); println!("Checking file contents"); diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index 3d10a31..468ef19 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -205,7 +205,7 @@ fn zip64_large() { match file.read_exact(&mut buf) { Ok(()) => println!("The first {} bytes are: {:?}", buf.len(), buf), - Err(e) => println!("Could not read the file: {:?}", e), + Err(e) => println!("Could not read the file: {e:?}"), }; } } |