diff --git a/src/read.rs b/src/read.rs index 129b37ff..6296d4fa 100644 --- a/src/read.rs +++ b/src/read.rs @@ -8,7 +8,7 @@ use crate::crc32::Crc32Reader; use crate::extra_fields::{ExtendedTimestamp, ExtraField}; use crate::read::zip_archive::{Shared, SharedBuilder}; use crate::result::{ZipError, ZipResult}; -use crate::spec::{self, FixedSizeBlock}; +use crate::spec::{self, FixedSizeBlock, Zip32CentralDirectoryEnd}; use crate::types::{ AesMode, AesVendorVersion, DateTime, System, ZipCentralEntryBlock, ZipFileData, ZipLocalEntryBlock, @@ -23,6 +23,7 @@ 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}; #[cfg(feature = "deflate-flate2")] @@ -671,51 +672,55 @@ impl ZipArchive { pub(crate) fn get_metadata( config: Config, reader: &mut R, - footer: &spec::Zip32CentralDirectoryEnd, - cde_start_pos: u64, - ) -> ZipResult { - // Check if file has a zip64 footer - let results = Self::get_directory_info_zip64(&config, reader, footer, cde_start_pos) - .unwrap_or_else(|e| vec![Err(e)]); + ) -> ZipResult<(Zip32CentralDirectoryEnd, Shared)> { let mut invalid_errors = Vec::new(); let mut unsupported_errors = Vec::new(); let mut ok_results = Vec::new(); - results - .into_iter() - .map(|result| { - result.and_then(|dir_info| Self::read_central_header(dir_info, config, reader)) - }) - .for_each(|result| { - Self::sort_result( - result, - &mut invalid_errors, - &mut unsupported_errors, - &mut ok_results, - ) - }); - if ok_results.is_empty() { + let cde_locations = spec::Zip32CentralDirectoryEnd::find_and_parse(reader)?; + IntoIterator::into_iter(cde_locations).for_each(|(footer, cde_start_pos)| { let zip32_result = - Self::get_directory_info_zip32(&config, reader, footer, cde_start_pos); + Self::get_directory_info_zip32(&config, reader, &footer, cde_start_pos); Self::sort_result( zip32_result.and_then(|result| Self::read_central_header(result, config, reader)), &mut invalid_errors, &mut unsupported_errors, &mut ok_results, + &footer, ); - } + // Check if file has a zip64 footer + if let Ok(zip64_footers) = + Self::get_directory_info_zip64(&config, reader, &footer, cde_start_pos) + { + zip64_footers + .into_iter() + .map(|result| { + result.and_then(|dir_info| { + Self::read_central_header(dir_info, config, reader) + }) + }) + .for_each(|result| { + Self::sort_result( + result, + &mut invalid_errors, + &mut unsupported_errors, + &mut ok_results, + &footer, + ) + }); + } + }); if ok_results.is_empty() { return Err(unsupported_errors .into_iter() .next() .unwrap_or_else(|| invalid_errors.into_iter().next().unwrap())); } - let shared = ok_results + let (footer, shared) = ok_results .into_iter() - .max_by_key(|shared| (shared.dir_start - shared.offset, shared.dir_start)) - .unwrap() - .build(); + .max_by_key(|(_, shared)| (shared.dir_start - shared.offset, shared.dir_start)) + .unwrap(); reader.seek(io::SeekFrom::Start(shared.dir_start))?; - Ok(shared) + Ok((Rc::try_unwrap(footer).unwrap(), shared.build())) } fn read_central_header( @@ -751,14 +756,15 @@ impl ZipArchive { result: Result, invalid_errors: &mut Vec, unsupported_errors: &mut Vec, - ok_results: &mut Vec, + ok_results: &mut Vec<(Rc, SharedBuilder)>, + footer: &Rc, ) { match result { Err(ZipError::UnsupportedArchive(e)) => { unsupported_errors.push(ZipError::UnsupportedArchive(e)) } Err(e) => invalid_errors.push(e), - Ok(o) => ok_results.push(o), + Ok(o) => ok_results.push((footer.clone(), o)), } } @@ -810,15 +816,12 @@ impl ZipArchive { /// /// This uses the central directory record of the ZIP file, and ignores local file headers. pub fn with_config(config: Config, mut reader: R) -> ZipResult> { - let mut results = spec::Zip32CentralDirectoryEnd::find_and_parse(&mut reader)?; - for (footer, cde_start_pos) in results.iter_mut() { - if let Ok(shared) = Self::get_metadata(config, &mut reader, footer, *cde_start_pos) { - return Ok(ZipArchive { - reader, - shared: shared.into(), - comment: Arc::from(mem::replace(&mut footer.zip_file_comment, Box::new([]))), - }); - } + if let Ok((footer, shared)) = Self::get_metadata(config, &mut reader) { + return Ok(ZipArchive { + reader, + shared: shared.into(), + comment: footer.zip_file_comment.into(), + }); } Err(InvalidArchive("No valid central directory found")) } diff --git a/src/spec.rs b/src/spec.rs index 3aa14d6d..84aa06bf 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -5,6 +5,7 @@ use memchr::memmem::FinderRev; use std::io; use std::io::prelude::*; use std::mem; +use std::rc::Rc; /// "Magic" header values used in the zip spec to locate metadata records. /// @@ -298,7 +299,7 @@ impl Zip32CentralDirectoryEnd { pub fn find_and_parse( reader: &mut T, - ) -> ZipResult> { + ) -> ZipResult, u64)]>> { let mut results = vec![]; let file_length = reader.seek(io::SeekFrom::End(0))?; @@ -338,7 +339,7 @@ impl Zip32CentralDirectoryEnd { reader.seek(io::SeekFrom::Start(cde_start_pos))?; /* Drop any headers that don't parse. */ if let Ok(cde) = Self::parse(reader) { - results.push((cde, cde_start_pos)); + results.push((Rc::new(cde), cde_start_pos)); } } diff --git a/src/write.rs b/src/write.rs index 37453eed..e4114f7d 100644 --- a/src/write.rs +++ b/src/write.rs @@ -625,23 +625,19 @@ impl ZipWriter { /// /// This uses the given read configuration to initially read the archive. pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult> { - let mut results = spec::Zip32CentralDirectoryEnd::find_and_parse(&mut readwriter)?; - for (footer, cde_start_pos) in results.iter_mut() { - if let Ok(metadata) = - ZipArchive::get_metadata(config, &mut readwriter, footer, *cde_start_pos) - { - return Ok(ZipWriter { - inner: Storer(MaybeEncrypted::Unencrypted(readwriter)), - files: metadata.files, - stats: Default::default(), - writing_to_file: false, - comment: mem::replace(&mut footer.zip_file_comment, Box::new([])), - writing_raw: true, // avoid recomputing the last file's header - flush_on_finish_file: false, - }); - } + if let Ok((footer, shared)) = ZipArchive::get_metadata(config, &mut readwriter) { + Ok(ZipWriter { + inner: Storer(MaybeEncrypted::Unencrypted(readwriter)), + files: shared.files, + stats: Default::default(), + writing_to_file: false, + comment: footer.zip_file_comment, + writing_raw: true, // avoid recomputing the last file's header + flush_on_finish_file: false, + }) + } else { + Err(InvalidArchive("No central-directory end header found")) } - Err(InvalidArchive("No central-directory end header found")) } /// `flush_on_finish_file` is designed to support a streaming `inner` that may unload flushed @@ -3300,4 +3296,76 @@ mod test { let _ = writer.finish_into_readable()?; Ok(()) } + + #[test] + fn test_fuzz_crash_2024_06_18a() -> ZipResult<()> { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + writer.set_raw_comment(Box::<[u8]>::from([])); + let sub_writer = { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let sub_writer = { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let sub_writer = { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let options = FullFileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::from_date_and_time(2107, 4, 8, 14, 0, 19)?, + permissions: None, + large_file: false, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![ + 182, 180, 1, 0, 180, 182, 74, 0, 0, 200, 0, 0, 0, 2, 0, 0, 0, + ] + .into(), + central_extra_data: vec![].into(), + }, + alignment: 1542, + ..Default::default() + }; + writer.start_file_from_path("\0\0PK\u{6}\u{6}K\u{6}PK\u{3}\u{4}\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\u{1}\0\0\0\0\0\0\0\0\u{1}\u{1}\0PK\u{1}\u{2},\0\0\0\0\0\0\0\0\0\0\0P\u{7}\u{4}/.\0KP\0\0;\0\0\0\u{1e}\0\0\0\0\0\0\0\0\0\0\0\0\0", options)?; + let finished = writer.finish_into_readable()?; + assert_eq!(1, finished.file_names().count()); + writer = ZipWriter::new_append(finished.into_inner())?; + let options = FullFileOptions { + compression_method: Stored, + compression_level: Some(5), + last_modified_time: DateTime::from_date_and_time(2107, 4, 1, 0, 0, 0)?, + permissions: None, + large_file: false, + encrypt_with: Some(ZipCrypto( + ZipCryptoKeys::of(0x0, 0x62e4b50, 0x100), + PhantomData, + )), + ..Default::default() + }; + writer.add_symlink_from_path( + "\0K\u{6}\0PK\u{6}\u{7}PK\u{6}\u{6}\0\0\0\0\0\0\0\0PK\u{2}\u{6}", + "\u{8}\0\0\0\0/\0", + options, + )?; + let finished = writer.finish_into_readable()?; + assert_eq!(2, finished.file_names().count()); + writer = ZipWriter::new_append(finished.into_inner())?; + assert_eq!(2, writer.files.len()); + writer + }; + let finished = sub_writer.finish_into_readable()?; + assert_eq!(2, finished.file_names().count()); + writer.merge_archive(finished)?; + writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + writer + }; + writer.merge_archive(sub_writer.finish_into_readable()?)?; + writer + }; + writer.merge_archive(sub_writer.finish_into_readable()?)?; + let _ = writer.finish_into_readable()?; + Ok(()) + } }