aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-02-02 23:56:39 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-02-02 23:56:39 +0000
commitcdc53da3936ca3fae571af62aea30ff89beacc9c (patch)
treef22a08f84fabca451e486e8a3460064a9f75ec8c
parent8a982376b29a8c3b72b27477fa6d65bd9de1ee37 (diff)
parent5baa525f72e2eace28d60e8c7d9617e65cd503ef (diff)
downloadzip-simpleperf-release.tar.gz
Snap for 11400057 from 5baa525f72e2eace28d60e8c7d9617e65cd503ef to simpleperf-releasesimpleperf-release
Change-Id: If6598e8d2d1e374ac263573d40517db7dad39887
-rw-r--r--.cargo_vcs_info.json2
-rw-r--r--Android.bp4
-rw-r--r--CHANGELOG.md10
-rw-r--r--Cargo.toml7
-rw-r--r--Cargo.toml.orig7
-rw-r--r--METADATA21
-rw-r--r--README.md7
-rw-r--r--src/aes_ctr.rs11
-rw-r--r--src/lib.rs11
-rw-r--r--src/read.rs64
-rw-r--r--src/read/stream.rs372
-rw-r--r--src/types.rs72
-rw-r--r--src/unstable.rs20
-rw-r--r--src/write.rs67
-rw-r--r--src/zipcrypto.rs46
-rw-r--r--tests/issue_234.rs2
-rw-r--r--tests/zip_crypto.rs17
17 files changed, 616 insertions, 124 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
index 535489c..cb0d9ad 100644
--- a/.cargo_vcs_info.json
+++ b/.cargo_vcs_info.json
@@ -1,6 +1,6 @@
{
"git": {
- "sha1": "f7dcc666b75256e766295589a5ac5dc5a9617c39"
+ "sha1": "21a20584bc9e05dfa4f3c5b0bc420a1389fae2c3"
},
"path_in_vcs": ""
} \ No newline at end of file
diff --git a/Android.bp b/Android.bp
index 7c4953a..54b6fde 100644
--- a/Android.bp
+++ b/Android.bp
@@ -23,9 +23,9 @@ rust_library {
host_supported: true,
crate_name: "zip",
cargo_env_compat: true,
- cargo_pkg_version: "0.6.4",
+ cargo_pkg_version: "0.6.6",
srcs: ["src/lib.rs"],
- edition: "2018",
+ edition: "2021",
features: [
"deflate-zlib",
"flate2",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd79e39..96c6994 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## [0.6.6]
+### Changed
+
+- Updated `aes` dependency to `0.8.2` (https://github.com/zip-rs/zip/pull/354)
+
+## [0.6.5]
+### Changed
+
+- Added experimental [`zip::unstable::write::FileOptions::with_deprecated_encryption`] API to enable encrypting files with PKWARE encryption.
+
## [0.6.4]
### Changed
diff --git a/Cargo.toml b/Cargo.toml
index 7a1b656..da0180e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,9 +10,10 @@
# See Cargo.toml.orig for the original contents.
[package]
-edition = "2018"
+edition = "2021"
+rust-version = "1.59.0"
name = "zip"
-version = "0.6.4"
+version = "0.6.6"
authors = [
"Mathijs van de Nes <git@mathijs.vd-nes.nl>",
"Marli Frost <marli@frost.red>",
@@ -38,7 +39,7 @@ name = "read_metadata"
harness = false
[dependencies.aes]
-version = "0.7.5"
+version = "0.8.2"
optional = true
[dependencies.byteorder]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
index caf6a07..510df9c 100644
--- a/Cargo.toml.orig
+++ b/Cargo.toml.orig
@@ -1,6 +1,6 @@
[package]
name = "zip"
-version = "0.6.4"
+version = "0.6.6"
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"
@@ -8,10 +8,11 @@ keywords = ["zip", "archive"]
description = """
Library to support the reading and writing of zip files.
"""
-edition = "2018"
+edition = "2021"
+rust-version = "1.59.0"
[dependencies]
-aes = { version = "0.7.5", optional = true }
+aes = { version = "0.8.2", optional = true }
byteorder = "1.4.3"
bzip2 = { version = "0.4.3", optional = true }
constant_time_eq = { version = "0.1.5", optional = true }
diff --git a/METADATA b/METADATA
index a547795..7248554 100644
--- a/METADATA
+++ b/METADATA
@@ -1,23 +1,20 @@
# 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
+# For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md
name: "zip"
description: "Library to support the reading and writing of zip files."
third_party {
- url {
- type: HOMEPAGE
- value: "https://crates.io/crates/zip"
- }
- url {
- type: ARCHIVE
- value: "https://static.crates.io/crates/zip/zip-0.6.4.crate"
- }
- version: "0.6.4"
license_type: NOTICE
last_upgrade_date {
year: 2023
- month: 2
- day: 17
+ month: 12
+ day: 4
+ }
+ homepage: "https://crates.io/crates/zip"
+ identifier {
+ type: "Archive"
+ value: "https://static.crates.io/crates/zip/zip-0.6.6.crate"
+ version: "0.6.6"
}
}
diff --git a/README.md b/README.md
index 3754a7c..f06cdbb 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,6 @@ zip-rs
[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.
-
Info
----
@@ -35,14 +32,14 @@ With all default features:
```toml
[dependencies]
-zip = "0.6.4"
+zip = "0.6"
```
Without the default features:
```toml
[dependencies]
-zip = { version = "0.6.4", default-features = false }
+zip = { version = "0.6.6", default-features = false }
```
The features available are:
diff --git a/src/aes_ctr.rs b/src/aes_ctr.rs
index 0f34335..211727c 100644
--- a/src/aes_ctr.rs
+++ b/src/aes_ctr.rs
@@ -2,10 +2,11 @@
//!
//! 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.
+//! See [AesCtrZipKeyStream] for more information.
use aes::cipher::generic_array::GenericArray;
-use aes::{BlockEncrypt, NewBlockCipher};
+// use aes::{BlockEncrypt, NewBlockCipher};
+use aes::cipher::{BlockEncrypt, KeyInit};
use byteorder::WriteBytesExt;
use std::{any, fmt};
@@ -82,7 +83,7 @@ where
impl<C> AesCtrZipKeyStream<C>
where
C: AesKind,
- C::Cipher: NewBlockCipher,
+ C::Cipher: KeyInit,
{
/// Creates a new zip variant AES-CTR key stream.
///
@@ -150,14 +151,14 @@ fn xor(dest: &mut [u8], src: &[u8]) {
#[cfg(test)]
mod tests {
use super::{Aes128, Aes192, Aes256, AesCipher, AesCtrZipKeyStream, AesKind};
- use aes::{BlockEncrypt, NewBlockCipher};
+ use aes::cipher::{BlockEncrypt, KeyInit};
/// 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,
+ Aes::Cipher: KeyInit + BlockEncrypt,
{
let mut key_stream = AesCtrZipKeyStream::<Aes>::new(key);
diff --git a/src/lib.rs b/src/lib.rs
index 0fee99c..e2228e5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,3 +42,14 @@ mod spec;
mod types;
pub mod write;
mod zipcrypto;
+
+/// Unstable APIs
+///
+/// All APIs accessible by importing this module are unstable; They may be changed in patch releases.
+/// You MUST you an exact version specifier in `Cargo.toml`, to indicate the version of this API you're using:
+///
+/// ```toml
+/// [dependencies]
+/// zip = "=0.6.6"
+/// ```
+pub mod unstable;
diff --git a/src/read.rs b/src/read.rs
index dad20c2..b702b4f 100644
--- a/src/read.rs
+++ b/src/read.rs
@@ -13,7 +13,7 @@ use byteorder::{LittleEndian, ReadBytesExt};
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::{self, prelude::*};
-use std::path::{Component, Path};
+use std::path::Path;
use std::sync::Arc;
#[cfg(any(
@@ -29,10 +29,8 @@ 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;
-}
+/// Provides high level API for reading from a stream.
+pub(crate) mod stream;
// Put the struct declaration in a private module to convince rustdoc to display ZipArchive nicely
pub(crate) mod zip_archive {
@@ -650,12 +648,22 @@ pub(crate) fn central_header_to_zip_file<R: Read + io::Seek>(
archive_offset: u64,
) -> ZipResult<ZipFileData> {
let central_header_start = reader.stream_position()?;
+
// Parse central header
let signature = reader.read_u32::<LittleEndian>()?;
if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE {
- return Err(ZipError::InvalidArchive("Invalid Central Directory header"));
+ Err(ZipError::InvalidArchive("Invalid Central Directory header"))
+ } else {
+ central_header_to_zip_file_inner(reader, archive_offset, central_header_start)
}
+}
+/// Parse a central directory entry to collect the information for the file.
+fn central_header_to_zip_file_inner<R: Read>(
+ reader: &mut R,
+ archive_offset: u64,
+ central_header_start: u64,
+) -> ZipResult<ZipFileData> {
let version_made_by = reader.read_u16::<LittleEndian>()?;
let _version_to_extract = reader.read_u16::<LittleEndian>()?;
let flags = reader.read_u16::<LittleEndian>()?;
@@ -896,20 +904,7 @@ impl<'a> ZipFile<'a> {
/// to path-based exploits. It is recommended over
/// [`ZipFile::mangled_name`].
pub fn enclosed_name(&self) -> Option<&Path> {
- if self.data.file_name.contains('\0') {
- return None;
- }
- let path = Path::new(&self.data.file_name);
- let mut depth = 0usize;
- for component in path.components() {
- match component {
- Component::Prefix(_) | Component::RootDir => return None,
- Component::ParentDir => depth = depth.checked_sub(1)?,
- Component::Normal(_) => depth += 1,
- Component::CurDir => (),
- }
- }
- Some(path)
+ self.data.enclosed_name()
}
/// Get the comment of the file
@@ -952,27 +947,7 @@ impl<'a> ZipFile<'a> {
/// Get unix mode for the file
pub fn unix_mode(&self) -> Option<u32> {
- if self.data.external_attributes == 0 {
- return None;
- }
-
- match self.data.system {
- System::Unix => Some(self.data.external_attributes >> 16),
- System::Dos => {
- // Interpret MS-DOS directory bit
- let mut mode = if 0x10 == (self.data.external_attributes & 0x10) {
- ffi::S_IFDIR | 0o0775
- } else {
- ffi::S_IFREG | 0o0664
- };
- if 0x01 == (self.data.external_attributes & 0x01) {
- // Read-only bit; strip write permissions
- mode &= 0o0555;
- }
- Some(mode)
- }
- _ => None,
- }
+ self.data.unix_mode()
}
/// Get the CRC32 hash of the original file
@@ -1029,10 +1004,9 @@ impl<'a> Drop for ZipFile<'a> {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(_) => (),
- Err(e) => panic!(
- "Could not consume all of the output of the current ZipFile: {:?}",
- e
- ),
+ Err(e) => {
+ panic!("Could not consume all of the output of the current ZipFile: {e:?}")
+ }
}
}
}
diff --git a/src/read/stream.rs b/src/read/stream.rs
new file mode 100644
index 0000000..5a01b23
--- /dev/null
+++ b/src/read/stream.rs
@@ -0,0 +1,372 @@
+use std::fs;
+use std::io::{self, Read};
+use std::path::Path;
+
+use super::{
+ central_header_to_zip_file_inner, read_zipfile_from_stream, spec, ZipError, ZipFile,
+ ZipFileData, ZipResult,
+};
+
+use byteorder::{LittleEndian, ReadBytesExt};
+
+/// Stream decoder for zip.
+#[derive(Debug)]
+pub struct ZipStreamReader<R>(R);
+
+impl<R> ZipStreamReader<R> {
+ /// Create a new ZipStreamReader
+ pub fn new(reader: R) -> Self {
+ Self(reader)
+ }
+}
+
+impl<R: Read> ZipStreamReader<R> {
+ fn parse_central_directory(&mut self) -> ZipResult<Option<ZipStreamFileMetadata>> {
+ // Give archive_offset and central_header_start dummy value 0, since
+ // they are not used in the output.
+ let archive_offset = 0;
+ let central_header_start = 0;
+
+ // Parse central header
+ let signature = self.0.read_u32::<LittleEndian>()?;
+ if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE {
+ Ok(None)
+ } else {
+ central_header_to_zip_file_inner(&mut self.0, archive_offset, central_header_start)
+ .map(ZipStreamFileMetadata)
+ .map(Some)
+ }
+ }
+
+ /// Iteraate over the stream and extract all file and their
+ /// metadata.
+ pub fn visit<V: ZipStreamVisitor>(mut self, visitor: &mut V) -> ZipResult<()> {
+ while let Some(mut file) = read_zipfile_from_stream(&mut self.0)? {
+ visitor.visit_file(&mut file)?;
+ }
+
+ while let Some(metadata) = self.parse_central_directory()? {
+ visitor.visit_additional_metadata(&metadata)?;
+ }
+
+ Ok(())
+ }
+
+ /// Extract a Zip archive into a directory, overwriting files if they
+ /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`].
+ ///
+ /// Extraction is not atomic; If an error is encountered, some of the files
+ /// may be left on disk.
+ pub fn extract<P: AsRef<Path>>(self, directory: P) -> ZipResult<()> {
+ struct Extractor<'a>(&'a Path);
+ impl ZipStreamVisitor for Extractor<'_> {
+ fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
+ let filepath = file
+ .enclosed_name()
+ .ok_or(ZipError::InvalidArchive("Invalid file path"))?;
+
+ let outpath = self.0.join(filepath);
+
+ if file.name().ends_with('/') {
+ fs::create_dir_all(&outpath)?;
+ } else {
+ if let Some(p) = outpath.parent() {
+ fs::create_dir_all(p)?;
+ }
+ let mut outfile = fs::File::create(&outpath)?;
+ io::copy(file, &mut outfile)?;
+ }
+
+ Ok(())
+ }
+
+ #[allow(unused)]
+ fn visit_additional_metadata(
+ &mut self,
+ metadata: &ZipStreamFileMetadata,
+ ) -> ZipResult<()> {
+ #[cfg(unix)]
+ {
+ let filepath = metadata
+ .enclosed_name()
+ .ok_or(ZipError::InvalidArchive("Invalid file path"))?;
+
+ let outpath = self.0.join(filepath);
+
+ use std::os::unix::fs::PermissionsExt;
+ if let Some(mode) = metadata.unix_mode() {
+ fs::set_permissions(outpath, fs::Permissions::from_mode(mode))?;
+ }
+ }
+
+ Ok(())
+ }
+ }
+
+ self.visit(&mut Extractor(directory.as_ref()))
+ }
+}
+
+/// Visitor for ZipStreamReader
+pub trait ZipStreamVisitor {
+ /// * `file` - contains the content of the file and most of the metadata,
+ /// except:
+ /// - `comment`: set to an empty string
+ /// - `data_start`: set to 0
+ /// - `external_attributes`: `unix_mode()`: will return None
+ fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()>;
+
+ /// This function is guranteed to be called after all `visit_file`s.
+ ///
+ /// * `metadata` - Provides missing metadata in `visit_file`.
+ fn visit_additional_metadata(&mut self, metadata: &ZipStreamFileMetadata) -> ZipResult<()>;
+}
+
+/// Additional metadata for the file.
+#[derive(Debug)]
+pub struct ZipStreamFileMetadata(ZipFileData);
+
+impl ZipStreamFileMetadata {
+ /// Get the name of the file
+ ///
+ /// # Warnings
+ ///
+ /// It is dangerous to use this name directly when extracting an archive.
+ /// It may contain an absolute path (`/etc/shadow`), or break out of the
+ /// current directory (`../runtime`). Carelessly writing to these paths
+ /// allows an attacker to craft a ZIP archive that will overwrite critical
+ /// files.
+ ///
+ /// You can use the [`ZipFile::enclosed_name`] method to validate the name
+ /// as a safe path.
+ pub fn name(&self) -> &str {
+ &self.0.file_name
+ }
+
+ /// Get the name of the file, in the raw (internal) byte representation.
+ ///
+ /// The encoding of this data is currently undefined.
+ pub fn name_raw(&self) -> &[u8] {
+ &self.0.file_name_raw
+ }
+
+ /// Rewrite the path, ignoring any path components with special meaning.
+ ///
+ /// - Absolute paths are made relative
+ /// - [`ParentDir`]s are ignored
+ /// - Truncates the filename at a NULL byte
+ ///
+ /// This is appropriate if you need to be able to extract *something* from
+ /// any archive, but will easily misrepresent trivial paths like
+ /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this,
+ /// [`ZipFile::enclosed_name`] is the better option in most scenarios.
+ ///
+ /// [`ParentDir`]: `Component::ParentDir`
+ pub fn mangled_name(&self) -> ::std::path::PathBuf {
+ self.0.file_name_sanitized()
+ }
+
+ /// Ensure the file path is safe to use as a [`Path`].
+ ///
+ /// - It can't contain NULL bytes
+ /// - It can't resolve to a path outside the current directory
+ /// > `foo/../bar` is fine, `foo/../../bar` is not.
+ /// - It can't be an absolute path
+ ///
+ /// This will read well-formed ZIP files correctly, and is resistant
+ /// to path-based exploits. It is recommended over
+ /// [`ZipFile::mangled_name`].
+ pub fn enclosed_name(&self) -> Option<&Path> {
+ self.0.enclosed_name()
+ }
+
+ /// Returns whether the file is actually a directory
+ pub fn is_dir(&self) -> bool {
+ self.name()
+ .chars()
+ .rev()
+ .next()
+ .map_or(false, |c| c == '/' || c == '\\')
+ }
+
+ /// Returns whether the file is a regular file
+ pub fn is_file(&self) -> bool {
+ !self.is_dir()
+ }
+
+ /// Get the comment of the file
+ pub fn comment(&self) -> &str {
+ &self.0.file_comment
+ }
+
+ /// Get the starting offset of the data of the compressed file
+ pub fn data_start(&self) -> u64 {
+ self.0.data_start.load()
+ }
+
+ /// Get unix mode for the file
+ pub fn unix_mode(&self) -> Option<u32> {
+ self.0.unix_mode()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use std::collections::BTreeSet;
+ use std::io;
+
+ struct DummyVisitor;
+ impl ZipStreamVisitor for DummyVisitor {
+ fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> {
+ Ok(())
+ }
+
+ fn visit_additional_metadata(
+ &mut self,
+ _metadata: &ZipStreamFileMetadata,
+ ) -> ZipResult<()> {
+ Ok(())
+ }
+ }
+
+ #[derive(Default, Debug, Eq, PartialEq)]
+ struct CounterVisitor(u64, u64);
+ impl ZipStreamVisitor for CounterVisitor {
+ fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> {
+ self.0 += 1;
+ Ok(())
+ }
+
+ fn visit_additional_metadata(
+ &mut self,
+ _metadata: &ZipStreamFileMetadata,
+ ) -> ZipResult<()> {
+ self.1 += 1;
+ Ok(())
+ }
+ }
+
+ #[test]
+ fn invalid_offset() {
+ ZipStreamReader::new(io::Cursor::new(include_bytes!(
+ "../../tests/data/invalid_offset.zip"
+ )))
+ .visit(&mut DummyVisitor)
+ .unwrap_err();
+ }
+
+ #[test]
+ fn invalid_offset2() {
+ ZipStreamReader::new(io::Cursor::new(include_bytes!(
+ "../../tests/data/invalid_offset2.zip"
+ )))
+ .visit(&mut DummyVisitor)
+ .unwrap_err();
+ }
+
+ #[test]
+ fn zip_read_streaming() {
+ let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!(
+ "../../tests/data/mimetype.zip"
+ )));
+
+ #[derive(Default)]
+ struct V {
+ filenames: BTreeSet<Box<str>>,
+ }
+ impl ZipStreamVisitor for V {
+ fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
+ if file.is_file() {
+ self.filenames.insert(file.name().into());
+ }
+
+ Ok(())
+ }
+ fn visit_additional_metadata(
+ &mut self,
+ metadata: &ZipStreamFileMetadata,
+ ) -> ZipResult<()> {
+ if metadata.is_file() {
+ assert!(
+ self.filenames.contains(metadata.name()),
+ "{} is missing its file content",
+ metadata.name()
+ );
+ }
+
+ Ok(())
+ }
+ }
+
+ reader.visit(&mut V::default()).unwrap();
+ }
+
+ #[test]
+ fn file_and_dir_predicates() {
+ let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!(
+ "../../tests/data/files_and_dirs.zip"
+ )));
+
+ #[derive(Default)]
+ struct V {
+ filenames: BTreeSet<Box<str>>,
+ }
+ impl ZipStreamVisitor for V {
+ fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
+ let full_name = file.enclosed_name().unwrap();
+ let file_name = full_name.file_name().unwrap().to_str().unwrap();
+ assert!(
+ (file_name.starts_with("dir") && file.is_dir())
+ || (file_name.starts_with("file") && file.is_file())
+ );
+
+ if file.is_file() {
+ self.filenames.insert(file.name().into());
+ }
+
+ Ok(())
+ }
+ fn visit_additional_metadata(
+ &mut self,
+ metadata: &ZipStreamFileMetadata,
+ ) -> ZipResult<()> {
+ if metadata.is_file() {
+ assert!(
+ self.filenames.contains(metadata.name()),
+ "{} is missing its file content",
+ metadata.name()
+ );
+ }
+
+ Ok(())
+ }
+ }
+
+ reader.visit(&mut V::default()).unwrap();
+ }
+
+ /// test case to ensure we don't preemptively over allocate based on the
+ /// declared number of files in the CDE of an invalid zip when the number of
+ /// files declared is more than the alleged offset in the CDE
+ #[test]
+ fn invalid_cde_number_of_files_allocation_smaller_offset() {
+ ZipStreamReader::new(io::Cursor::new(include_bytes!(
+ "../../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip"
+ )))
+ .visit(&mut DummyVisitor)
+ .unwrap_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() {
+ ZipStreamReader::new(io::Cursor::new(include_bytes!(
+ "../../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip"
+ )))
+ .visit(&mut DummyVisitor)
+ .unwrap_err();
+ }
+}
diff --git a/src/types.rs b/src/types.rs
index ad3a570..c3d0a45 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,6 +1,6 @@
//! Types that specify what is contained in a ZIP.
-#[cfg(feature = "time")]
-use std::convert::{TryFrom, TryInto};
+use std::path;
+
#[cfg(not(any(
all(target_arch = "arm", target_pointer_width = "32"),
target_arch = "mips",
@@ -12,6 +12,11 @@ use std::time::SystemTime;
#[cfg(doc)]
use {crate::read::ZipFile, crate::write::FileOptions};
+mod ffi {
+ pub const S_IFDIR: u32 = 0o0040000;
+ pub const S_IFREG: u32 = 0o0100000;
+}
+
#[cfg(any(
all(target_arch = "arm", target_pointer_width = "32"),
target_arch = "mips",
@@ -375,6 +380,48 @@ impl ZipFileData {
})
}
+ pub(crate) fn enclosed_name(&self) -> Option<&path::Path> {
+ if self.file_name.contains('\0') {
+ return None;
+ }
+ let path = path::Path::new(&self.file_name);
+ let mut depth = 0usize;
+ for component in path.components() {
+ match component {
+ path::Component::Prefix(_) | path::Component::RootDir => return None,
+ path::Component::ParentDir => depth = depth.checked_sub(1)?,
+ path::Component::Normal(_) => depth += 1,
+ path::Component::CurDir => (),
+ }
+ }
+ Some(path)
+ }
+
+ /// Get unix mode for the file
+ pub(crate) fn unix_mode(&self) -> Option<u32> {
+ if self.external_attributes == 0 {
+ return None;
+ }
+
+ match self.system {
+ System::Unix => Some(self.external_attributes >> 16),
+ System::Dos => {
+ // Interpret MS-DOS directory bit
+ let mut mode = if 0x10 == (self.external_attributes & 0x10) {
+ ffi::S_IFDIR | 0o0775
+ } else {
+ ffi::S_IFREG | 0o0664
+ };
+ if 0x01 == (self.external_attributes & 0x01) {
+ // Read-only bit; strip write permissions
+ mode &= 0o0555;
+ }
+ Some(mode)
+ }
+ _ => None,
+ }
+ }
+
pub fn zip64_extension(&self) -> bool {
self.uncompressed_size > 0xFFFFFFFF
|| self.compressed_size > 0xFFFFFFFF
@@ -510,27 +557,6 @@ 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;
diff --git a/src/unstable.rs b/src/unstable.rs
new file mode 100644
index 0000000..f8b46a9
--- /dev/null
+++ b/src/unstable.rs
@@ -0,0 +1,20 @@
+/// Provides high level API for reading from a stream.
+pub mod stream {
+ pub use crate::read::stream::*;
+}
+/// Types for creating ZIP archives.
+pub mod write {
+ use crate::write::FileOptions;
+ /// Unstable methods for [`FileOptions`].
+ pub trait FileOptionsExt {
+ /// Write the file with the given password using the deprecated ZipCrypto algorithm.
+ ///
+ /// This is not recommended for new archives, as ZipCrypto is not secure.
+ fn with_deprecated_encryption(self, password: &[u8]) -> Self;
+ }
+ impl FileOptionsExt for FileOptions {
+ fn with_deprecated_encryption(self, password: &[u8]) -> Self {
+ self.with_deprecated_encryption(password)
+ }
+ }
+} \ No newline at end of file
diff --git a/src/write.rs b/src/write.rs
index 14252b4..4cdc031 100644
--- a/src/write.rs
+++ b/src/write.rs
@@ -29,19 +29,37 @@ use time::OffsetDateTime;
#[cfg(feature = "zstd")]
use zstd::stream::write::Encoder as ZstdEncoder;
+enum MaybeEncrypted<W> {
+ Unencrypted(W),
+ Encrypted(crate::zipcrypto::ZipCryptoWriter<W>),
+}
+impl<W: Write> Write for MaybeEncrypted<W> {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ match self {
+ MaybeEncrypted::Unencrypted(w) => w.write(buf),
+ MaybeEncrypted::Encrypted(w) => w.write(buf),
+ }
+ }
+ fn flush(&mut self) -> io::Result<()> {
+ match self {
+ MaybeEncrypted::Unencrypted(w) => w.flush(),
+ MaybeEncrypted::Encrypted(w) => w.flush(),
+ }
+ }
+}
enum GenericZipWriter<W: Write + io::Seek> {
Closed,
- Storer(W),
+ Storer(MaybeEncrypted<W>),
#[cfg(any(
feature = "deflate",
feature = "deflate-miniz",
feature = "deflate-zlib"
))]
- Deflater(DeflateEncoder<W>),
+ Deflater(DeflateEncoder<MaybeEncrypted<W>>),
#[cfg(feature = "bzip2")]
- Bzip2(BzEncoder<W>),
+ Bzip2(BzEncoder<MaybeEncrypted<W>>),
#[cfg(feature = "zstd")]
- Zstd(ZstdEncoder<'static, W>),
+ Zstd(ZstdEncoder<'static, MaybeEncrypted<W>>),
}
// Put the struct declaration in a private module to convince rustdoc to display ZipWriter nicely
pub(crate) mod zip_writer {
@@ -108,6 +126,7 @@ pub struct FileOptions {
last_modified_time: DateTime,
permissions: Option<u32>,
large_file: bool,
+ encrypt_with: Option<crate::zipcrypto::ZipCryptoKeys>,
}
impl FileOptions {
@@ -171,6 +190,10 @@ impl FileOptions {
self.large_file = large;
self
}
+ pub(crate) fn with_deprecated_encryption(mut self, password: &[u8]) -> FileOptions {
+ self.encrypt_with = Some(crate::zipcrypto::ZipCryptoKeys::derive(password));
+ self
+ }
}
impl Default for FileOptions {
@@ -196,6 +219,7 @@ impl Default for FileOptions {
last_modified_time: DateTime::default(),
permissions: None,
large_file: false,
+ encrypt_with: None,
}
}
}
@@ -284,7 +308,7 @@ impl<A: Read + Write + io::Seek> ZipWriter<A> {
let _ = readwriter.seek(io::SeekFrom::Start(directory_start)); // seek directory_start to overwrite it
Ok(ZipWriter {
- inner: GenericZipWriter::Storer(readwriter),
+ inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)),
files,
stats: Default::default(),
writing_to_file: false,
@@ -302,7 +326,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
/// Before writing to this object, the [`ZipWriter::start_file`] function should be called.
pub fn new(inner: W) -> ZipWriter<W> {
ZipWriter {
- inner: GenericZipWriter::Storer(inner),
+ inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(inner)),
files: Vec::new(),
stats: Default::default(),
writing_to_file: false,
@@ -355,7 +379,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
let mut file = ZipFileData {
system: System::Unix,
version_made_by: DEFAULT_VERSION,
- encrypted: false,
+ encrypted: options.encrypt_with.is_some(),
using_data_descriptor: false,
compression_method: options.compression_method,
compression_level: options.compression_level,
@@ -385,7 +409,13 @@ impl<W: Write + io::Seek> ZipWriter<W> {
self.files.push(file);
}
+ if let Some(keys) = options.encrypt_with {
+ let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { writer: core::mem::replace(&mut self.inner, GenericZipWriter::Closed).unwrap(), buffer: vec![], keys };
+ let mut crypto_header = [0u8; 12];
+ zipwriter.write_all(&crypto_header)?;
+ self.inner = GenericZipWriter::Storer(MaybeEncrypted::Encrypted(zipwriter));
+ }
Ok(())
}
@@ -395,6 +425,14 @@ impl<W: Write + io::Seek> ZipWriter<W> {
self.end_extra_data()?;
}
self.inner.switch_to(CompressionMethod::Stored, None)?;
+ match core::mem::replace(&mut self.inner, GenericZipWriter::Closed) {
+ GenericZipWriter::Storer(MaybeEncrypted::Encrypted(writer)) => {
+ let crc32 = self.stats.hasher.clone().finalize();
+ self.inner = GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(writer.finish(crc32)?))
+ }
+ GenericZipWriter::Storer(w) => self.inner = GenericZipWriter::Storer(w),
+ _ => unreachable!()
+ }
let writer = self.inner.get_plain();
if !self.writing_raw {
@@ -699,7 +737,7 @@ impl<W: Write + io::Seek> ZipWriter<W> {
/// Add a directory entry.
///
- /// You can't write data to the file afterwards.
+ /// As directories have no content, you must not call [`ZipWriter::write`] before adding a new file.
pub fn add_directory<S>(&mut self, name: S, mut options: FileOptions) -> ZipResult<()>
where
S: Into<String>,
@@ -985,8 +1023,8 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
fn get_plain(&mut self) -> &mut W {
match *self {
- GenericZipWriter::Storer(ref mut w) => w,
- _ => panic!("Should have switched to stored beforehand"),
+ GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(ref mut w)) => w,
+ _ => panic!("Should have switched to stored and unencrypted beforehand"),
}
}
@@ -1009,8 +1047,8 @@ impl<W: Write + io::Seek> GenericZipWriter<W> {
fn unwrap(self) -> W {
match self {
- GenericZipWriter::Storer(w) => w,
- _ => panic!("Should have switched to stored beforehand"),
+ GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(w)) => w,
+ _ => panic!("Should have switched to stored and unencrypted beforehand"),
}
}
}
@@ -1058,7 +1096,7 @@ fn write_local_file_header<T: Write>(writer: &mut T, file: &ZipFileData) -> ZipR
1u16 << 11
} else {
0
- };
+ } | if file.encrypted { 1u16 << 0 } else { 0 };
writer.write_u16::<LittleEndian>(flag)?;
// Compression method
#[allow(deprecated)]
@@ -1133,7 +1171,7 @@ fn write_central_directory_header<T: Write>(writer: &mut T, file: &ZipFileData)
1u16 << 11
} else {
0
- };
+ } | if file.encrypted { 1u16 << 0 } else { 0 };
writer.write_u16::<LittleEndian>(flag)?;
// compression method
#[allow(deprecated)]
@@ -1428,6 +1466,7 @@ mod test {
last_modified_time: DateTime::default(),
permissions: Some(33188),
large_file: false,
+ encrypt_with: None,
};
writer.start_file("mimetype", options).unwrap();
writer
diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs
index 91d4039..c3696e4 100644
--- a/src/zipcrypto.rs
+++ b/src/zipcrypto.rs
@@ -6,7 +6,8 @@
use std::num::Wrapping;
/// A container to hold the current key state
-struct ZipCryptoKeys {
+#[derive(Clone, Copy)]
+pub(crate) struct ZipCryptoKeys {
key_0: Wrapping<u32>,
key_1: Wrapping<u32>,
key_2: Wrapping<u32>,
@@ -49,6 +50,13 @@ impl ZipCryptoKeys {
fn crc32(crc: Wrapping<u32>, input: u8) -> Wrapping<u32> {
(crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize])
}
+ pub(crate) fn derive(password: &[u8]) -> ZipCryptoKeys {
+ let mut keys = ZipCryptoKeys::new();
+ for byte in password.iter() {
+ keys.update(*byte);
+ }
+ keys
+ }
}
/// A ZipCrypto reader with unverified password
@@ -70,17 +78,10 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
/// would be impossible to decrypt files that were encrypted with a
/// password byte sequence that is unrepresentable in UTF-8.
pub fn new(file: R, password: &[u8]) -> ZipCryptoReader<R> {
- let mut result = ZipCryptoReader {
+ ZipCryptoReader {
file,
- keys: ZipCryptoKeys::new(),
- };
-
- // Key the cipher by updating the keys with the password.
- for byte in password.iter() {
- result.keys.update(*byte);
+ keys: ZipCryptoKeys::derive(password),
}
-
- result
}
/// Read the ZipCrypto header bytes and validate the password.
@@ -122,6 +123,31 @@ impl<R: std::io::Read> ZipCryptoReader<R> {
Ok(Some(ZipCryptoReaderValid { reader: self }))
}
}
+pub(crate) struct ZipCryptoWriter<W> {
+ pub(crate) writer: W,
+ pub(crate) buffer: Vec<u8>,
+ pub(crate) keys: ZipCryptoKeys,
+}
+impl<W: std::io::Write> ZipCryptoWriter<W> {
+ pub(crate) fn finish(mut self, crc32: u32) -> std::io::Result<W> {
+ self.buffer[11] = (crc32 >> 24) as u8;
+ for byte in self.buffer.iter_mut() {
+ *byte = self.keys.encrypt_byte(*byte);
+ }
+ self.writer.write_all(&self.buffer)?;
+ self.writer.flush()?;
+ Ok(self.writer)
+ }
+}
+impl<W: std::io::Write> std::io::Write for ZipCryptoWriter<W> {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ self.buffer.extend_from_slice(buf);
+ Ok(buf.len())
+ }
+ fn flush(&mut self) -> std::io::Result<()> {
+ Ok(())
+ }
+}
/// A ZipCrypto reader with verified password
pub struct ZipCryptoReaderValid<R> {
diff --git a/tests/issue_234.rs b/tests/issue_234.rs
index bd01d1d..f8c1d2c 100644
--- a/tests/issue_234.rs
+++ b/tests/issue_234.rs
@@ -26,6 +26,6 @@ fn invalid_header() {
let archive = zip::ZipArchive::new(reader);
match archive {
Err(ZipError::InvalidArchive(_)) => {}
- value => panic!("Unexpected value: {:?}", value),
+ value => panic!("Unexpected value: {value:?}"),
}
}
diff --git a/tests/zip_crypto.rs b/tests/zip_crypto.rs
index 6c4d6b8..d831c1e 100644
--- a/tests/zip_crypto.rs
+++ b/tests/zip_crypto.rs
@@ -21,6 +21,23 @@ use std::io::Cursor;
use std::io::Read;
#[test]
+fn encrypting_file() {
+ use zip::unstable::write::FileOptionsExt;
+ use std::io::{Read, Write};
+ let mut buf = vec![0; 2048];
+ let mut archive = zip::write::ZipWriter::new(std::io::Cursor::new(&mut buf));
+ archive.start_file("name", zip::write::FileOptions::default().with_deprecated_encryption(b"password")).unwrap();
+ archive.write_all(b"test").unwrap();
+ archive.finish().unwrap();
+ drop(archive);
+ let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&mut buf)).unwrap();
+ let mut file = archive.by_index_decrypt(0, b"password").unwrap().unwrap();
+ let mut buf = Vec::new();
+ file.read_to_end(&mut buf).unwrap();
+ assert_eq!(buf, b"test");
+
+}
+#[test]
fn encrypted_file() {
let zip_file_bytes = &mut Cursor::new(vec![
0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x54, 0xbd, 0xb5, 0x50, 0x2f,