diff --git a/src/read.rs b/src/read.rs index 109d95f65..d55761159 100644 --- a/src/read.rs +++ b/src/read.rs @@ -359,7 +359,7 @@ fn find_data_start( // easily overflow a u16. block.file_name_length as u64 + block.extra_field_length as u64; let data_start = - data.header_start + mem::size_of::() as u64 + variable_fields_len; + data.header_start + size_of::() as u64 + variable_fields_len; // Set the value so we don't have to read it again. match data.data_start.set(data_start) { Ok(()) => (), @@ -497,22 +497,28 @@ impl ZipArchive { * assert_eq!(0, new_files[0].header_start); // Avoid this. */ - let new_initial_header_start = w.stream_position()?; + let first_new_file_header_start = w.stream_position()?; + /* Push back file header starts for all entries in the covered files. */ new_files.values_mut().try_for_each(|f| { /* This is probably the only really important thing to change. */ - f.header_start = f.header_start.checked_add(new_initial_header_start).ok_or( - ZipError::InvalidArchive("new header start from merge would have been too large"), - )?; + f.header_start = f + .header_start + .checked_add(first_new_file_header_start) + .ok_or(InvalidArchive( + "new header start from merge would have been too large", + ))?; /* This is only ever used internally to cache metadata lookups (it's not part of the * zip spec), and 0 is the sentinel value. */ - // f.central_header_start = 0; + f.central_header_start = 0; /* This is an atomic variable so it can be updated from another thread in the * implementation (which is good!). */ if let Some(old_data_start) = f.data_start.take() { - let new_data_start = old_data_start.checked_add(new_initial_header_start).ok_or( - ZipError::InvalidArchive("new data start from merge would have been too large"), - )?; + let new_data_start = old_data_start + .checked_add(first_new_file_header_start) + .ok_or(InvalidArchive( + "new data start from merge would have been too large", + ))?; f.data_start.get_or_init(|| new_data_start); } Ok::<_, ZipError>(()) @@ -1239,7 +1245,6 @@ pub(crate) fn central_header_to_zip_file( "A file can't start after its central-directory header", )); } - file.data_start.get_or_init(|| data_start); reader.seek(SeekFrom::Start(central_header_end))?; Ok(file) } diff --git a/src/types.rs b/src/types.rs index 31cb31f39..1e738b765 100644 --- a/src/types.rs +++ b/src/types.rs @@ -24,7 +24,7 @@ use crate::extra_fields::ExtraField; use crate::result::DateTimeRangeError; use crate::spec::is_dir; use crate::types::ffi::S_IFDIR; -use crate::CompressionMethod; +use crate::{CompressionMethod, ZIP64_BYTES_THR}; #[cfg(feature = "time")] use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; @@ -625,10 +625,10 @@ impl ZipFileData { extra_field: &[u8], ) -> Self where - S: Into>, + S: ToString, { let permissions = options.permissions.unwrap_or(0o100644); - let file_name: Box = name.into(); + let file_name: Box = name.to_string().into_boxed_str(); let file_name_raw: Box<[u8]> = file_name.bytes().collect(); let mut local_block = ZipFileData { system: System::Unix, @@ -778,12 +778,8 @@ impl ZipFileData { pub(crate) fn local_block(&self) -> ZipResult { let compressed_size: u32 = self.clamp_size_field(self.compressed_size); let uncompressed_size: u32 = self.clamp_size_field(self.uncompressed_size); - - let extra_block_len: usize = self - .zip64_extra_field_block() - .map(|block| block.full_size()) - .unwrap_or(0); - let extra_field_length: u16 = (self.extra_field_len() + extra_block_len) + let extra_field_length: u16 = self + .extra_field_len() .try_into() .map_err(|_| ZipError::InvalidArchive("Extra data field is too large"))?; @@ -805,7 +801,7 @@ impl ZipFileData { }) } - pub(crate) fn block(&self, zip64_extra_field_length: u16) -> ZipResult { + pub(crate) fn block(&self) -> ZipResult { let extra_field_len: u16 = self.extra_field_len().try_into().unwrap(); let central_extra_field_len: u16 = self.central_extra_field_len().try_into().unwrap(); let last_modified_time = self @@ -832,11 +828,9 @@ impl ZipFileData { .try_into() .unwrap(), file_name_length: self.file_name_raw.len().try_into().unwrap(), - extra_field_length: zip64_extra_field_length - .checked_add(extra_field_len + central_extra_field_len) - .ok_or(ZipError::InvalidArchive( - "Extra field length in central directory exceeds 64KiB", - ))?, + extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or( + ZipError::InvalidArchive("Extra field length in central directory exceeds 64KiB"), + )?, file_comment_length: self.file_comment.as_bytes().len().try_into().unwrap(), disk_number: 0, internal_file_attributes: 0, @@ -850,45 +844,12 @@ impl ZipFileData { } pub(crate) fn zip64_extra_field_block(&self) -> Option { - let uncompressed_size: Option = - if self.uncompressed_size >= spec::ZIP64_BYTES_THR || self.large_file { - Some(spec::ZIP64_BYTES_THR) - } else { - None - }; - let compressed_size: Option = - if self.compressed_size >= spec::ZIP64_BYTES_THR || self.large_file { - Some(spec::ZIP64_BYTES_THR) - } else { - None - }; - let header_start: Option = if self.header_start >= spec::ZIP64_BYTES_THR { - Some(spec::ZIP64_BYTES_THR) - } else { - None - }; - - let mut size: u16 = 0; - if uncompressed_size.is_some() { - size += mem::size_of::() as u16; - } - if compressed_size.is_some() { - size += mem::size_of::() as u16; - } - if header_start.is_some() { - size += mem::size_of::() as u16; - } - if size == 0 { - return None; - } - - Some(Zip64ExtraFieldBlock { - magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG, - size, - uncompressed_size, - compressed_size, - header_start, - }) + Zip64ExtraFieldBlock::maybe_new( + self.large_file, + self.uncompressed_size, + self.compressed_size, + self.header_start, + ) } } @@ -1002,6 +963,46 @@ pub(crate) struct Zip64ExtraFieldBlock { // u32: disk start number } +impl Zip64ExtraFieldBlock { + pub(crate) fn maybe_new( + large_file: bool, + uncompressed_size: u64, + compressed_size: u64, + header_start: u64, + ) -> Option { + let mut size: u16 = 0; + let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file { + size += mem::size_of::() as u16; + Some(uncompressed_size) + } else { + None + }; + let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file { + size += mem::size_of::() as u16; + Some(compressed_size) + } else { + None + }; + let header_start = if header_start >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(header_start) + } else { + None + }; + if size == 0 { + return None; + } + + Some(Zip64ExtraFieldBlock { + magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG, + size, + uncompressed_size, + compressed_size, + header_start, + }) + } +} + impl Zip64ExtraFieldBlock { pub fn full_size(&self) -> usize { assert!(self.size > 0); diff --git a/src/write.rs b/src/write.rs index 40dda2690..913219c3e 100644 --- a/src/write.rs +++ b/src/write.rs @@ -7,11 +7,12 @@ use crate::read::{ find_content, parse_single_extra_field, Config, ZipArchive, ZipFile, ZipFileReader, }; use crate::result::{ZipError, ZipResult}; -use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock}; +use crate::spec::{self, FixedSizeBlock, Pod, Zip32CDEBlock}; #[cfg(feature = "aes-crypto")] use crate::types::AesMode; use crate::types::{ - ffi, AesVendorVersion, DateTime, ZipFileData, ZipLocalEntryBlock, ZipRawValues, MIN_VERSION, + ffi, AesVendorVersion, DateTime, Zip64ExtraFieldBlock, ZipFileData, ZipLocalEntryBlock, + ZipRawValues, MIN_VERSION, }; use crate::write::ffi::S_IFLNK; #[cfg(any(feature = "_deflate-any", feature = "bzip2", feature = "zstd",))] @@ -304,7 +305,7 @@ impl ExtendedFileOptions { } }; Self::add_extra_data_unchecked(vec, header_id, data)?; - Self::validate_extra_data(vec, 0)?; + Self::validate_extra_data(vec, true)?; Ok(()) } } @@ -321,12 +322,12 @@ impl ExtendedFileOptions { Ok(()) } - fn validate_extra_data(data: &[u8], reserved: u64) -> ZipResult<()> { + fn validate_extra_data(data: &[u8], disallow_zip64: bool) -> ZipResult<()> { let len = data.len() as u64; if len == 0 { return Ok(()); } - if len + reserved > u16::MAX as u64 { + if len > u16::MAX as u64 { return Err(ZipError::Io(io::Error::new( io::ErrorKind::Other, "Extra-data field can't exceed u16::MAX bytes", @@ -358,7 +359,7 @@ impl ExtendedFileOptions { } data.seek(SeekFrom::Current(-2))?; } - parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, true)?; + parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, disallow_zip64)?; pos = data.position(); } Ok(()) @@ -406,6 +407,9 @@ impl<'a> arbitrary::Arbitrary<'a> for FileOptions<'a, ExtendedFileOptions> { .map_err(|_| arbitrary::Error::IncorrectFormat)?; Ok(core::ops::ControlFlow::Continue(())) })?; + ZipWriter::new(Cursor::new(Vec::new())) + .start_file("", options.clone()) + .map_err(|_| arbitrary::Error::IncorrectFormat)?; Ok(options) } } @@ -668,19 +672,13 @@ impl ZipWriter { let write_position = self.inner.get_plain().stream_position()?; let src_index = self.index_by_name(src_name)?; let src_data = &mut self.files[src_index]; - let data_start = src_data.data_start(); + let src_data_start = src_data.data_start(); + debug_assert!(src_data_start <= write_position); let mut compressed_size = src_data.compressed_size; - if compressed_size > (write_position - data_start) { - compressed_size = write_position - data_start; + if compressed_size > (write_position - src_data_start) { + compressed_size = write_position - src_data_start; src_data.compressed_size = compressed_size; } - let uncompressed_size = src_data.uncompressed_size; - - let raw_values = ZipRawValues { - crc32: src_data.crc32, - compressed_size, - uncompressed_size, - }; let mut reader = BufReader::new(ZipFileReader::Raw(find_content( src_data, self.inner.get_plain(), @@ -691,56 +689,46 @@ impl ZipWriter { self.inner .get_plain() .seek(SeekFrom::Start(write_position))?; - if src_data.extra_field.is_some() || src_data.central_extra_field.is_some() { - let mut options = FileOptions:: { - compression_method: src_data.compression_method, - compression_level: src_data.compression_level, - last_modified_time: src_data - .last_modified_time - .unwrap_or_else(DateTime::default_for_write), - permissions: src_data.unix_mode(), - large_file: src_data.large_file, - encrypt_with: None, - extended_options: ExtendedFileOptions { - extra_data: src_data.extra_field.clone().unwrap_or_default(), - central_extra_data: src_data.central_extra_field.clone().unwrap_or_default(), - }, - alignment: 1, - #[cfg(feature = "deflate-zopfli")] - zopfli_buffer_size: None, - }; - if let Some(perms) = src_data.unix_mode() { - options = options.unix_permissions(perms); - } - Self::normalize_options(&mut options); - self.start_entry(dest_name, options, Some(raw_values))?; - } else { - let mut options = FileOptions::<()> { - compression_method: src_data.compression_method, - compression_level: src_data.compression_level, - last_modified_time: src_data - .last_modified_time - .unwrap_or_else(DateTime::default_for_write), - permissions: src_data.unix_mode(), - large_file: src_data.large_file, - encrypt_with: None, - extended_options: (), - alignment: 1, - #[cfg(feature = "deflate-zopfli")] - zopfli_buffer_size: None, - }; - if let Some(perms) = src_data.unix_mode() { - options = options.unix_permissions(perms); - } - Self::normalize_options(&mut options); - self.start_entry(dest_name, options, Some(raw_values))?; + let mut new_data = src_data.clone(); + let dest_name_raw = dest_name.as_bytes(); + new_data.file_name = dest_name.into(); + new_data.file_name_raw = dest_name_raw.into(); + new_data.is_utf8 = !dest_name.is_ascii(); + new_data.header_start = write_position; + let extra_data_start = write_position + + size_of::() as u64 + + new_data.file_name_raw.len() as u64; + new_data.extra_data_start = Some(extra_data_start); + let mut data_start = extra_data_start; + if let Some(extra) = &src_data.extra_field { + data_start += extra.len() as u64; } - - self.writing_to_file = true; - self.writing_raw = true; - let result = self.write_all(©); + new_data.data_start.take(); + new_data.data_start.get_or_init(|| data_start); + new_data.central_header_start = 0; + let block = new_data.local_block()?; + let index = self.insert_file_data(new_data)?; + let result = (|| { + let plain_writer = self.inner.get_plain(); + plain_writer.write_all(block.as_bytes())?; + plain_writer.write_all(dest_name_raw)?; + let new_data = &self.files[index]; + if let Some(data) = &new_data.extra_field { + plain_writer.write_all(data)?; + } + debug_assert_eq!(data_start, plain_writer.stream_position()?); + self.writing_to_file = true; + plain_writer.write_all(©) + })(); self.ok_or_abort_file(result)?; - self.finish_file() + + // Copying will overwrite the central header + self.files + .values_mut() + .for_each(|file| file.central_header_start = 0); + + self.writing_to_file = false; + Ok(()) } /// Like `deep_copy_file`, but uses Path arguments. @@ -873,18 +861,15 @@ impl ZipWriter { } /// Start a new file for with the requested options. - fn start_entry( + fn start_entry( &mut self, name: S, options: FileOptions, raw_values: Option, - ) -> ZipResult<()> - where - S: Into> + ToOwned, - SToOwned: Into>, - { + ) -> ZipResult<()> { self.finish_file()?; + let header_start = self.inner.get_plain().stream_position()?; let raw_values = raw_values.unwrap_or(ZipRawValues { crc32: 0, compressed_size: 0, @@ -896,7 +881,13 @@ impl ZipWriter { None => vec![], }; let central_extra_data = options.extended_options.central_extra_data(); - + if let Some(zip64_block) = + Zip64ExtraFieldBlock::maybe_new(options.large_file, 0, 0, header_start) + { + let mut new_extra_data = zip64_block.serialize().into_vec(); + new_extra_data.append(&mut extra_data); + extra_data = new_extra_data; + } // Write AES encryption extra data. #[allow(unused_mut)] let mut aes_extra_data_start = 0; @@ -911,125 +902,110 @@ impl ZipWriter { aes_dummy_extra_data, )?; } - { - let header_start = self.inner.get_plain().stream_position()?; - let (compression_method, aes_mode) = match options.encrypt_with { - #[cfg(feature = "aes-crypto")] - Some(EncryptWith::Aes { mode, .. }) => ( - CompressionMethod::Aes, - Some((mode, AesVendorVersion::Ae2, options.compression_method)), - ), - _ => (options.compression_method, None), - }; - let mut file = ZipFileData::initialize_local_block( - name, - &options, - raw_values, - header_start, - None, - aes_extra_data_start, - compression_method, - aes_mode, - &extra_data, - ); - file.version_made_by = file.version_made_by.max(file.version_needed() as u8); - let block = file.local_block(); - let index = self.insert_file_data(file)?; - let writer = self.inner.get_plain(); - let result = block?.write(writer); - self.ok_or_abort_file(result)?; - let writer = self.inner.get_plain(); + let (compression_method, aes_mode) = match options.encrypt_with { + #[cfg(feature = "aes-crypto")] + Some(EncryptWith::Aes { mode, .. }) => ( + CompressionMethod::Aes, + Some((mode, AesVendorVersion::Ae2, options.compression_method)), + ), + _ => (options.compression_method, None), + }; + let header_end = header_start + + size_of::() as u64 + + name.to_string().as_bytes().len() as u64; + if options.alignment > 1 { + let extra_data_end = header_end + extra_data.len() as u64; + let align = options.alignment as u64; + let unaligned_header_bytes = extra_data_end % align; + if unaligned_header_bytes != 0 { + let mut pad_length = (align - unaligned_header_bytes) as usize; + while pad_length < 6 { + pad_length += align as usize; + } + // Add an extra field to the extra_data, per APPNOTE 4.6.11 + let mut pad_body = vec![0; pad_length - 4]; + debug_assert!(pad_body.len() >= 2); + [pad_body[0], pad_body[1]] = options.alignment.to_le_bytes(); + ExtendedFileOptions::add_extra_data_unchecked( + &mut extra_data, + 0xa11e, + pad_body.into_boxed_slice(), + )?; + debug_assert_eq!((extra_data.len() as u64 + header_end) % align, 0); + } + } + let extra_data_len = extra_data.len(); + if let Some(data) = central_extra_data { + if extra_data_len + data.len() > u16::MAX as usize { + return Err(InvalidArchive( + "Extra data and central extra data must be less than 64KiB when combined", + )); + } + ExtendedFileOptions::validate_extra_data(data, true)?; + } + let mut file = ZipFileData::initialize_local_block( + name, + &options, + raw_values, + header_start, + None, + aes_extra_data_start, + compression_method, + aes_mode, + &extra_data, + ); + file.version_made_by = file.version_made_by.max(file.version_needed() as u8); + file.extra_data_start = Some(header_end); + let index = self.insert_file_data(file)?; + self.writing_to_file = true; + let result: ZipResult<()> = (|| { + ExtendedFileOptions::validate_extra_data(&extra_data, false)?; let file = &mut self.files[index]; + let block = file.local_block()?; + let writer = self.inner.get_plain(); + block.write(writer)?; // file name writer.write_all(&file.file_name_raw)?; - let zip64_start = writer.stream_position()?; - if file.large_file { - write_local_zip64_extra_field(writer, file)?; - } - let header_end = writer.stream_position()?; - file.extra_data_start = Some(header_end); - let mut extra_data_end = header_end + extra_data.len() as u64; - if options.alignment > 1 { - let align = options.alignment as u64; - let unaligned_header_bytes = extra_data_end % align; - if unaligned_header_bytes != 0 { - let mut pad_length = (align - unaligned_header_bytes) as usize; - while pad_length < 6 { - pad_length += align as usize; - } - // Add an extra field to the extra_data, per APPNOTE 4.6.11 - let mut pad_body = vec![0; pad_length - 4]; - debug_assert!(pad_body.len() >= 2); - [pad_body[0], pad_body[1]] = options.alignment.to_le_bytes(); - ExtendedFileOptions::add_extra_data_unchecked( - &mut extra_data, - 0xa11e, - pad_body.into_boxed_slice(), - )?; - } - } - let extra_data_len = extra_data.len(); if extra_data_len > 0 { - let result = (|| { - ExtendedFileOptions::validate_extra_data( - &extra_data, - header_end - zip64_start, - )?; - writer.write_all(&extra_data)?; - extra_data_end = writer.stream_position()?; - Ok(()) - })(); - if let Err(e) = result { - let _ = self.abort_file(); - return Err(e); - } - debug_assert_eq!(extra_data_end % (options.alignment.max(1) as u64), 0); - self.stats.start = extra_data_end; + writer.write_all(&extra_data)?; file.extra_field = Some(extra_data.into()); - } else { - self.stats.start = extra_data_end; } - if let Some(data) = central_extra_data { - let validation_result = - ExtendedFileOptions::validate_extra_data(data, extra_data_end - zip64_start); - if let Err(e) = validation_result { - let _ = self.abort_file(); - return Err(e); - } - file.central_extra_field = Some(data.clone()); + Ok(()) + })(); + self.ok_or_abort_file(result)?; + let writer = self.inner.get_plain(); + self.stats.start = writer.stream_position()?; + match options.encrypt_with { + #[cfg(feature = "aes-crypto")] + Some(EncryptWith::Aes { mode, password }) => { + let aeswriter = AesWriter::new( + mem::replace(&mut self.inner, Closed).unwrap(), + mode, + password.as_bytes(), + )?; + self.inner = Storer(MaybeEncrypted::Aes(aeswriter)); } - match options.encrypt_with { - #[cfg(feature = "aes-crypto")] - Some(EncryptWith::Aes { mode, password }) => { - let aeswriter = AesWriter::new( - mem::replace(&mut self.inner, Closed).unwrap(), - mode, - password.as_bytes(), - )?; - self.inner = Storer(MaybeEncrypted::Aes(aeswriter)); - } - Some(EncryptWith::ZipCrypto(keys, ..)) => { - let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { - writer: mem::replace(&mut self.inner, Closed).unwrap(), - buffer: vec![], - keys, - }; - let crypto_header = [0u8; 12]; - - zipwriter.write_all(&crypto_header)?; - self.stats.start = zipwriter.writer.stream_position()?; - self.inner = Storer(MaybeEncrypted::ZipCrypto(zipwriter)); - } - None => {} + Some(EncryptWith::ZipCrypto(keys, ..)) => { + let mut zipwriter = crate::zipcrypto::ZipCryptoWriter { + writer: mem::replace(&mut self.inner, Closed).unwrap(), + buffer: vec![], + keys, + }; + self.stats.start = zipwriter.writer.stream_position()?; + // crypto_header is counted as part of the data + let crypto_header = [0u8; 12]; + let result = zipwriter.write_all(&crypto_header); + self.ok_or_abort_file(result)?; + self.inner = Storer(MaybeEncrypted::ZipCrypto(zipwriter)); } - - debug_assert!(file.data_start.get().is_none()); - file.data_start.get_or_init(|| self.stats.start); - self.writing_to_file = true; - self.stats.bytes_written = 0; - self.stats.hasher = Hasher::new(); + None => {} } + let file = &mut self.files[index]; + debug_assert!(file.data_start.get().is_none()); + file.data_start.get_or_init(|| self.stats.start); + self.stats.bytes_written = 0; + self.stats.hasher = Hasher::new(); Ok(()) } @@ -1152,15 +1128,11 @@ impl ZipWriter { /// same name as a file already in the archive. /// /// The data should be written using the [`Write`] implementation on this [`ZipWriter`] - pub fn start_file( + pub fn start_file( &mut self, name: S, mut options: FileOptions, - ) -> ZipResult<()> - where - S: Into> + ToOwned, - SToOwned: Into>, - { + ) -> ZipResult<()> { Self::normalize_options(&mut options); let make_new_self = self.inner.prepare_next_writer( options.compression_method, @@ -1286,11 +1258,11 @@ impl ZipWriter { /// Ok(()) /// } /// ``` - pub fn raw_copy_file_rename(&mut self, mut file: ZipFile, name: S) -> ZipResult<()> - where - S: Into> + ToOwned, - SToOwned: Into>, - { + pub fn raw_copy_file_rename( + &mut self, + mut file: ZipFile, + name: S, + ) -> ZipResult<()> { let mut options = SimpleFileOptions::default() .large_file(file.compressed_size().max(file.size()) > spec::ZIP64_BYTES_THR) .last_modified_time( @@ -1314,8 +1286,7 @@ impl ZipWriter { self.writing_raw = true; io::copy(&mut file.take_raw_reader()?, self)?; - - Ok(()) + self.finish_file() } /// Like `raw_copy_file_to_path`, but uses Path arguments. @@ -1425,17 +1396,12 @@ impl ZipWriter { /// implementations may materialize a symlink as a regular file, possibly with the /// content incorrectly set to the symlink target. For maximum portability, consider /// storing a regular file instead. - pub fn add_symlink( + pub fn add_symlink( &mut self, name: N, target: T, mut options: FileOptions, - ) -> ZipResult<()> - where - N: Into> + ToOwned, - NToOwned: Into>, - T: Into>, - { + ) -> ZipResult<()> { if options.permissions.is_none() { options.permissions = Some(0o777); } @@ -1446,7 +1412,7 @@ impl ZipWriter { self.start_entry(name, options, None)?; self.writing_to_file = true; - let result = self.write_all(target.into().as_bytes()); + let result = self.write_all(target.to_string().as_bytes()); self.ok_or_abort_file(result)?; self.writing_raw = false; self.finish_file()?; @@ -1474,8 +1440,8 @@ impl ZipWriter { let mut central_start = self.write_central_and_footer()?; let writer = self.inner.get_plain(); let footer_end = writer.stream_position()?; - let file_end = writer.seek(SeekFrom::End(0))?; - if footer_end < file_end { + let archive_end = writer.seek(SeekFrom::End(0))?; + if footer_end < archive_end { // Data from an aborted file is past the end of the footer. // Overwrite the magic so the footer is no longer valid. @@ -1564,7 +1530,9 @@ impl ZipWriter { let mut dest_data = self.files[src_index].to_owned(); dest_data.file_name = dest_name.to_string().into(); dest_data.file_name_raw = dest_name.to_string().into_bytes().into(); + dest_data.central_header_start = 0; self.insert_file_data(dest_data)?; + Ok(()) } @@ -1855,12 +1823,7 @@ fn update_aes_extra_data(writer: &mut W, file: &mut ZipFileData let aes_extra_data_start = file.aes_extra_data_start as usize; let extra_field = Arc::get_mut(file.extra_field.as_mut().unwrap()).unwrap(); - extra_field - .splice( - aes_extra_data_start..(aes_extra_data_start + buf.len()), - buf, - ) - .count(); + extra_field[aes_extra_data_start..aes_extra_data_start + buf.len()].copy_from_slice(&buf); Ok(()) } @@ -1887,16 +1850,10 @@ fn update_local_file_header(writer: &mut T, file: &ZipFileData) } fn write_central_directory_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { - // buffer zip64 extra field to determine its variable length - let mut zip64_extra_field = [0; 28]; - let zip64_extra_field_length = - write_central_zip64_extra_field(&mut zip64_extra_field.as_mut(), file)?; - let block = file.block(zip64_extra_field_length)?; + let block = file.block()?; block.write(writer)?; // file name writer.write_all(&file.file_name_raw)?; - // zip64 extra field - writer.write_all(&zip64_extra_field[..zip64_extra_field_length as usize])?; // extra field if let Some(extra_field) = &file.extra_field { writer.write_all(extra_field)?; @@ -1910,19 +1867,6 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) Ok(()) } -fn write_local_zip64_extra_field(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { - // This entry in the Local header MUST include BOTH original - // and compressed file size fields. - let Some(block) = file.zip64_extra_field_block() else { - return Err(InvalidArchive( - "Attempted to write a ZIP64 extra field for a file that's within zip32 limits", - )); - }; - let block = block.serialize(); - writer.write_all(&block)?; - Ok(()) -} - fn update_local_zip64_extra_field( writer: &mut T, file: &ZipFileData, @@ -1941,22 +1885,6 @@ fn update_local_zip64_extra_field( Ok(()) } -fn write_central_zip64_extra_field(writer: &mut T, file: &ZipFileData) -> ZipResult { - // The order of the fields in the zip64 extended - // information record is fixed, but the fields MUST - // only appear if the corresponding Local or Central - // directory record field is set to 0xFFFF or 0xFFFFFFFF. - match file.zip64_extra_field_block() { - None => Ok(0), - Some(block) => { - let block = block.serialize(); - writer.write_all(&block)?; - let len: u16 = block.len().try_into().unwrap(); - Ok(len) - } - } -} - #[cfg(not(feature = "unreserved"))] const EXTRA_FIELD_MAPPING: [u16; 43] = [ 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016, 0x0017, @@ -3498,4 +3426,197 @@ mod test { assert!(archive.comment().starts_with(&[33])); Ok(()) } + + #[test] + #[cfg(feature = "bzip2")] + fn fuzz_crash_2024_07_17() -> ZipResult<()> { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let options = FileOptions { + compression_method: CompressionMethod::Bzip2, + compression_level: None, + last_modified_time: DateTime::from_date_and_time(2095, 2, 16, 21, 0, 1)?, + permissions: Some(84238341), + large_file: true, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![117, 99, 6, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 2, 0, 0, 0].into(), + central_extra_data: vec![].into(), + }, + alignment: 65535, + ..Default::default() + }; + writer.start_file_from_path("", options)?; + //writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + writer.deep_copy_file_from_path("", "copy")?; + let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + Ok(()) + } + + #[test] + fn fuzz_crash_2024_07_19() -> ZipResult<()> { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let options = FileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::from_date_and_time(1980, 6, 1, 0, 34, 47)?, + permissions: None, + large_file: true, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![].into(), + central_extra_data: vec![].into(), + }, + alignment: 45232, + ..Default::default() + }; + writer.add_directory_from_path("", options)?; + writer.deep_copy_file_from_path("/", "")?; + writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + writer.deep_copy_file_from_path("", "copy")?; + let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + Ok(()) + } + + #[test] + #[cfg(feature = "aes-crypto")] + fn fuzz_crash_2024_07_19a() -> ZipResult<()> { + use crate::write::EncryptWith::Aes; + use crate::AesMode::Aes128; + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let options = FileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::from_date_and_time(2107, 6, 5, 13, 0, 21)?, + permissions: None, + large_file: true, + encrypt_with: Some(Aes { + mode: Aes128, + password: "", + }), + extended_options: ExtendedFileOptions { + extra_data: vec![3, 0, 4, 0, 209, 53, 53, 8, 2, 61, 0, 0].into(), + central_extra_data: vec![].into(), + }, + alignment: 65535, + ..Default::default() + }; + writer.start_file_from_path("", options)?; + let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + Ok(()) + } + + #[test] + fn fuzz_crash_2024_07_20() -> ZipResult<()> { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(true); + let options = FileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::from_date_and_time(2041, 8, 2, 19, 38, 0)?, + permissions: None, + large_file: false, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![].into(), + central_extra_data: vec![].into(), + }, + alignment: 0, + ..Default::default() + }; + writer.add_directory_from_path("\0\0\0\0\0\0\07黻", options)?; + let sub_writer = { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let options = FileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::default(), + permissions: None, + large_file: false, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![].into(), + central_extra_data: vec![].into(), + }, + alignment: 4, + ..Default::default() + }; + writer.add_directory_from_path("\0\0\0黻", options)?; + writer = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + writer.abort_file()?; + let options = FileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::from_date_and_time(1980, 1, 1, 0, 7, 0)?, + permissions: Some(2663103419), + large_file: false, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![].into(), + central_extra_data: vec![].into(), + }, + alignment: 32256, + ..Default::default() + }; + writer.add_directory_from_path("\0", options)?; + writer = ZipWriter::new_append(writer.finish()?)?; + writer + }; + writer.merge_archive(sub_writer.finish_into_readable()?)?; + let _ = ZipWriter::new_append(writer.finish_into_readable()?.into_inner())?; + Ok(()) + } + + #[test] + fn fuzz_crash_2024_07_21() -> ZipResult<()> { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + let sub_writer = { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.add_directory_from_path( + "", + FileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::from_date_and_time(2105, 8, 1, 15, 0, 0)?, + permissions: None, + large_file: false, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![].into(), + central_extra_data: vec![].into(), + }, + alignment: 0, + ..Default::default() + }, + )?; + writer.abort_file()?; + let mut writer = ZipWriter::new_append(writer.finish()?)?; + writer.add_directory_from_path( + "", + FileOptions { + compression_method: Stored, + compression_level: None, + last_modified_time: DateTime::default(), + permissions: None, + large_file: false, + encrypt_with: None, + extended_options: ExtendedFileOptions { + extra_data: vec![].into(), + central_extra_data: vec![].into(), + }, + alignment: 16, + ..Default::default() + }, + )?; + ZipWriter::new_append(writer.finish()?)? + }; + writer.merge_archive(sub_writer.finish_into_readable()?)?; + let writer = ZipWriter::new_append(writer.finish()?)?; + let _ = writer.finish_into_readable()?; + + Ok(()) + } }