diff --git a/examples/injection_validation_reader.rs b/examples/injection_validation_reader.rs new file mode 100644 index 000000000..af0a7322e --- /dev/null +++ b/examples/injection_validation_reader.rs @@ -0,0 +1,40 @@ +//! Read a ZIP archive with CRC checking disabled by using a custom final reader. +//! The built-in final reader (Crc32Reader) is replaced with a pass-through that does not validate CRC. + +use std::fs::File; +use std::io::Read; +use std::sync::Arc; +use zip::read::Config; +use zip::{ValidatorReaderFactory, ZipArchive, ZipReadOptions}; + +/// Final reader that just passes through the decompressed stream (no CRC validation). +fn skip_crc_check<'a>( + inner: Box, + _crc32: u32, + _ae2: bool, +) -> Box { + inner +} + +fn main() -> zip::result::ZipResult<()> { + // Override with your file; make sure to use a small dummy zip + let file = File::open("/home/user/dummy.zip")?; + let config = Config::default(); + let mut archive = ZipArchive::with_config(config, file)?; + + let factory: Arc = Arc::new(skip_crc_check); + + for i in 0..archive.len() { + let options = ZipReadOptions::new().final_reader_factory(Some(Arc::clone(&factory))); + let mut zip_file = archive.by_index_with_options(i, options)?; + println!("Filename: {}", zip_file.name()); + println!("---"); + let mut buf = Vec::new(); + zip_file.read_to_end(&mut buf)?; + let content = String::from_utf8_lossy(&buf); + print!("{content}"); + println!("---"); + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 6efa1a85c..0befabf32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ #![allow(clippy::multiple_crate_versions)] // https://github.com/rust-lang/rust-clippy/issues/16440 pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; pub use crate::read::HasZipMetadata; -pub use crate::read::{ZipArchive, ZipReadOptions}; +pub use crate::read::{ValidatorReaderFactory, ZipArchive, ZipReadOptions}; pub use crate::spec::{ZIP64_BYTES_THR, ZIP64_ENTRY_THR}; pub use crate::types::{AesMode, DateTime, System}; pub use crate::write::ZipWriter; diff --git a/src/read.rs b/src/read.rs index 983fc32c8..39c46075c 100644 --- a/src/read.rs +++ b/src/read.rs @@ -27,6 +27,12 @@ mod config; pub use config::{ArchiveOffset, Config}; +/// Factory for the final reader in the chain (wraps the decompressed stream). +/// Use with `ZipReadOptions::final_reader_factory`. Example: `Arc::new(|inner, _crc32, _ae2| inner)` to skip CRC checks. +/// `take_raw_reader()` is not supported when using a custom final reader. +pub type ValidatorReaderFactory = + dyn for<'a> Fn(Box, u32, bool) -> Box + Send + Sync; + /// Provides high level API for reading from a stream. pub(crate) mod stream; @@ -191,11 +197,32 @@ fn invalid_state() -> io::Result { Err(io::Error::other("ZipFileReader was in an invalid state")) } +// Same inner chain in both variants: Decompressor>>. +// We only swap the final reader (Go-style: swap one layer in the chain). +type BuiltinCrcReader<'a, R> = + Crc32Reader>>>; + +pub(crate) enum CompressedInner<'a, R: Read + ?Sized> { + /// Builtin: Crc32Reader wraps Decompressor>. + Builtin(Box>), + /// Custom: user's reader wraps the same decompressor (factory receives it as Box). + Custom(Box), +} + +impl core::fmt::Debug for CompressedInner<'_, R> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CompressedInner::Builtin(_) => f.write_str("Builtin(..)"), + CompressedInner::Custom(_) => f.write_str("Custom(..)"), + } + } +} + #[derive(Debug)] pub(crate) enum ZipFileReader<'a, R: Read + ?Sized> { NoReader, Raw(io::Take<&'a mut R>), - Compressed(Box>>>>), + Compressed(CompressedInner<'a, R>), } impl Read for ZipFileReader<'_, R> { @@ -203,7 +230,8 @@ impl Read for ZipFileReader<'_, R> { match self { ZipFileReader::NoReader => invalid_state(), ZipFileReader::Raw(r) => r.read(buf), - ZipFileReader::Compressed(r) => r.read(buf), + ZipFileReader::Compressed(CompressedInner::Builtin(r)) => r.read(buf), + ZipFileReader::Compressed(CompressedInner::Custom(r)) => r.read(buf), } } @@ -211,7 +239,8 @@ impl Read for ZipFileReader<'_, R> { match self { ZipFileReader::NoReader => invalid_state(), ZipFileReader::Raw(r) => r.read_exact(buf), - ZipFileReader::Compressed(r) => r.read_exact(buf), + ZipFileReader::Compressed(CompressedInner::Builtin(r)) => r.read_exact(buf), + ZipFileReader::Compressed(CompressedInner::Custom(r)) => r.read_exact(buf), } } @@ -219,7 +248,8 @@ impl Read for ZipFileReader<'_, R> { match self { ZipFileReader::NoReader => invalid_state(), ZipFileReader::Raw(r) => r.read_to_end(buf), - ZipFileReader::Compressed(r) => r.read_to_end(buf), + ZipFileReader::Compressed(CompressedInner::Builtin(r)) => r.read_to_end(buf), + ZipFileReader::Compressed(CompressedInner::Custom(r)) => r.read_to_end(buf), } } @@ -227,7 +257,8 @@ impl Read for ZipFileReader<'_, R> { match self { ZipFileReader::NoReader => invalid_state(), ZipFileReader::Raw(r) => r.read_to_string(buf), - ZipFileReader::Compressed(r) => r.read_to_string(buf), + ZipFileReader::Compressed(CompressedInner::Builtin(r)) => r.read_to_string(buf), + ZipFileReader::Compressed(CompressedInner::Custom(r)) => r.read_to_string(buf), } } } @@ -237,9 +268,12 @@ impl<'a, R: Read + ?Sized> ZipFileReader<'a, R> { match self { ZipFileReader::NoReader => invalid_state(), ZipFileReader::Raw(r) => Ok(r), - ZipFileReader::Compressed(r) => { + ZipFileReader::Compressed(CompressedInner::Builtin(r)) => { Ok(r.into_inner().into_inner()?.into_inner().into_inner()) } + ZipFileReader::Compressed(CompressedInner::Custom(_)) => Err(io::Error::other( + "take_raw_reader is not supported when using a custom final reader", + )), } } } @@ -429,26 +463,36 @@ pub(crate) fn make_crypto_reader<'a, R: Read + ?Sized>( Ok(reader) } -pub(crate) fn make_reader( +pub(crate) fn make_reader<'a, R: Read + ?Sized>( compression_method: CompressionMethod, uncompressed_size: u64, crc32: u32, - reader: CryptoReader<'_, R>, + reader: CryptoReader<'a, R>, #[cfg(feature = "legacy-zip")] flags: u16, -) -> ZipResult> { + final_reader_factory: Option<&Arc>, +) -> ZipResult> { let ae2_encrypted = reader.is_ae2_encrypted(); #[cfg(not(feature = "legacy-zip"))] let flags = 0; - Ok(ZipFileReader::Compressed(Box::new(Crc32Reader::new( - Decompressor::new( - io::BufReader::new(reader), - compression_method, - uncompressed_size, - flags, - )?, - crc32, - ae2_encrypted, - )))) + let decompressor = Decompressor::new( + io::BufReader::new(reader), + compression_method, + uncompressed_size, + flags, + )?; + let inner = match final_reader_factory { + None => CompressedInner::Builtin(Box::new(Crc32Reader::new( + decompressor, + crc32, + ae2_encrypted, + ))), + Some(factory) => { + let boxed: Box = Box::new(decompressor); + let custom = factory.as_ref()(boxed, crc32, ae2_encrypted); + CompressedInner::Custom(custom) + } + }; + Ok(ZipFileReader::Compressed(inner)) } pub(crate) fn make_symlink( @@ -1287,6 +1331,7 @@ impl ZipArchive { crypto_reader, #[cfg(feature = "legacy-zip")] data.flags, + options.validation_reader_factory.as_ref(), )?, }) } @@ -1733,6 +1778,9 @@ pub struct ZipReadOptions<'a> { /// Ignore the value of the encryption flag and proceed as if the file were plaintext. ignore_encryption_flag: bool, + + /// Custom validation reader in the chain (e.g. to skip or replace CRC checking). Default is the built-in CRC reader. + validation_reader_factory: Option>, } impl<'a> ZipReadOptions<'a> { @@ -1755,6 +1803,13 @@ impl<'a> ZipReadOptions<'a> { self.ignore_encryption_flag = ignore; self } + + /// Set a custom final reader factory (final reader in the chain). Return for chaining. + #[must_use] + pub fn final_reader_factory(mut self, f: Option>) -> Self { + self.validation_reader_factory = f; + self + } } /// Methods for retrieving information on zip files @@ -2112,9 +2167,12 @@ impl Drop for ZipFile<'_, R> { // self.data is Owned, this reader is constructed by a streaming reader. // In this case, we want to exhaust the reader so that the next file is accessible. if let Cow::Owned(_) = self.data { - // Get the inner `Take` reader so all decryption, decompression and CRC calculation is skipped. if let Ok(mut inner) = self.take_raw_reader() { let _ = copy(&mut inner, &mut sink()); + } else if let ZipFileReader::Compressed(CompressedInner::Custom(mut r)) = + replace(&mut self.reader, ZipFileReader::NoReader) + { + let _ = copy(&mut *r, &mut sink()); } } } @@ -2180,6 +2238,7 @@ pub fn read_zipfile_from_stream(reader: &mut R) -> ZipResult( crypto_reader, #[cfg(feature = "legacy-zip")] flags, + None, )?, })) }