diff --git a/Cargo.toml b/Cargo.toml index b161f1155..f55bcabfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,8 @@ time = { workspace = true, optional = true, features = [ "std", ] } zeroize = { version = "1.8.1", optional = true, features = ["zeroize_derive"] } -zstd = { version = "0.13.1", optional = true, default-features = false } +# zstd = { version = "0.13.1", optional = true, default-features = false } +zstd = { git = "https://github.com/gyscos/zstd-rs", rev = "a3738d680542e34b5529207ee30491ed7b69ed71", optional = true, default-features = false } zopfli = { version = "0.8.1", optional = true } deflate64 = { version = "0.1.8", optional = true } lzma-rs = { version = "0.3.0", default-features = false, optional = true } diff --git a/src/aes.rs b/src/aes.rs index 6c9eb371f..c86a8e3a2 100644 --- a/src/aes.rs +++ b/src/aes.rs @@ -66,7 +66,7 @@ pub struct AesReader { data_length: u64, } -impl AesReader { +impl AesReader { pub const fn new(reader: R, aes_mode: AesMode, compressed_size: u64) -> AesReader { let data_length = compressed_size - (PWD_VERIFY_LENGTH + AUTH_CODE_LENGTH + aes_mode.salt_length()) as u64; @@ -77,7 +77,9 @@ impl AesReader { data_length, } } +} +impl AesReader { /// 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 @@ -150,7 +152,7 @@ impl AesReader { /// 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 { +pub struct AesReaderValid { reader: R, data_remaining: u64, cipher: Cipher, @@ -214,7 +216,7 @@ impl Read for AesReaderValid { } } -impl AesReaderValid { +impl AesReaderValid { /// Consumes this decoder, returning the underlying reader. pub fn into_inner(self) -> R { self.reader diff --git a/src/crc32.rs b/src/crc32.rs index 4b2beb62e..73ce52292 100644 --- a/src/crc32.rs +++ b/src/crc32.rs @@ -31,6 +31,7 @@ impl Crc32Reader { self.check == self.hasher.clone().finalize() } + #[allow(dead_code)] pub fn into_inner(self) -> R { self.inner } @@ -83,6 +84,110 @@ impl Read for Crc32Reader { } } +pub(crate) mod non_crypto { + use std::io; + use std::io::prelude::*; + + use crc32fast::Hasher; + + /// Reader that validates the CRC32 when it reaches the EOF. + pub struct Crc32Reader { + inner: R, + hasher: Hasher, + check: u32, + } + + impl Crc32Reader { + /// Get a new Crc32Reader which checks the inner reader against checksum. + pub(crate) fn new(inner: R, checksum: u32) -> Self { + Crc32Reader { + inner, + hasher: Hasher::new(), + check: checksum, + } + } + + fn check_matches(&self) -> Result<(), &'static str> { + let res = self.hasher.clone().finalize(); + if self.check == res { + Ok(()) + } else { + /* TODO: make this into our own Crc32Error error type! */ + Err("Invalid checksum") + } + } + } + + impl Read for Crc32Reader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + /* We want to make sure we only check the hash when the input stream is exhausted. */ + if buf.is_empty() { + /* If the input buf is empty (this shouldn't happen, but isn't guaranteed), we + * still want to "pull" from the source in case it surfaces an i/o error. This will + * always return a count of Ok(0) if successful. */ + return self.inner.read(buf); + } + + let count = self.inner.read(buf)?; + if count == 0 { + return self + .check_matches() + .map(|()| 0) + /* TODO: use io::Error::other for MSRV >=1.74 */ + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)); + } + self.hasher.update(&buf[..count]); + Ok(count) + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn test_empty_reader() { + let data: &[u8] = b""; + let mut buf = [0; 1]; + + let mut reader = Crc32Reader::new(data, 0); + assert_eq!(reader.read(&mut buf).unwrap(), 0); + + let mut reader = Crc32Reader::new(data, 1); + assert!(reader + .read(&mut buf) + .unwrap_err() + .to_string() + .contains("Invalid checksum")); + } + + #[test] + fn test_byte_by_byte() { + let data: &[u8] = b"1234"; + let mut buf = [0; 1]; + + let mut reader = Crc32Reader::new(data, 0x9be3e0a3); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 0); + // Can keep reading 0 bytes after the end + assert_eq!(reader.read(&mut buf).unwrap(), 0); + } + + #[test] + fn test_zero_read() { + let data: &[u8] = b"1234"; + let mut buf = [0; 5]; + + let mut reader = Crc32Reader::new(data, 0x9be3e0a3); + assert_eq!(reader.read(&mut buf[..0]).unwrap(), 0); + assert_eq!(reader.read(&mut buf).unwrap(), 4); + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/read.rs b/src/read.rs index 830a58514..9fb3555a0 100644 --- a/src/read.rs +++ b/src/read.rs @@ -10,7 +10,7 @@ use crate::read::zip_archive::{Shared, SharedBuilder}; use crate::result::{ZipError, ZipResult}; use crate::spec::{self, FixedSizeBlock, Zip32CentralDirectoryEnd, ZIP64_ENTRY_THR}; use crate::types::{ - AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData, + AesMode, AesModeInfo, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData, ZipLocalEntryBlock, }; use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator}; @@ -21,7 +21,6 @@ use std::fs::create_dir_all; use std::io::{self, copy, prelude::*, sink, SeekFrom}; use std::mem; use std::mem::size_of; -use std::ops::Deref; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::{Arc, OnceLock}; @@ -100,6 +99,7 @@ pub(crate) mod zip_archive { /// /// ```no_run /// use std::io::prelude::*; + /// use zip::read::ArchiveEntry; /// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> { /// let mut zip = zip::ZipArchive::new(reader)?; /// @@ -128,9 +128,15 @@ use crate::read::lzma::LzmaDecoder; #[cfg(feature = "xz")] use crate::read::xz::XzDecoder; use crate::result::ZipError::{InvalidArchive, InvalidPassword, UnsupportedArchive}; -use crate::spec::is_dir; use crate::types::ffi::S_IFLNK; use crate::unstable::{path_to_string, LittleEndianReadExt}; + +use crate::crc32::non_crypto::Crc32Reader as NewCrc32Reader; +use crate::unstable::read::{ + construct_decompressing_reader, find_entry_content_range, CryptoEntryReader, CryptoVariant, +}; +pub use crate::unstable::read::{ArchiveEntry, ZipEntry}; + pub use zip_archive::ZipArchive; #[allow(clippy::large_enum_variant)] @@ -351,9 +357,9 @@ pub(crate) fn find_content<'a>( Ok((reader as &mut dyn Read).take(data.compressed_size)) } -fn find_data_start( +pub(crate) fn find_data_start( data: &ZipFileData, - reader: &mut (impl Read + Seek + Sized), + reader: &mut (impl Read + Seek), ) -> Result { // Go to start of data. reader.seek(io::SeekFrom::Start(data.header_start))?; @@ -388,7 +394,7 @@ pub(crate) fn make_crypto_reader<'a>( using_data_descriptor: bool, reader: io::Take<&'a mut dyn Read>, password: Option<&[u8]>, - aes_info: Option<(AesMode, AesVendorVersion, CompressionMethod)>, + aes_info: Option, #[cfg(feature = "aes-crypto")] compressed_size: u64, ) -> ZipResult> { #[allow(deprecated)] @@ -406,7 +412,14 @@ pub(crate) fn make_crypto_reader<'a>( )) } #[cfg(feature = "aes-crypto")] - (Some(password), Some((aes_mode, vendor_version, _))) => CryptoReader::Aes { + ( + Some(password), + Some(AesModeInfo { + aes_mode, + vendor_version, + .. + }), + ) => CryptoReader::Aes { reader: AesReader::new(reader, aes_mode, compressed_size).validate(password)?, vendor_version, }, @@ -547,6 +560,75 @@ impl ZipArchive { } Some(total) } + + const fn zip64_cde_len() -> usize { + mem::size_of::() + + mem::size_of::() + } + + const fn order_lower_upper_bounds(a: u64, b: u64) -> (u64, u64) { + if a > b { + (b, a) + } else { + (a, b) + } + } + + /// Number of files contained in this zip. + pub fn len(&self) -> usize { + self.shared.files.len() + } + + /// Whether this zip archive contains no files + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get the offset from the beginning of the underlying reader that this zip begins at, in bytes. + /// + /// 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.shared.offset + } + + /// Get the comment of the zip archive. + pub fn comment(&self) -> &[u8] { + &self.comment + } + + /// Returns an iterator over all the file and directory names in this archive. + pub fn file_names(&self) -> impl Iterator { + self.shared.files.keys().map(|s| s.as_ref()) + } + + /// Get the index of a file entry by name, if it's present. + #[inline(always)] + pub fn index_for_name(&self, name: &str) -> Option { + self.shared.files.get_index_of(name) + } + + /// Get the index of a file entry by path, if it's present. + #[inline(always)] + pub fn index_for_path>(&self, path: T) -> Option { + self.index_for_name(&path_to_string(path)) + } + + #[inline(always)] + fn index_for_name_err(&self, name: impl AsRef) -> Result { + self.index_for_name(name.as_ref()) + .map(Ok) + .unwrap_or(Err(ZipError::FileNotFound)) + } + + /// Get the name of a file entry, if it's present. + #[inline(always)] + pub fn name_for_index(&self, index: usize) -> Option<&str> { + self.shared + .files + .get_index(index) + .map(|(name, _)| name.as_ref()) + } } impl ZipArchive { @@ -600,10 +682,8 @@ impl ZipArchive { self.reader.rewind()?; /* Find the end of the file data. */ let length_to_read = self.shared.dir_start; - /* Produce a Read that reads bytes up until the start of the central directory header. - * This "as &mut dyn Read" trick is used elsewhere to avoid having to clone the underlying - * handle, which it really shouldn't need to anyway. */ - let mut limited_raw = (&mut self.reader as &mut dyn Read).take(length_to_read); + /* Produce a Read that reads bytes up until the start of the central directory header. */ + let mut limited_raw = self.reader.by_ref().take(length_to_read); /* Copy over file data from source archive directly. */ io::copy(&mut limited_raw, &mut w)?; @@ -664,19 +744,6 @@ impl ZipArchive { }) } - const fn zip64_cde_len() -> usize { - mem::size_of::() - + mem::size_of::() - } - - const fn order_lower_upper_bounds(a: u64, b: u64) -> (u64, u64) { - if a > b { - (b, a) - } else { - (a, b) - } - } - fn get_directory_info_zip64( config: &Config, reader: &mut R, @@ -930,27 +997,8 @@ impl ZipArchive { &mut self, file_number: usize, ) -> ZipResult> { - let (_, data) = self - .shared - .files - .get_index(file_number) - .ok_or(ZipError::FileNotFound)?; - - let limit_reader = find_content(data, &mut self.reader)?; - match data.aes_mode { - None => Ok(None), - Some((aes_mode, _, _)) => { - let (verification_value, salt) = - AesReader::new(limit_reader, aes_mode, data.compressed_size) - .get_verification_value_and_salt()?; - let aes_info = AesInfo { - aes_mode, - verification_value, - salt, - }; - Ok(Some(aes_info)) - } - } + let entry = self.by_index_raw_new(file_number)?; + entry.get_aes_verification_key_and_salt() } /// Read a ZIP archive, collecting the files it contains. @@ -993,7 +1041,7 @@ impl ZipArchive { #[cfg(unix)] let mut files_by_unix_mode = Vec::new(); for i in 0..self.len() { - let mut file = self.by_index(i)?; + let mut file = self.by_index_new(i)?; let filepath = file .enclosed_name() .ok_or(ZipError::InvalidArchive("Invalid file path"))?; @@ -1006,6 +1054,8 @@ impl ZipArchive { } let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) { let mut target = Vec::with_capacity(file.size() as usize); + /* FIXME: this is broken: needs to be .read_to_end(), otherwise it writes into an + * empty slice. */ file.read_exact(&mut target)?; Some(target) } else { @@ -1030,7 +1080,7 @@ impl ZipArchive { }; let target = target.into_boxed_str(); let target_is_dir_from_archive = - self.shared.files.contains_key(&target) && is_dir(&target); + self.shared.files.contains_key(&target) && spec::is_dir(&target); let target_path = directory.as_ref().join(OsString::from(target.to_string())); let target_is_dir = if target_is_dir_from_archive { true @@ -1047,7 +1097,7 @@ impl ZipArchive { } continue; } - let mut file = self.by_index(i)?; + let mut file = self.by_index_new(i)?; let mut outfile = fs::File::create(&outpath)?; io::copy(&mut file, &mut outfile)?; #[cfg(unix)] @@ -1090,34 +1140,6 @@ impl ZipArchive { Ok(()) } - /// Number of files contained in this zip. - pub fn len(&self) -> usize { - self.shared.files.len() - } - - /// Whether this zip archive contains no files - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Get the offset from the beginning of the underlying reader that this zip begins at, in bytes. - /// - /// 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.shared.offset - } - - /// Get the comment of the zip archive. - pub fn comment(&self) -> &[u8] { - &self.comment - } - - /// Returns an iterator over all the file and directory names in this archive. - pub fn file_names(&self) -> impl Iterator { - self.shared.files.keys().map(|s| s.as_ref()) - } - /// Search for a file entry by name, decrypt with given password /// /// # Warning @@ -1140,35 +1162,12 @@ impl ZipArchive { self.by_name_with_optional_password(name, None) } - /// Get the index of a file entry by name, if it's present. - #[inline(always)] - pub fn index_for_name(&self, name: &str) -> Option { - self.shared.files.get_index_of(name) - } - - /// Get the index of a file entry by path, if it's present. - #[inline(always)] - pub fn index_for_path>(&self, path: T) -> Option { - self.index_for_name(&path_to_string(path)) - } - - /// Get the name of a file entry, if it's present. - #[inline(always)] - pub fn name_for_index(&self, index: usize) -> Option<&str> { - self.shared - .files - .get_index(index) - .map(|(name, _)| name.as_ref()) - } - fn by_name_with_optional_password<'a>( &'a mut self, name: &str, password: Option<&[u8]>, ) -> ZipResult> { - let Some(index) = self.shared.files.get_index_of(name) else { - return Err(ZipError::FileNotFound); - }; + let index = self.index_for_name_err(name)?; self.by_index_with_optional_password(index, password) } @@ -1257,6 +1256,146 @@ impl ZipArchive { } } +impl ZipArchive +where + R: Read + Seek, +{ + /// Search for a file entry by name + pub fn by_name_new( + &mut self, + name: impl AsRef, + ) -> ZipResult> { + let index = self.index_for_name_err(name)?; + self.by_index_new(index) + } + + /// Get a contained file by index + pub fn by_index_new(&mut self, file_number: usize) -> ZipResult> { + let Self { + ref mut reader, + ref shared, + .. + } = self; + let (_, data) = shared + .files + .get_index(file_number) + .ok_or(ZipError::FileNotFound)?; + let content = find_entry_content_range(data, reader)?; + let entry_reader = construct_decompressing_reader(&data.compression_method, content)?; + let crc32_reader = NewCrc32Reader::new(entry_reader, data.crc32); + Ok(ZipEntry { + data, + reader: crc32_reader, + }) + } + + /// Get a contained file by name without decompressing it + pub fn by_name_raw_new( + &mut self, + name: impl AsRef, + ) -> ZipResult> { + let index = self.index_for_name_err(name)?; + self.by_index_raw_new(index) + } + + /// Get a contained file by index without decompressing it + pub fn by_index_raw_new( + &mut self, + file_number: usize, + ) -> ZipResult> { + let Self { + ref mut reader, + ref shared, + .. + } = self; + let (_, data) = shared + .files + .get_index(file_number) + .ok_or(ZipError::FileNotFound)?; + let content = find_entry_content_range(data, reader)?; + Ok(ZipEntry { + data, + reader: content, + }) + } + + /// 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_new( + &mut self, + name: impl AsRef, + password: &[u8], + ) -> ZipResult> { + let index = self.index_for_name_err(name)?; + self.by_index_decrypt_new(index, password) + } + + /// 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_new( + &mut self, + file_number: usize, + password: &[u8], + ) -> ZipResult> { + let Self { + ref mut reader, + ref shared, + .. + } = self; + let (_, data) = shared + .files + .get_index(file_number) + .ok_or(ZipError::FileNotFound)?; + + let content = find_entry_content_range(data, reader)?; + + let final_reader = if data.encrypted { + let crypto_variant = CryptoVariant::from_data(data)?; + let is_ae2_encrypted = crypto_variant.is_ae2_encrypted(); + let crypto_reader = crypto_variant.make_crypto_reader(password, content)?; + let entry_reader = + construct_decompressing_reader(&data.compression_method, crypto_reader)?; + if is_ae2_encrypted { + /* Ae2 voids crc checking: https://www.winzip.com/en/support/aes-encryption/ */ + CryptoEntryReader::Ae2Encrypted(entry_reader) + } else { + CryptoEntryReader::NonAe2Encrypted(NewCrc32Reader::new(entry_reader, data.crc32)) + } + } else { + /* Not encrypted, so do the same as in .by_index_new(): */ + let entry_reader = construct_decompressing_reader(&data.compression_method, content)?; + CryptoEntryReader::Unencrypted(NewCrc32Reader::new(entry_reader, data.crc32)) + }; + + Ok(ZipEntry { + data, + reader: final_reader, + }) + } +} + /// Holds the AES information of a file in the zip archive #[derive(Debug)] #[cfg(feature = "aes-crypto")] @@ -1304,7 +1443,7 @@ fn read_variable_length_byte_field(reader: &mut R, len: usize) -> io::R } /// Parse a central directory entry to collect the information for the file. -fn central_header_to_zip_file_inner( +pub(crate) fn central_header_to_zip_file_inner( reader: &mut R, archive_offset: u64, central_header_start: u64, @@ -1376,13 +1515,8 @@ fn central_header_to_zip_file_inner( aes_extra_data_start: 0, extra_fields: Vec::new(), }; - match parse_extra_field(&mut result) { - Ok(stripped_extra_field) => { - result.extra_field = stripped_extra_field; - } - Err(ZipError::Io(..)) => {} - Err(e) => return Err(e), - } + let stripped_extra_field = parse_extra_field(&mut result)?; + result.extra_field = stripped_extra_field; let aes_enabled = result.compression_method == CompressionMethod::AES; if aes_enabled && result.aes_mode.is_none() { @@ -1411,9 +1545,17 @@ pub(crate) fn parse_extra_field(file: &mut ZipFileData) -> ZipResult b, + /* If we get an error reading too far, then assume this is an extra field we don't know + * how to handle, and just return the remaining amount. */ + Err(ZipError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { + return Ok(Some(processed_extra_field)) + } + Err(e) => return Err(e), + }; position = reader.position() as usize; if remove { let remaining = len - (position - old_position); @@ -1489,9 +1631,27 @@ pub(crate) fn parse_single_extra_field( _ => return Err(ZipError::InvalidArchive("Invalid AES vendor version")), }; match aes_mode { - 0x01 => file.aes_mode = Some((AesMode::Aes128, vendor_version, compression_method)), - 0x02 => file.aes_mode = Some((AesMode::Aes192, vendor_version, compression_method)), - 0x03 => file.aes_mode = Some((AesMode::Aes256, vendor_version, compression_method)), + 0x01 => { + file.aes_mode = Some(AesModeInfo { + aes_mode: AesMode::Aes128, + vendor_version, + compression_method, + }) + } + 0x02 => { + file.aes_mode = Some(AesModeInfo { + aes_mode: AesMode::Aes192, + vendor_version, + compression_method, + }) + } + 0x03 => { + file.aes_mode = Some(AesModeInfo { + aes_mode: AesMode::Aes256, + vendor_version, + compression_method, + }) + } _ => return Err(ZipError::InvalidArchive("Invalid AES encryption strength")), }; file.compression_method = compression_method; @@ -1647,9 +1807,10 @@ impl<'a> ZipFile<'a> { pub fn last_modified(&self) -> Option { self.data.last_modified_time } + /// Returns whether the file is actually a directory pub fn is_dir(&self) -> bool { - is_dir(self.name()) + self.data.is_dir() } /// Returns whether the file is actually a symbolic link @@ -1675,7 +1836,7 @@ impl<'a> ZipFile<'a> { /// Get the extra data of the zip header for this file pub fn extra_data(&self) -> Option<&[u8]> { - self.data.extra_field.as_ref().map(|v| v.deref().deref()) + self.data.extra_field.as_deref().map(|v| v.as_ref()) } /// Get the starting offset of the data of the compressed file diff --git a/src/types.rs b/src/types.rs index 91031a08c..1aff01b7c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -420,6 +420,13 @@ impl TryFrom for OffsetDateTime { pub const MIN_VERSION: u8 = 10; pub const DEFAULT_VERSION: u8 = 45; +#[derive(Debug, Copy, Clone)] +pub struct AesModeInfo { + pub aes_mode: AesMode, + pub vendor_version: AesVendorVersion, + pub compression_method: CompressionMethod, +} + /// Structure representing a ZIP file. #[derive(Debug, Clone, Default)] pub struct ZipFileData { @@ -470,7 +477,7 @@ pub struct ZipFileData { /// Reserve local ZIP64 extra field pub large_file: bool, /// AES mode if applicable - pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>, + pub aes_mode: Option, /// Specifies where in the extra data the AES metadata starts pub aes_extra_data_start: u64, @@ -621,7 +628,7 @@ impl ZipFileData { extra_data_start: Option, aes_extra_data_start: u64, compression_method: crate::compression::CompressionMethod, - aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>, + aes_mode: Option, extra_field: &[u8], ) -> Self where diff --git a/src/unstable.rs b/src/unstable.rs old mode 100644 new mode 100755 index 81cf18ea5..3c6a725cf --- a/src/unstable.rs +++ b/src/unstable.rs @@ -5,6 +5,8 @@ use std::io; use std::io::{Read, Write}; use std::path::{Component, Path, MAIN_SEPARATOR}; +pub mod read; + /// Provides high level API for reading from a stream. pub mod stream { pub use crate::read::stream::*; diff --git a/src/unstable/read.rs b/src/unstable/read.rs new file mode 100644 index 000000000..a4d5e4092 --- /dev/null +++ b/src/unstable/read.rs @@ -0,0 +1,751 @@ +//! Alternate implementation of [`crate::read`]. + +use crate::compression::CompressionMethod; +use crate::crc32::non_crypto::Crc32Reader; +use crate::extra_fields::ExtraField; +use crate::read::find_data_start; +use crate::result::{ZipError, ZipResult}; +use crate::types::ffi::S_IFLNK; +use crate::types::{DateTime, ZipFileData}; +use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator}; + +#[cfg(feature = "lzma")] +use crate::read::lzma::LzmaDecoder; +#[cfg(feature = "xz")] +use crate::read::xz::XzDecoder; +#[cfg(feature = "aes-crypto")] +use crate::{ + aes::{AesReader, AesReaderValid}, + read::AesInfo, + types::{AesModeInfo, AesVendorVersion}, +}; + +#[cfg(feature = "bzip2")] +use bzip2::read::BzDecoder; +#[cfg(feature = "deflate64")] +use deflate64::Deflate64Decoder; +#[cfg(feature = "deflate-flate2")] +use flate2::read::DeflateDecoder; +#[cfg(feature = "zstd")] +use zstd::stream::read::Decoder as ZstdDecoder; + +use std::io::{self, Read, Seek}; +use std::path::PathBuf; +use std::slice; + +pub(crate) enum EntryReader { + Stored(R), + #[cfg(feature = "_deflate-any")] + Deflated(DeflateDecoder), + #[cfg(feature = "deflate64")] + Deflate64(Deflate64Decoder>), + #[cfg(feature = "bzip2")] + Bzip2(BzDecoder), + #[cfg(feature = "zstd")] + Zstd(ZstdDecoder<'static, io::BufReader>), + #[cfg(feature = "lzma")] + /* According to clippy, this is >30x larger than the other variants, so we box it to avoid + * unnecessary large stack allocations. */ + Lzma(Box>), + #[cfg(feature = "xz")] + Xz(XzDecoder), +} + +impl Read for EntryReader +where + R: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + Self::Stored(r) => r.read(buf), + #[cfg(feature = "_deflate-any")] + Self::Deflated(r) => r.read(buf), + #[cfg(feature = "deflate64")] + Self::Deflate64(r) => r.read(buf), + #[cfg(feature = "bzip2")] + Self::Bzip2(r) => r.read(buf), + #[cfg(feature = "zstd")] + Self::Zstd(r) => r.read(buf), + #[cfg(feature = "lzma")] + Self::Lzma(r) => r.read(buf), + #[cfg(feature = "xz")] + Self::Xz(r) => r.read(buf), + } + } +} + +/// A struct for reading a zip file +pub struct ZipEntry<'a, R> { + pub(crate) data: &'a ZipFileData, + pub(crate) reader: R, +} + +impl<'a, R> Read for ZipEntry<'a, R> +where + R: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.reader.read(buf) + } +} + +impl<'a, R> ZipEntry<'a, R> +where + R: Read, +{ + /// Returns the verification value and salt for the AES encryption of the file + /// + /// # Returns + /// + /// - None if the file is not encrypted with AES + #[cfg(feature = "aes-crypto")] + pub fn get_aes_verification_key_and_salt(self) -> ZipResult> { + let Self { data, reader } = self; + if let Some(AesModeInfo { aes_mode, .. }) = data.aes_mode { + let (verification_value, salt) = AesReader::new(reader, aes_mode, data.compressed_size) + .get_verification_value_and_salt()?; + let aes_info = AesInfo { + aes_mode, + verification_value, + salt, + }; + Ok(Some(aes_info)) + } else { + Ok(None) + } + } +} + +mod sealed_data { + use super::ZipFileData; + + #[doc(hidden)] + pub trait ArchiveData { + fn data(&self) -> &ZipFileData; + } +} + +pub trait ArchiveEntry: Read + sealed_data::ArchiveData { + /// Get the version of the file + fn version_made_by(&self) -> (u8, u8) { + ( + self.data().version_made_by / 10, + self.data().version_made_by % 10, + ) + } + + /// 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. + fn name(&self) -> &str { + &self.data().file_name + } + + /// Get the name of the file, in the raw (internal) byte representation. + /// + /// The encoding of this data is currently undefined. + fn name_raw(&self) -> &[u8] { + &self.data().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` + fn mangled_name(&self) -> PathBuf { + self.data().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`]. + fn enclosed_name(&self) -> Option { + self.data().enclosed_name() + } + + /// Get the comment of the file + fn comment(&self) -> &str { + &self.data().file_comment + } + + /// Get the compression method used to store the file + fn compression(&self) -> CompressionMethod { + self.data().compression_method + } + + /// Get the size of the file, in bytes, in the archive + fn compressed_size(&self) -> u64 { + self.data().compressed_size + } + + /// Get the size of the file, in bytes, when uncompressed + fn size(&self) -> u64 { + self.data().uncompressed_size + } + + /// Get the time the file was last modified + fn last_modified(&self) -> Option { + self.data().last_modified_time + } + + /// Returns whether the file is actually a directory + fn is_dir(&self) -> bool { + self.data().is_dir() + } + + /// Returns whether the file is actually a symbolic link + fn is_symlink(&self) -> bool { + self.unix_mode() + .is_some_and(|mode| mode & S_IFLNK == S_IFLNK) + } + + /// Returns whether the file is a normal file (i.e. not a directory or symlink) + fn is_file(&self) -> bool { + !self.is_dir() && !self.is_symlink() + } + + /// Get unix mode for the file + fn unix_mode(&self) -> Option { + self.data().unix_mode() + } + + /// Get the CRC32 hash of the original file + fn crc32(&self) -> u32 { + self.data().crc32 + } + + /// Get the extra data of the zip header for this file + fn extra_data(&self) -> Option<&[u8]> { + self.data().extra_field.as_deref().map(|v| v.as_ref()) + } + + /// Get the starting offset of the data of the compressed file + fn data_start(&self) -> u64 { + *self.data().data_start.get().unwrap() + } + + /// Get the starting offset of the zip header for this file + fn header_start(&self) -> u64 { + self.data().header_start + } + /// Get the starting offset of the zip header in the central directory for this file + fn central_header_start(&self) -> u64 { + self.data().central_header_start + } + + /// iterate through all extra fields + fn extra_data_fields(&self) -> slice::Iter<'_, ExtraField> { + self.data().extra_fields.iter() + } +} + +impl<'a, R> sealed_data::ArchiveData for ZipEntry<'a, R> { + fn data(&self) -> &ZipFileData { + self.data + } +} + +impl<'a, R> ArchiveEntry for ZipEntry<'a, R> where R: Read {} + +pub(crate) fn find_entry_content_range( + data: &ZipFileData, + mut reader: R, +) -> Result, ZipError> +where + R: Read + Seek, +{ + // TODO: use .get_or_try_init() once stabilized to provide a closure returning a Result! + let data_start = match data.data_start.get() { + Some(data_start) => *data_start, + None => find_data_start(data, &mut reader)?, + }; + + reader.seek(io::SeekFrom::Start(data_start))?; + Ok(reader.take(data.compressed_size)) +} + +pub(crate) fn construct_decompressing_reader( + compression_method: &CompressionMethod, + reader: R, +) -> Result, ZipError> +where + /* TODO: this really shouldn't be required upon construction (especially since the reader + * doesn't need to be mutable, indicating the Read capability isn't used), but multiple of our + * constituent constructors require it. We should be able to make upstream PRs to fix these. */ + R: Read, +{ + match compression_method { + &CompressionMethod::Stored => Ok(EntryReader::Stored(reader)), + #[cfg(feature = "_deflate-any")] + &CompressionMethod::Deflated => { + let deflate_reader = DeflateDecoder::new(reader); + Ok(EntryReader::Deflated(deflate_reader)) + } + #[cfg(feature = "deflate64")] + &CompressionMethod::Deflate64 => { + let deflate64_reader = Deflate64Decoder::new(reader); + Ok(EntryReader::Deflate64(deflate64_reader)) + } + #[cfg(feature = "bzip2")] + &CompressionMethod::Bzip2 => { + let bzip2_reader = BzDecoder::new(reader); + Ok(EntryReader::Bzip2(bzip2_reader)) + } + #[cfg(feature = "zstd")] + &CompressionMethod::Zstd => { + let zstd_reader = ZstdDecoder::new(reader).unwrap(); + Ok(EntryReader::Zstd(zstd_reader)) + } + #[cfg(feature = "lzma")] + &CompressionMethod::Lzma => { + let reader = LzmaDecoder::new(reader); + Ok(EntryReader::Lzma(Box::new(reader))) + } + #[cfg(feature = "xz")] + &CompressionMethod::Xz => { + let reader = XzDecoder::new(reader); + Ok(EntryReader::Xz(reader)) + } + /* TODO: make this into its own EntryReadError error type! */ + _ => Err(ZipError::UnsupportedArchive( + "Compression method not supported", + )), + } +} + +pub(crate) enum CryptoReader { + ZipCrypto(ZipCryptoReaderValid), + #[cfg(feature = "aes-crypto")] + Aes(AesReaderValid), +} + +impl Read for CryptoReader +where + R: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + CryptoReader::ZipCrypto(r) => r.read(buf), + #[cfg(feature = "aes-crypto")] + CryptoReader::Aes(r) => r.read(buf), + } + } +} + +pub(crate) enum CryptoVariant { + Crc32(u32), + DateTime(DateTime), + #[cfg(feature = "aes-crypto")] + Aes { + info: AesModeInfo, + compressed_size: u64, + }, +} + +impl CryptoVariant { + pub fn from_data(data: &ZipFileData) -> Result { + assert!( + data.encrypted, + "should never enter this method except on encrypted entries" + ); + + #[allow(deprecated)] + if let CompressionMethod::Unsupported(_) = data.compression_method { + /* TODO: make this into its own EntryReadError error type! */ + return Err(ZipError::UnsupportedArchive( + "Compression method not supported", + )); + } + + #[cfg(feature = "aes-crypto")] + if let Some(info) = data.aes_mode { + return Ok(Self::Aes { + info, + compressed_size: data.compressed_size, + }); + } + #[cfg(not(feature = "aes-crypto"))] + if data.aes_mode.is_some() { + /* TODO: make this into its own EntryReadError error type! */ + return Err(ZipError::UnsupportedArchive( + "AES encrypted files cannot be decrypted without the aes-crypto feature.", + )); + } + + if let Some(last_modified_time) = data.last_modified_time { + /* TODO: use let chains once stabilized! */ + if data.using_data_descriptor { + return Ok(Self::DateTime(last_modified_time)); + } + } + + Ok(Self::Crc32(data.crc32)) + } + + /// Returns `true` if the data is encrypted using AE2. + pub const fn is_ae2_encrypted(&self) -> bool { + match self { + #[cfg(feature = "aes-crypto")] + Self::Aes { + info: + AesModeInfo { + vendor_version: AesVendorVersion::Ae2, + .. + }, + .. + } => true, + _ => false, + } + } + + pub fn make_crypto_reader( + self, + password: &[u8], + reader: R, + ) -> Result, ZipError> + where + R: Read, + { + match self { + #[cfg(feature = "aes-crypto")] + Self::Aes { + info: AesModeInfo { aes_mode, .. }, + compressed_size, + } => { + let aes_reader = + AesReader::new(reader, aes_mode, compressed_size).validate(password)?; + Ok(CryptoReader::Aes(aes_reader)) + } + Self::DateTime(last_modified_time) => { + let validator = ZipCryptoValidator::InfoZipMsdosTime(last_modified_time.timepart()); + let zc_reader = ZipCryptoReader::new(reader, password).validate(validator)?; + Ok(CryptoReader::ZipCrypto(zc_reader)) + } + Self::Crc32(crc32) => { + let validator = ZipCryptoValidator::PkzipCrc32(crc32); + let zc_reader = ZipCryptoReader::new(reader, password).validate(validator)?; + Ok(CryptoReader::ZipCrypto(zc_reader)) + } + } + } +} + +pub(crate) enum CryptoEntryReader { + Unencrypted(Crc32Reader>), + Ae2Encrypted(EntryReader>), + NonAe2Encrypted(Crc32Reader>>), +} + +impl Read for CryptoEntryReader +where + R: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + Self::Unencrypted(r) => r.read(buf), + Self::Ae2Encrypted(r) => r.read(buf), + Self::NonAe2Encrypted(r) => r.read(buf), + } + } +} + +pub mod streaming { + use super::{ + construct_decompressing_reader, sealed_data, ArchiveEntry, Crc32Reader, ZipError, + ZipFileData, ZipResult, + }; + + use crate::read::{central_header_to_zip_file_inner, parse_extra_field}; + use crate::spec::{self, FixedSizeBlock}; + use crate::types::{ZipCentralEntryBlock, ZipLocalEntryBlock}; + + use std::io::{self, Read}; + use std::mem; + use std::ops; + + pub struct StreamingArchive { + reader: R, + remaining_before_next_entry: u64, + first_metadata_block: Option<[u8; mem::size_of::()]>, + } + + impl StreamingArchive { + pub const fn new(reader: R) -> Self { + Self { + reader, + remaining_before_next_entry: 0, + first_metadata_block: None, + } + } + + pub fn into_inner(self) -> R { + let Self { reader, .. } = self; + reader + } + } + + impl StreamingArchive + where + R: Read, + { + fn drain_remaining(&mut self) -> Result<(), ZipError> { + let Self { + ref mut reader, + ref mut remaining_before_next_entry, + .. + } = self; + if *remaining_before_next_entry > 0 { + io::copy( + &mut reader.by_ref().take(*remaining_before_next_entry), + &mut io::sink(), + )?; + *remaining_before_next_entry = 0; + } + Ok(()) + } + + pub fn next_entry(&mut self) -> ZipResult>> { + // We can't use the typical ::parse() method, as we follow separate code paths depending + // on the "magic" value (since the magic value will be from the central directory header + // if we've finished iterating over all the actual files). + self.drain_remaining()?; + let Self { + ref mut reader, + ref mut remaining_before_next_entry, + ref mut first_metadata_block, + } = self; + assert_eq!(0, *remaining_before_next_entry); + + let mut block = [0u8; mem::size_of::()]; + reader.read_exact(&mut block)?; + + let signature = spec::Magic::from_first_le_bytes(block.as_ref()); + match signature { + ZipLocalEntryBlock::MAGIC => (), + /* If the signature corresponds to the first central directory entry, then we are + * out of file entries. We can't seek backwards in a stream, so we save the block + * we just read to our mutable state. */ + ZipCentralEntryBlock::MAGIC => { + assert!( + first_metadata_block.is_none(), + "metadata block should never be set except exactly once" + ); + assert!( + mem::size_of::() + < mem::size_of::() + ); + *first_metadata_block = Some(block); + return Ok(None); + } + _ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR), + } + let block = ZipLocalEntryBlock::interpret(&block)?; + + let mut data = ZipFileData::from_local_block(block, reader)?; + + let stripped_extra_field = parse_extra_field(&mut data)?; + data.extra_field = stripped_extra_field; + + let limit_reader = + DrainWrapper::new(data.compressed_size, remaining_before_next_entry, reader); + let entry_reader = + construct_decompressing_reader(&data.compression_method, limit_reader)?; + let crc32_reader = Crc32Reader::new(entry_reader, data.crc32); + Ok(Some(StreamingZipEntry { + data, + reader: crc32_reader, + })) + } + + pub fn next_metadata_entry(&mut self) -> ZipResult> { + /* We should only need to drain remaining exactly once (if at all), since that's only + * employed if the end user fails to read the entire contents of a streaming file + * entry. */ + self.drain_remaining()?; + let Self { + ref mut reader, + ref remaining_before_next_entry, + ref mut first_metadata_block, + } = self; + assert_eq!(0, *remaining_before_next_entry); + + /* Get the bytes out of the stream necessary to create a parseable central block. */ + let block: [u8; mem::size_of::()] = + match first_metadata_block.take() { + /* If we have a block we tried to parse earlier from .next_entry(), get the + * data from there, then read the additional bytes necessary to construct + * a central directory entry. This should always happen exactly once. */ + Some(block) => { + assert!( + mem::size_of::() + < mem::size_of::() + ); + assert_eq!(block.len(), mem::size_of::()); + + let mut remaining_block = [0u8; mem::size_of::() + - mem::size_of::()]; + reader.read_exact(remaining_block.as_mut())?; + + let mut joined_block = [0u8; mem::size_of::()]; + joined_block[..block.len()].copy_from_slice(&block); + joined_block[block.len()..].copy_from_slice(&remaining_block); + joined_block + } + /* After the first central block is parsed, we should always go into this + * branch, reading the necessary bytes from the stream. */ + None => { + let mut block = [0u8; mem::size_of::()]; + match reader.read_exact(&mut block) { + Ok(()) => (), + /* The reader is done! This is expected to happen exactly once when the + * stream is completely finished. */ + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e.into()), + }; + block + } + }; + // Parse central header + let block = ZipCentralEntryBlock::interpret(&block)?; + + // 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; + + let data = central_header_to_zip_file_inner( + reader, + archive_offset, + central_header_start, + block, + )?; + Ok(Some(ZipStreamFileMetadata(data))) + } + } + + struct DrainWrapper<'a, R> { + full_extent: usize, + current_progress: usize, + remaining_to_notify: &'a mut u64, + inner: R, + } + + impl<'a, R> DrainWrapper<'a, R> { + pub fn new(extent: u64, remaining_to_notify: &'a mut u64, inner: R) -> Self { + Self { + full_extent: extent.try_into().unwrap(), + current_progress: 0, + remaining_to_notify, + inner, + } + } + + fn remaining(&self) -> usize { + debug_assert!(self.current_progress <= self.full_extent); + self.full_extent - self.current_progress + } + } + + impl<'a, R> Read for DrainWrapper<'a, R> + where + R: Read, + { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + assert!(!buf.is_empty()); + let to_read = self.remaining().min(buf.len()); + /* If the input is exhausted, or `buf` was empty, we are done. */ + if to_read == 0 { + return Ok(0); + } + + let count = self.inner.read(&mut buf[..to_read])?; + if count == 0 { + /* `to_read` was >0, so this was unexpected: */ + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "failed to read expected number of bytes for zip entry from stream", + )); + } + + debug_assert!(count <= to_read); + self.current_progress += count; + Ok(count) + } + } + + impl<'a, R> ops::Drop for DrainWrapper<'a, R> { + fn drop(&mut self) { + assert_eq!( + 0, *self.remaining_to_notify, + "remaining should always be zero before drop is called" + ); + *self.remaining_to_notify = self.remaining().try_into().unwrap(); + } + } + + /// A struct for reading a zip file from a stream. + pub struct StreamingZipEntry { + pub(crate) data: ZipFileData, + pub(crate) reader: R, + } + + impl Read for StreamingZipEntry + where + R: Read, + { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.reader.read(buf) + } + } + + impl sealed_data::ArchiveData for StreamingZipEntry { + fn data(&self) -> &ZipFileData { + &self.data + } + } + + impl ArchiveEntry for StreamingZipEntry where R: Read {} + + /// Additional metadata for the file. + #[derive(Debug)] + pub struct ZipStreamFileMetadata(ZipFileData); + + impl sealed_data::ArchiveData for ZipStreamFileMetadata { + fn data(&self) -> &ZipFileData { + let Self(data) = self; + data + } + } + + impl Read for ZipStreamFileMetadata { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + Ok(0) + } + } + + impl ArchiveEntry for ZipStreamFileMetadata {} +} diff --git a/src/write.rs b/src/write.rs index a8a31a53f..d719b0b2b 100644 --- a/src/write.rs +++ b/src/write.rs @@ -3,16 +3,17 @@ #[cfg(feature = "aes-crypto")] use crate::aes::AesWriter; use crate::compression::CompressionMethod; -use crate::read::{ - find_content, parse_single_extra_field, Config, ZipArchive, ZipFile, ZipFileReader, -}; +use crate::read::ZipFile; +use crate::read::{parse_single_extra_field, Config, ZipArchive}; use crate::result::{ZipError, ZipResult}; use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock}; #[cfg(feature = "aes-crypto")] use crate::types::AesMode; use crate::types::{ - ffi, AesVendorVersion, DateTime, ZipFileData, ZipLocalEntryBlock, ZipRawValues, MIN_VERSION, + ffi, AesModeInfo, AesVendorVersion, DateTime, ZipFileData, ZipLocalEntryBlock, ZipRawValues, + MIN_VERSION, }; +use crate::unstable::read::find_entry_content_range; use crate::write::ffi::S_IFLNK; #[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))] use core::num::NonZeroU64; @@ -85,7 +86,7 @@ impl Write for MaybeEncrypted { } } -enum GenericZipWriter { +enum GenericZipWriter { Closed, Storer(MaybeEncrypted), #[cfg(feature = "deflate-flate2")] @@ -683,10 +684,8 @@ impl ZipWriter { compressed_size, uncompressed_size, }; - let mut reader = BufReader::new(ZipFileReader::Raw(find_content( - src_data, - self.inner.get_plain(), - )?)); + let mut reader = + BufReader::new(find_entry_content_range(src_data, self.inner.get_plain())?); let mut copy = Vec::with_capacity(compressed_size as usize); reader.read_to_end(&mut copy)?; drop(reader); @@ -902,7 +901,11 @@ impl ZipWriter { #[cfg(feature = "aes-crypto")] Some(EncryptWith::Aes { mode, .. }) => ( CompressionMethod::Aes, - Some((mode, AesVendorVersion::Ae2, options.compression_method)), + Some(AesModeInfo { + aes_mode: mode, + vendor_version: AesVendorVersion::Ae2, + compression_method: options.compression_method, + }), ), _ => (options.compression_method, None), }; @@ -1058,7 +1061,7 @@ impl ZipWriter { // unencrypted contents. // // C.f. https://www.winzip.com/en/support/aes-encryption/#crc-faq - aes_mode.1 = if self.stats.bytes_written < 20 { + aes_mode.vendor_version = if self.stats.bytes_written < 20 { crc = false; AesVendorVersion::Ae2 } else { @@ -1247,7 +1250,7 @@ impl ZipWriter { /// Add a new file using the already compressed data from a ZIP file being read and renames it, this /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again. /// Any `ZipFile` metadata is copied and not checked, for example the file CRC. - + /// /// ```no_run /// use std::fs::File; /// use std::io::{Read, Seek, Write}; @@ -1818,7 +1821,12 @@ fn update_aes_extra_data( writer: &mut W, file: &mut ZipFileData, ) -> ZipResult<()> { - let Some((aes_mode, version, compression_method)) = file.aes_mode else { + let Some(AesModeInfo { + aes_mode, + vendor_version: version, + compression_method, + }) = file.aes_mode + else { return Ok(()); }; diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs index bd81b5c43..42d0e58b6 100644 --- a/src/zipcrypto.rs +++ b/src/zipcrypto.rs @@ -202,8 +202,9 @@ impl std::io::Read for ZipCryptoReaderValid { } } -impl ZipCryptoReaderValid { +impl ZipCryptoReaderValid { /// Consumes this decoder, returning the underlying reader. + #[allow(dead_code)] pub fn into_inner(self) -> R { self.reader.file }