diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index 3db4d5f7c..388a5b5a8 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -9,12 +9,12 @@ use crate::result::{ZipError, ZipResult, invalid, invalid_archive_const}; use crate::types::AesVendorVersion; use crate::unstable::LittleEndianReadExt; -#[derive(Copy, Clone)] -#[repr(packed, C)] -pub(crate) struct AexEncryption { - pub(crate) version: u16, - aes_mode: u8, - compression_method: u16, +#[derive(Copy, Clone, Debug)] +pub struct AexEncryption { + pub(crate) aes_vendor_version: AesVendorVersion, + pub(crate) aes_mode: AesMode, + pub(crate) compression_method: CompressionMethod, + pub(crate) aes_extra_field_start: Option, } impl AexEncryption { @@ -31,14 +31,15 @@ impl AexEncryption { #[inline] pub(crate) fn new( - version: AesVendorVersion, + aes_vendor_version: AesVendorVersion, aes_mode: AesMode, compression_method: CompressionMethod, ) -> Self { Self { - version: version.as_u16(), - aes_mode: aes_mode.as_u8(), - compression_method: compression_method.serialize_to_u16(), + aes_vendor_version, + aes_mode, + compression_method, + aes_extra_field_start: None, } } @@ -50,10 +51,10 @@ impl AexEncryption { } pub fn write_data(self, writer: &mut T) -> ZipResult<()> { - writer.write_all(&u16::to_le_bytes(self.version))?; + writer.write_all(&self.aes_vendor_version.as_u16().to_le_bytes())?; writer.write_all(&u16::to_le_bytes(Self::VENDOR_ID))?; - writer.write_all(&u8::to_le_bytes(self.aes_mode))?; - writer.write_all(&u16::to_le_bytes(self.compression_method))?; + writer.write_all(&self.aes_mode.as_u8().to_le_bytes())?; + writer.write_all(&self.compression_method.serialize_to_u16().to_le_bytes())?; Ok(()) } diff --git a/src/extra_fields/extra_field.rs b/src/extra_fields/extra_field.rs new file mode 100644 index 000000000..83868efb2 --- /dev/null +++ b/src/extra_fields/extra_field.rs @@ -0,0 +1,328 @@ +//! Code related to the `ExtraField` enum + +use crate::extra_fields::AexEncryption; +use crate::extra_fields::CustomExtraField; +use crate::extra_fields::ExtendedTimestamp; +use crate::extra_fields::Ntfs; +use crate::extra_fields::UnicodeExtraField; +use crate::extra_fields::UsedExtraField; +use crate::extra_fields::Zip64ExtendedInformation; +use crate::format::flags::ZipFlags; +use crate::result::ZipResult; +use crate::result::invalid; +use crate::spec::ZipEntryBlock; +use crate::types::ZipFileData; +use crate::unstable::LittleEndianReadExt; +use core::mem; +use std::io::ErrorKind; +use std::io::{Cursor, Read, Write}; + +/// contains one extra field +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum ExtraField { + /// NTFS extra field + Ntfs(Ntfs), + + /// extended timestamp, as described in + ExtendedTimestamp(ExtendedTimestamp), + + /// AeX Encryption + AeXEncryption(AexEncryption), + /// Zip64 Information + Zip64ExtendedInformation { + /// uncompressed size + uncompressed_size: Option, + /// compressed size + compressed_size: Option, + /// header start + header_start: Option, + }, + /// Unicode Comment + UnicodeComment(UnicodeExtraField), + /// UnicodePath + UnicodePath(UnicodeExtraField), + /// Data Stream Alignment + DataStreamAlignment(u64), + /// Custom extra field + Custom(CustomExtraField), +} + +/// Extra fields list +#[derive(Debug, Clone, Default)] +pub struct ExtraFields { + pub(crate) inner: Vec, +} + +impl ExtraFields { + pub(crate) fn parse(buff: &[u8], block: &B) -> ZipResult { + let mut reader = Cursor::new(buff); + let mut extra_fields = Vec::new(); + while (reader.position() as usize) < buff.len() { + let parsed_extra_field = ExtraField::parse(&mut reader, block)?; + let Some(parsed_extra_field) = parsed_extra_field else { + break; + }; + extra_fields.push(parsed_extra_field); + } + Ok(Self { + inner: extra_fields, + }) + } + + pub(crate) fn local_extra_fields_mut(&mut self) -> impl Iterator { + self.inner.iter_mut() + } + + pub(crate) fn local_extra_fields(&self) -> impl Iterator { + self.inner.iter().filter(|ef| match ef { + ExtraField::Custom(cef) => !cef.central_only, + _ => true, + }) + } + + pub(crate) fn central_extra_fields(&self) -> impl Iterator { + // data alignment is local only + self.inner + .iter() + .filter(|ef| !matches!(ef, ExtraField::DataStreamAlignment(_))) + } +} + +impl ExtraField { + pub(crate) fn parse( + reader: &mut R, + file: &B, + ) -> ZipResult> { + let extra_field_header_id = match reader.read_u16_le() { + Ok(value) => value, + Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e.into()), + }; + let decoded_extra_field = UsedExtraField::try_from(extra_field_header_id); + let len = match decoded_extra_field { + Ok(known_field) => match reader.read_u16_le() { + Ok(len) => len, + Err(e) if e.kind() == ErrorKind::UnexpectedEof => { + return Err(invalid!("Extra field {} header truncated", known_field)); + } + Err(e) => return Err(e.into()), + }, + Err(()) => { + match reader.read_u16_le() { + Ok(len) => len, + Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None), // early return, most likely a padding + Err(_e) => { + // Consume remaining bytes to avoid infinite loop in caller + let _ = std::io::copy(reader, &mut std::io::sink()); + return Ok(None); + } + } + } + }; + let parsed_extra_field = match decoded_extra_field { + // Zip64 extended information extra field + Ok(UsedExtraField::Zip64ExtendedInfo) => { + let (new_uncomp, new_comp, new_head) = Zip64ExtendedInformation::parse( + reader, + len, + file.get_uncompressed_size(), + file.get_compressed_size(), + file.get_header_start(), + )?; + ExtraField::Zip64ExtendedInformation { + uncompressed_size: Some(new_uncomp), + compressed_size: Some(new_comp), + header_start: Some(new_head), + } + } + Ok(UsedExtraField::Ntfs) => { + // NTFS extra field + ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?) + } + Ok(UsedExtraField::AeXEncryption) => { + // AES + let (new_aes_enc, inner_compression) = AexEncryption::parse(reader, len)?; + ExtraField::AeXEncryption(AexEncryption::new( + new_aes_enc.1, + new_aes_enc.0, + inner_compression, + )) + } + Ok(UsedExtraField::ExtendedTimestamp) => { + ExtraField::ExtendedTimestamp(ExtendedTimestamp::try_from_reader(reader, len)?) + } + Ok(UsedExtraField::UnicodeComment) => { + // Info-ZIP Unicode Comment Extra Field + // APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt + let unicode = UnicodeExtraField::try_from_reader(reader, len)?; + ExtraField::UnicodeComment(unicode) + } + Ok(UsedExtraField::UnicodePath) => { + // Info-ZIP Unicode Path Extra Field + // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt + let unicode = UnicodeExtraField::try_from_reader(reader, len)?; + ExtraField::UnicodePath(unicode) + } + _ => { + let mut buf = vec![0u8; len as usize]; + if let Err(e) = reader.read_exact(&mut buf) { + if e.kind() == ErrorKind::UnexpectedEof { + return Err(invalid!("Extra field content truncated")); + } + return Err(e.into()); + } + ExtraField::Custom(CustomExtraField::new(false, extra_field_header_id, &buf)) + // Other fields are ignored + } + }; + Ok(Some(parsed_extra_field)) + } + + pub(crate) fn size(&self, is_local_header: bool) -> usize { + match self { + // Zip64 extended information extra field + ExtraField::Zip64ExtendedInformation { + uncompressed_size, + compressed_size, + header_start, + } => { + let mut size = mem::size_of::() + mem::size_of::(); + if uncompressed_size.is_some() { + size += mem::size_of::(); + } + if compressed_size.is_some() { + size += mem::size_of::(); + } + if !is_local_header && header_start.is_some() { + size += mem::size_of::(); + } + size + } + ExtraField::Ntfs(_ntfs) => { + // NTFS extra field + 0 + } + ExtraField::AeXEncryption { .. } => AexEncryption::FULL_SIZE, + ExtraField::ExtendedTimestamp(_extended_timestamp) => { + // nothing to do + 0 + } + ExtraField::UnicodeComment(unicode_comment) => unicode_comment.full_size(), + ExtraField::UnicodePath(unicode_path) => unicode_path.full_size(), + ExtraField::Custom(custom) => custom.len_with_header(), + _ => 0, + } + } + + pub(crate) fn write(&self, writer: &mut W, is_local_header: bool) -> ZipResult<()> { + match self { + // Zip64 extended information extra field + ExtraField::Zip64ExtendedInformation { + compressed_size, + uncompressed_size, + header_start, + } => { + let magic = UsedExtraField::Zip64ExtendedInfo.as_u16(); + writer.write_all(&magic.to_le_bytes())?; + let size = self.size(is_local_header); + let size = size - mem::size_of::() - mem::size_of::(); + let size = size as u16; + writer.write_all(&size.to_le_bytes())?; + if let Some(uncomp_size) = uncompressed_size { + writer.write_all(&uncomp_size.to_le_bytes())?; + } + if let Some(comp_size) = compressed_size { + writer.write_all(&comp_size.to_le_bytes())?; + } + if !is_local_header && let Some(head_start) = header_start { + writer.write_all(&head_start.to_le_bytes())?; + } + } + ExtraField::AeXEncryption(aex) => { + aex.write(writer)?; + } + ExtraField::Custom(custom) => { + custom.write(writer)?; + } + ExtraField::UnicodeComment(unicode_comment) => { + let magic = UsedExtraField::UnicodeComment.as_u16(); + writer.write_all(&magic.to_le_bytes())?; + unicode_comment.write(writer)?; + } + ExtraField::UnicodePath(unicode_path) => { + let magic = UsedExtraField::UnicodePath.as_u16(); + writer.write_all(&magic.to_le_bytes())?; + unicode_path.write(writer)?; + } + _ => { + // nothing to do + } + } + Ok(()) + } +} + +impl ZipFileData { + pub(crate) fn apply_extra_fields(&mut self, file_name_raw: &mut Vec) -> ZipResult<()> { + for one_extra_field in &self.extra_fields.inner { + match one_extra_field { + // Zip64 extended information extra field + ExtraField::Zip64ExtendedInformation { + uncompressed_size, + compressed_size, + header_start, + } => { + self.large_file = true; + if let Some(uncomp_size) = *uncompressed_size { + self.uncompressed_size = uncomp_size; + } + if let Some(comp_size) = *compressed_size { + self.compressed_size = comp_size; + } + if let Some(head_start) = *header_start { + self.header_start = head_start; + } + } + ExtraField::AeXEncryption(AexEncryption { + aes_mode, + aes_vendor_version, + compression_method, + .. + }) => { + self.aes_mode = Some((*aes_mode, *aes_vendor_version)); + self.compression_method = *compression_method; + } + ExtraField::UnicodeComment(unicode) => { + // Info-ZIP Unicode Comment Extra Field + // APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt + // If the CRC check fails, this Unicode Comment extra field SHOULD be ignored and + // the File Comment field in the header SHOULD be used instead. + // Check if the comment is UTF-8 + if unicode.is_crc32_valid(self.file_comment.as_bytes()) + && let Ok(comment) = String::from_utf8(unicode.content.to_vec()) + { + self.file_comment = comment.into_boxed_str(); + } + } + #[allow(clippy::collapsible_match)] + ExtraField::UnicodePath(unicode) => { + // Info-ZIP Unicode Path Extra Field + // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt + // If the CRC check fails, this UTF-8 Path Extra Field SHOULD be ignored and + // the File Name field in the header SHOULD be used instead. + if unicode.is_crc32_valid(file_name_raw) + && std::str::from_utf8(&unicode.content).is_ok() + { + *file_name_raw = unicode.content.to_vec(); + self.flags |= ZipFlags::LanguageEncoding.as_u16(); + } + } + _ => { + // nothing to do + } + } + } + Ok(()) + } +} diff --git a/src/extra_fields/mod.rs b/src/extra_fields/mod.rs index 3639503dd..f2f262fe7 100644 --- a/src/extra_fields/mod.rs +++ b/src/extra_fields/mod.rs @@ -3,9 +3,11 @@ use crate::result::ZipResult; use crate::result::invalid; use core::fmt::Display; +use std::io::Write; mod aex_encryption; mod extended_timestamp; +mod extra_field; mod ntfs; mod zip64_extended_information; mod zipinfo_utf8; @@ -15,6 +17,7 @@ pub(crate) use zip64_extended_information::Zip64ExtendedInformation; // re-export pub use extended_timestamp::ExtendedTimestamp; +pub use extra_field::{ExtraField, ExtraFields}; pub use ntfs::Ntfs; pub use zipinfo_utf8::UnicodeExtraField; @@ -32,17 +35,6 @@ pub struct CentralHeaderVersion; impl ExtraFieldVersion for LocalHeaderVersion {} impl ExtraFieldVersion for CentralHeaderVersion {} -/// Contains one extra field. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum ExtraField { - /// NTFS extra field - Ntfs(Ntfs), - - /// extended timestamp, as described in - ExtendedTimestamp(ExtendedTimestamp), -} - /// Internal extra-field identifiers (`u16` tags) recognized by this crate. /// /// This enum is crate-private and used for matching/dispatch on raw ZIP extra @@ -189,7 +181,7 @@ pub struct CustomExtraField { /// If true, this field will be included in the central directory entry but not the local file header. pub(crate) central_only: bool, /// Header ID of the extra field - header_id: u16, + pub(crate) header_id: u16, /// Data of the extra field data: Box<[u8]>, } @@ -232,15 +224,12 @@ impl CustomExtraField { size_of::() + size_of::() + size } - pub(crate) fn serialize(&self) -> Vec { - let mut out = Vec::with_capacity(4 + self.data.len()); - - out.extend_from_slice(&self.header_id.to_le_bytes()); + pub(crate) fn write(&self, write: &mut W) -> ZipResult<()> { + write.write_all(&self.header_id.to_le_bytes())?; let size = self.data.len() as u16; - out.extend_from_slice(&size.to_le_bytes()); - out.extend_from_slice(&self.data); - - out + write.write_all(&size.to_le_bytes())?; + write.write_all(&self.data)?; + Ok(()) } } diff --git a/src/extra_fields/zip64_extended_information.rs b/src/extra_fields/zip64_extended_information.rs index da501c2ad..6d5b72c91 100644 --- a/src/extra_fields/zip64_extended_information.rs +++ b/src/extra_fields/zip64_extended_information.rs @@ -25,9 +25,9 @@ use crate::{ pub(crate) struct Zip64ExtendedInformation { /// The local header does not contains any `header_start` is_local_header: bool, - uncompressed_size: Option, - compressed_size: Option, - header_start: Option, + pub(crate) uncompressed_size: Option, + pub(crate) compressed_size: Option, + pub(crate) header_start: Option, // Not used field // disk_start: Option } @@ -35,14 +35,6 @@ pub(crate) struct Zip64ExtendedInformation { impl Zip64ExtendedInformation { pub(crate) const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo; - pub(crate) fn new_local(is_large_file: bool) -> Option { - if is_large_file { - Self::local_header(true, u64::MAX, u64::MAX) - } else { - None - } - } - /// This entry in the Local header MUST include BOTH original and compressed file size fields /// If the user is using `is_large_file` when the file is not large we force the zip64 extra field pub(crate) fn local_header( @@ -112,11 +104,6 @@ impl Zip64ExtendedInformation { }) } - /// Get the full size of the block - pub(crate) fn full_size(&self) -> usize { - self.size() + mem::size_of::() + mem::size_of::() - } - pub(crate) fn size(&self) -> usize { let mut size = 0; if self.uncompressed_size.is_some() { @@ -166,12 +153,13 @@ impl Zip64ExtendedInformation { pub(crate) fn parse( reader: &mut R, len: u16, - uncompressed_size: u64, - compressed_size: u64, - header_start: u64, + uncompressed_size: u32, + compressed_size: u32, + header_start: Option, ) -> ZipResult<(u64, u64, u64)> { let mut consumed_len = 0; - let new_uncompressed_size = if len >= 24 || uncompressed_size == ZIP64_BYTES_THR { + let new_uncompressed_size = if len >= 24 || u64::from(uncompressed_size) == ZIP64_BYTES_THR + { let new_uncompressed_size = match reader.read_u64_le() { Ok(v) => v, Err(e) if e.kind() == ErrorKind::UnexpectedEof => { @@ -182,10 +170,10 @@ impl Zip64ExtendedInformation { consumed_len += mem::size_of::(); new_uncompressed_size } else { - uncompressed_size + uncompressed_size.into() }; - let new_compressed_size = if len >= 24 || compressed_size == ZIP64_BYTES_THR { + let new_compressed_size = if len >= 24 || u64::from(compressed_size) == ZIP64_BYTES_THR { let new_compressed_size = match reader.read_u64_le() { Ok(v) => v, Err(e) if e.kind() == ErrorKind::UnexpectedEof => { @@ -196,10 +184,10 @@ impl Zip64ExtendedInformation { consumed_len += mem::size_of::(); new_compressed_size } else { - compressed_size + compressed_size.into() }; - let new_header_start = if len >= 24 || header_start == ZIP64_BYTES_THR { + let new_header_start = if len >= 24 { let new_header_start = match reader.read_u64_le() { Ok(v) => v, Err(e) if e.kind() == ErrorKind::UnexpectedEof => { @@ -210,7 +198,23 @@ impl Zip64ExtendedInformation { consumed_len += mem::size_of::(); new_header_start } else { - header_start + if let Some(header_start) = header_start { + if u64::from(header_start) == ZIP64_BYTES_THR { + let new_header_start = match reader.read_u64_le() { + Ok(v) => v, + Err(e) if e.kind() == ErrorKind::UnexpectedEof => { + return Err(invalid!("ZIP64 extra field truncated")); + } + Err(e) => return Err(e.into()), + }; + consumed_len += mem::size_of::(); + new_header_start + } else { + header_start.into() + } + } else { + 0 + } }; let Some(leftover_len) = (len as usize).checked_sub(consumed_len) else { diff --git a/src/extra_fields/zipinfo_utf8.rs b/src/extra_fields/zipinfo_utf8.rs index d5b136828..5aed3a645 100644 --- a/src/extra_fields/zipinfo_utf8.rs +++ b/src/extra_fields/zipinfo_utf8.rs @@ -1,7 +1,7 @@ use crate::result::{ZipResult, invalid}; use crate::unstable::LittleEndianReadExt; -use core::mem::size_of; -use std::io::Read; +use core::mem; +use std::io::{Read, Write}; /// Info-ZIP Unicode Path Extra Field (0x7075) or Unicode Comment Extra Field (0x6375), as /// specified in APPNOTE 4.6.8 and 4.6.9 @@ -30,25 +30,48 @@ impl UnicodeExtraField { Ok(self.content) } + /// Check if the crc32 is valid pub(crate) fn is_crc32_valid(&self, ascii_field: &[u8]) -> bool { let computed_crc32 = crc32fast::hash(ascii_field); self.crc32 == computed_crc32 } -} -impl UnicodeExtraField { pub(crate) fn try_from_reader(reader: &mut R, len: u16) -> ZipResult { // Read and discard version byte reader.read_exact(&mut [0u8])?; let crc32 = reader.read_u32_le()?; let content_len = (len as usize) - .checked_sub(size_of::() + size_of::()) + .checked_sub(mem::size_of::() + mem::size_of::()) .ok_or(invalid!("Unicode extra field is too small"))?; let mut content = vec![0u8; content_len].into_boxed_slice(); reader.read_exact(&mut content)?; Ok(Self { crc32, content }) } + + pub(crate) fn full_size(&self) -> usize { + mem::size_of::() // magic + + mem::size_of::() // length + + self.size() + } + + pub(crate) fn size(&self) -> usize { + mem::size_of::() // version + + mem::size_of::() // crc32 + + self.content.len() // content + } + + /// Write the Unicode extra field. Magic header should already be written + pub(crate) fn write(&self, writer: &mut W) -> ZipResult<()> { + // Magic header should already be written + let size = self.size() as u16; + writer.write_all(&size.to_le_bytes())?; + let version = 1u8; + writer.write_all(&version.to_le_bytes())?; + writer.write_all(&self.crc32.to_le_bytes())?; + writer.write_all(&self.content)?; + Ok(()) + } } #[cfg(test)] @@ -73,4 +96,23 @@ mod tests { let res = extra.unwrap_valid(b"abcdef"); assert!(res.is_err()); } + + #[test] + fn unicode_extra_field_write_test() { + let data = [0x01, 0xef, 0x39, 0x8e, 0x4b, b'u', b't', b'f', b'-', b'8']; + assert_eq!(data.len(), 10); // 0x0A + let extra = + UnicodeExtraField::try_from_reader(&mut std::io::Cursor::new(data), 10).unwrap(); + let mut data = Vec::new(); + extra.write(&mut data).unwrap(); + + // The magic is NOT written + // Size is 10 + assert_eq!( + data, + [ + 0x0A, 0x00, 0x01, 0xef, 0x39, 0x8e, 0x4b, b'u', b't', b'f', b'-', b'8' + ] + ); + } } diff --git a/src/read/mod.rs b/src/read/mod.rs index 508d68ca4..5c1979bcc 100644 --- a/src/read/mod.rs +++ b/src/read/mod.rs @@ -3,20 +3,16 @@ use crate::compression::CompressionMethod; use crate::cp437::FromCp437; use crate::datetime::DateTime; -use crate::extra_fields::AexEncryption; -use crate::extra_fields::UnicodeExtraField; -use crate::extra_fields::Zip64ExtendedInformation; -use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs, UsedExtraField}; +use crate::extra_fields::{ExtraField, ExtraFields}; use crate::format::flags::ZipFlags; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::{CentralDirectoryEndInfo, DataAndPosition, ZipCentralEntryBlock}; use crate::types::{System, ZipFileData}; -use crate::unstable::LittleEndianReadExt; use indexmap::IndexMap; use std::ffi::OsStr; -use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::io::{self, Read, Seek, Write}; use std::path::Path; -use std::sync::{Arc, OnceLock}; +use std::sync::OnceLock; mod config; pub use config::{ArchiveOffset, Config}; @@ -480,7 +476,7 @@ fn central_header_to_zip_file_inner( let is_utf8 = ZipFlags::matching(flags, ZipFlags::LanguageEncoding); let mut file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?; - let extra_field = read_variable_length_byte_field(reader, extra_field_length as usize)?; + let extra_fields_raw = read_variable_length_byte_field(reader, extra_field_length as usize)?; let file_comment_raw = read_variable_length_byte_field(reader, file_comment_length as usize)?; let file_comment: Box = if is_utf8 { String::from_utf8_lossy(&file_comment_raw).into() @@ -489,6 +485,7 @@ fn central_header_to_zip_file_inner( }; let (version_made_by, system) = System::extract_bytes(version_made_by); + let extra_fields = ExtraFields::parse(&extra_fields_raw, &block)?; // Construct the result let mut result = ZipFileData { system, @@ -499,8 +496,6 @@ fn central_header_to_zip_file_inner( compressed_size: compressed_size.into(), uncompressed_size: uncompressed_size.into(), flags, - extra_field: Some(Arc::from(extra_field)), - central_extra_field: None, file_comment, header_start: offset.into(), extra_data_start: None, @@ -509,10 +504,9 @@ fn central_header_to_zip_file_inner( external_attributes: external_file_attributes, large_file: false, aes_mode: None, - aes_extra_data_start: 0, - extra_fields: Vec::new(), + extra_fields, }; - parse_extra_field(&mut result, &mut file_name_raw)?; + result.apply_extra_fields(&mut file_name_raw)?; // Account for shifted zip offsets. result.header_start = result @@ -523,159 +517,6 @@ fn central_header_to_zip_file_inner( Ok((result, file_name_raw)) } -pub(crate) fn parse_extra_field( - file: &mut ZipFileData, - file_name_raw: &mut Vec, -) -> ZipResult<()> { - let mut extra_field = file.extra_field.clone(); - let mut central_extra_field = file.central_extra_field.clone(); - for field_group in [&mut extra_field, &mut central_extra_field] { - let Some(extra_field) = field_group else { - continue; - }; - let mut modified = false; - let mut processed_extra_field = vec![]; - let len = extra_field.len(); - let mut reader = io::Cursor::new(&**extra_field); - - let mut position = reader.position(); - while position < len as u64 { - let old_position = position; - let remove = - parse_single_extra_field(file, &mut reader, position, false, file_name_raw)?; - position = reader.position(); - if remove { - modified = true; - } else { - let field_len = (position - old_position) as usize; - let write_start = processed_extra_field.len(); - reader.seek(SeekFrom::Start(old_position))?; - processed_extra_field.extend_from_slice(&vec![0u8; field_len]); - if let Err(e) = reader - .read_exact(&mut processed_extra_field[write_start..(write_start + field_len)]) - { - if e.kind() == io::ErrorKind::UnexpectedEof { - return Err(invalid!("Extra field content exceeds declared length")); - } - return Err(e.into()); - } - } - } - if modified { - *field_group = Some(Arc::from(processed_extra_field.into_boxed_slice())); - } - } - file.extra_field = extra_field; - file.central_extra_field = central_extra_field; - Ok(()) -} - -pub(crate) fn parse_single_extra_field( - file: &mut ZipFileData, - reader: &mut R, - bytes_already_read: u64, - disallow_zip64: bool, - file_name_raw: &mut Vec, -) -> ZipResult { - let kind = match reader.read_u16_le() { - Ok(kind) => kind, - Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(false), - Err(e) => return Err(e.into()), - }; - let decoded_extra_field = UsedExtraField::try_from(kind); - let len = match decoded_extra_field { - Ok(known_field) => match reader.read_u16_le() { - Ok(len) => len, - Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { - return Err(invalid!("Extra field {} header truncated", known_field)); - } - Err(e) => return Err(e.into()), - }, - Err(()) => { - match reader.read_u16_le() { - Ok(len) => len, - Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(false), // early return, most likely a padding - Err(_e) => { - // Consume remaining bytes to avoid infinite loop in caller - let mut buf = [0u8; 2048]; - while reader.read(&mut buf)? != 0 { - // loop to read and consume - } - return Ok(false); - } - } - } - }; - match decoded_extra_field { - // Zip64 extended information extra field - Ok(UsedExtraField::Zip64ExtendedInfo) => { - if disallow_zip64 { - return Err(invalid!("Can't write a custom field using the ZIP64 ID")); - } - file.large_file = true; - let (uncomp_size, comp_size, header_start) = Zip64ExtendedInformation::parse( - reader, - len, - file.uncompressed_size, - file.compressed_size, - file.header_start, - )?; - file.uncompressed_size = uncomp_size; - file.compressed_size = comp_size; - file.header_start = header_start; - return Ok(true); - } - Ok(UsedExtraField::Ntfs) => { - // NTFS extra field - file.extra_fields - .push(ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?)); - } - Ok(UsedExtraField::AeXEncryption) => { - // AES - let (aes_options, inner_compression_method) = AexEncryption::parse(reader, len)?; - file.aes_mode = Some(aes_options); - file.compression_method = inner_compression_method; - file.aes_extra_data_start = bytes_already_read; - } - Ok(UsedExtraField::ExtendedTimestamp) => { - file.extra_fields.push(ExtraField::ExtendedTimestamp( - ExtendedTimestamp::try_from_reader(reader, len)?, - )); - } - Ok(UsedExtraField::UnicodeComment) => { - // Info-ZIP Unicode Comment Extra Field - // APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt - let unicode = UnicodeExtraField::try_from_reader(reader, len)?; - // If the CRC check fails, this Unicode Comment extra field SHOULD be ignored and - // the File Comment field in the header SHOULD be used instead. - if unicode.is_crc32_valid(file.file_comment.as_bytes()) { - file.file_comment = String::from_utf8(unicode.content.into_vec())?.into(); - } - } - Ok(UsedExtraField::UnicodePath) => { - // Info-ZIP Unicode Path Extra Field - // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt - let unicode = UnicodeExtraField::try_from_reader(reader, len)?; - // If the CRC check fails, this UTF-8 Path Extra Field SHOULD be ignored and - // the File Name field in the header SHOULD be used instead. - if unicode.is_crc32_valid(file_name_raw) { - *file_name_raw = unicode.content.into_vec(); - file.flags |= ZipFlags::LanguageEncoding.as_u16(); - } - } - _ => { - if let Err(e) = reader.read_exact(&mut vec![0u8; len as usize]) { - if e.kind() == io::ErrorKind::UnexpectedEof { - return Err(invalid!("Extra field content truncated")); - } - return Err(e.into()); - } - // Other fields are ignored - } - } - Ok(false) -} - /// A trait for exposing file metadata inside the zip. pub trait HasZipMetadata { /// Get the file metadata diff --git a/src/read/stream.rs b/src/read/stream.rs index db482893d..07e1c8798 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -1,13 +1,14 @@ //! Code related to stream reading use crate::ZipReadOptions; -use crate::read::parse_extra_field; +use crate::extra_fields::ExtraFields; use crate::read::readers::{make_crypto_reader, make_reader}; use crate::read::{ ZipFile, ZipFileData, ZipResult, central_header_to_zip_file_inner, make_symlink, }; -use crate::result::ZipError; +use crate::result::{ZipError, invalid}; use crate::spec::{FixedSizeBlock, Magic, Pod, ZipCentralEntryBlock, ZipLocalEntryBlock}; + use indexmap::IndexMap; use std::borrow::Cow; use std::io::{self, Read}; @@ -237,7 +238,7 @@ pub fn read_zipfile_from_stream(reader: &mut R) -> ZipResult( +pub fn read_zipfile_from_stream_with_compressed_size<'a, R: Read>( reader: &'a mut R, compressed_size: u64, ) -> ZipResult>> { @@ -247,7 +248,7 @@ pub fn read_zipfile_from_stream_with_compressed_size<'a, R: io::Read>( /// Same as `read_zipfile_from_stream` but with `ZipReadOptions` /// Since LZMA decoding requires the uncompressed length, you will need to override it -pub fn read_zipfile_from_stream_with_options<'a, R: io::Read>( +pub fn read_zipfile_from_stream_with_options<'a, R: Read>( reader: &'a mut R, mut options: ZipReadOptions<'a>, ) -> ZipResult>> { @@ -267,8 +268,29 @@ pub fn read_zipfile_from_stream_with_options<'a, R: io::Read>( reader.read_exact(block.as_bytes_mut())?; let block = block.from_le(); + // parse file_name_raw + let file_name_length: usize = block.file_name_length.into(); + let mut file_name_raw = vec![0u8; file_name_length]; + if let Err(e) = reader.read_exact(&mut file_name_raw) { + if e.kind() == std::io::ErrorKind::UnexpectedEof { + return Err(invalid!("File name extends beyond file boundary")); + } + return Err(e.into()); + } - let (mut data, mut file_name_raw) = ZipFileData::from_local_block(block, reader)?; + // parse extra fields raw + let extra_field_length: usize = block.extra_field_length.into(); + let mut extra_fields_raw = vec![0u8; extra_field_length]; + if let Err(e) = reader.read_exact(&mut extra_fields_raw) { + if e.kind() == std::io::ErrorKind::UnexpectedEof { + return Err(invalid!("Extra field extends beyond file boundary")); + } + return Err(e.into()); + } + // parse extra fields + let extra_fields = ExtraFields::parse(&extra_fields_raw, &block)?; + let mut data = ZipFileData::from_local_block(block, extra_fields)?; + data.apply_extra_fields(&mut file_name_raw)?; if data.is_using_data_descriptor() { if let Some(comp_size) = options.force_compressed_size { data.compressed_size = comp_size; @@ -285,11 +307,6 @@ pub fn read_zipfile_from_stream_with_options<'a, R: io::Read>( data.crc32 = crc; } - match parse_extra_field(&mut data, &mut file_name_raw) { - Ok(..) | Err(ZipError::Io(..)) => {} - Err(e) => return Err(e), - } - if options.ignore_encryption_flag { // Always use no password when we're ignoring the encryption flag. options.password = None; diff --git a/src/read/zipfile.rs b/src/read/zipfile.rs index f46031a2a..694476baa 100644 --- a/src/read/zipfile.rs +++ b/src/read/zipfile.rs @@ -2,9 +2,9 @@ use crate::CompressionMethod; use crate::DateTime; -use crate::ExtraField; use crate::HasZipMetadata; use crate::ZIP64_BYTES_THR; +use crate::read::ExtraField; use crate::read::RootDirFilter; use crate::read::make_writable_dir_all; use crate::read::readers::{ZipFileReader, ZipFileSeekReader}; @@ -15,7 +15,7 @@ use crate::types::{SimpleFileOptions, ffi}; use core::mem::replace; use std::borrow::Cow; use std::ffi::OsStr; -use std::io::{self, Read, Seek, SeekFrom, copy, sink}; +use std::io::{self, Cursor, Read, Seek, SeekFrom, copy, sink}; use std::path::{Component, Path, PathBuf}; /// A struct for reading a zip file @@ -275,8 +275,19 @@ impl<'a, R: Read + ?Sized> ZipFile<'a, R> { } /// Get the extra data of the zip header for this file - pub fn extra_data(&self) -> Option<&[u8]> { - self.get_metadata().extra_field.as_deref() + pub fn extra_data(&self) -> Option> { + let out_buffer = Vec::new(); + let mut cursor = Cursor::new(out_buffer); + let extra_fields = self.data.extra_fields.local_extra_fields(); + for one_extra_field in extra_fields { + one_extra_field.write(&mut cursor, true).ok()?; + } + let extra_fields_data = cursor.into_inner(); + if extra_fields_data.is_empty() { + None + } else { + Some(extra_fields_data) + } } /// Get the starting offset of the data of the compressed file @@ -327,7 +338,7 @@ impl<'a, R: Read + ?Sized> ZipFile<'a, R> { impl ZipFile<'_, R> { /// iterate through all extra fields pub fn extra_data_fields(&self) -> impl Iterator { - self.data.extra_fields.iter() + self.data.extra_fields.inner.iter() } } diff --git a/src/spec.rs b/src/spec.rs index f3c0a9cc9..3b6ccc2e4 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -275,6 +275,18 @@ pub(crate) struct ZipCentralEntryBlock { unsafe impl Pod for ZipCentralEntryBlock {} +impl ZipEntryBlock for ZipCentralEntryBlock { + fn get_uncompressed_size(&self) -> u32 { + self.uncompressed_size + } + fn get_compressed_size(&self) -> u32 { + self.compressed_size + } + fn get_header_start(&self) -> Option { + Some(self.offset) + } +} + impl FixedSizeBlock for ZipCentralEntryBlock { const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE; @@ -300,6 +312,12 @@ impl FixedSizeBlock for ZipCentralEntryBlock { ]; } +pub(crate) trait ZipEntryBlock { + fn get_uncompressed_size(&self) -> u32; + fn get_compressed_size(&self) -> u32; + fn get_header_start(&self) -> Option; +} + #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipLocalEntryBlock { @@ -317,6 +335,18 @@ pub(crate) struct ZipLocalEntryBlock { unsafe impl Pod for ZipLocalEntryBlock {} +impl ZipEntryBlock for ZipLocalEntryBlock { + fn get_uncompressed_size(&self) -> u32 { + self.uncompressed_size + } + fn get_compressed_size(&self) -> u32 { + self.compressed_size + } + fn get_header_start(&self) -> Option { + None + } +} + impl FixedSizeBlock for ZipLocalEntryBlock { const MAGIC: Magic = Magic::LOCAL_FILE_HEADER_SIGNATURE; diff --git a/src/types.rs b/src/types.rs index 12f5f1981..9cf380a6a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,15 +3,15 @@ use crate::CompressionMethod; use crate::cp437::FromCp437; use crate::datetime::DateTime; -use crate::extra_fields::ExtraField; +use crate::extra_fields::ExtraFields; use crate::format::flags::ZipFlags; use crate::path::{enclosed_name, file_name_sanitized}; use crate::read::readers::SeekableTake; -use crate::result::{ZipError, ZipResult, invalid}; +use crate::result::{ZipError, ZipResult}; use crate::spec::is_dir; use crate::spec::{ - self, FixedSizeBlock, Magic, Zip64DataDescriptorBlock, ZipCentralEntryBlock, - ZipDataDescriptorBlock, ZipLocalEntryBlock, + self, FixedSizeBlock, Magic, Zip64DataDescriptorBlock, ZipDataDescriptorBlock, + ZipLocalEntryBlock, }; use crate::write::FileOptionExtension; use crate::zipcrypto::ZipCryptoKeys; @@ -20,7 +20,7 @@ use std::borrow::Cow; use std::ffi::OsStr; use std::io::{Read, Seek, SeekFrom, Take}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, OnceLock}; +use std::sync::OnceLock; pub(crate) use crate::format::aes::{AesMode, AesVendorVersion}; pub(crate) use crate::format::flags::System; @@ -121,10 +121,6 @@ pub struct ZipFileData { pub compressed_size: u64, /// Size of the file when extracted pub uncompressed_size: u64, - /// Extra field usually used for storage expansion - pub extra_field: Option>, - /// Extra field only written to central directory - pub central_extra_field: Option>, /// File comment pub file_comment: Box, /// Specifies where the local header of the file starts @@ -143,11 +139,8 @@ pub struct ZipFileData { pub large_file: bool, /// AES settings if applicable pub aes_mode: Option<(AesMode, AesVendorVersion)>, - /// Specifies where in the extra data the AES metadata starts - pub aes_extra_data_start: u64, - /// extra fields, see - pub extra_fields: Vec, + pub extra_fields: ExtraFields, } impl ZipFileData { @@ -340,20 +333,6 @@ impl ZipFileData { .max(crypto_version) .max(misc_feature_version) } - #[inline(always)] - pub(crate) fn extra_field_len(&self) -> usize { - self.extra_field - .as_ref() - .map(|v| v.len()) - .unwrap_or_default() - } - #[inline(always)] - pub(crate) fn central_extra_field_len(&self) -> usize { - self.central_extra_field - .as_ref() - .map(|v| v.len()) - .unwrap_or_default() - } #[allow(clippy::too_many_arguments)] pub(crate) fn initialize_local_block( @@ -362,10 +341,9 @@ impl ZipFileData { raw_values: &ZipRawValues, header_start: u64, extra_data_start: Option, - aes_extra_data_start: u64, compression_method: CompressionMethod, aes_settings: Option<(AesMode, AesVendorVersion)>, - extra_field: &[u8], + extra_fields: ExtraFields, ) -> Self { let permissions = options .permissions @@ -418,11 +396,6 @@ impl ZipFileData { crc32: raw_values.crc32, compressed_size: raw_values.compressed_size, uncompressed_size: raw_values.uncompressed_size, - extra_field: Some(Arc::from(extra_field)), - central_extra_field: options - .extended_options - .central_only_extra_fields() - .map(Arc::<[u8]>::from), file_comment: String::with_capacity(0).into_boxed_str(), header_start, data_start: OnceLock::new(), @@ -430,18 +403,17 @@ impl ZipFileData { external_attributes, large_file: options.large_file, aes_mode: aes_settings, - extra_fields: Vec::new(), + extra_fields, extra_data_start, - aes_extra_data_start, }; local_block.version_made_by = local_block.version_needed() as u8; local_block } - pub(crate) fn from_local_block( + pub(crate) fn from_local_block( block: ZipLocalEntryBlock, - reader: &mut R, - ) -> ZipResult<(Self, Vec)> { + extra_fields: ExtraFields, + ) -> ZipResult { let ZipLocalEntryBlock { version_made_by, flags, @@ -451,30 +423,10 @@ impl ZipFileData { crc32, compressed_size, uncompressed_size, - file_name_length, - extra_field_length, .. } = block; let compression_method = CompressionMethod::parse_from_u16(compression_method); - let file_name_length: usize = file_name_length.into(); - let extra_field_length: usize = extra_field_length.into(); - - let mut file_name_raw = vec![0u8; file_name_length]; - if let Err(e) = reader.read_exact(&mut file_name_raw) { - if e.kind() == std::io::ErrorKind::UnexpectedEof { - return Err(invalid!("File name extends beyond file boundary")); - } - return Err(e.into()); - } - let mut extra_field = vec![0u8; extra_field_length]; - if let Err(e) = reader.read_exact(&mut extra_field) { - if e.kind() == std::io::ErrorKind::UnexpectedEof { - return Err(invalid!("Extra field extends beyond file boundary")); - } - return Err(e.into()); - } - let (version_made_by, system) = System::extract_bytes(version_made_by); let data = ZipFileData { system, @@ -485,8 +437,6 @@ impl ZipFileData { crc32, compressed_size: compressed_size.into(), uncompressed_size: uncompressed_size.into(), - extra_field: Some(Arc::from(extra_field.into_boxed_slice())), - central_extra_field: None, file_comment: String::with_capacity(0).into_boxed_str(), // file comment is only available in the central directory // header_start and data start are not available, but also don't matter, since seeking is // not available. @@ -499,14 +449,13 @@ impl ZipFileData { external_attributes: 0, large_file: false, aes_mode: None, - extra_fields: Vec::new(), + extra_fields, extra_data_start: None, - aes_extra_data_start: 0, }; - Ok((data, file_name_raw)) + Ok(data) } - fn flags(&self, file_name_raw: &[u8]) -> u16 { + pub(crate) fn flags(&self, file_name_raw: &[u8]) -> u16 { let is_utf8 = std::str::from_utf8(file_name_raw).is_ok(); let is_ascii = file_name_raw.is_ascii() && self.file_comment.is_ascii(); let utf8_bit: u16 = if is_utf8 && !is_ascii { @@ -525,7 +474,8 @@ impl ZipFileData { utf8_bit | using_data_descriptor_bit | encrypted_bit } - fn clamp_size_field(&self, field: u64) -> Result { + + pub(crate) fn clamp_size_field(&self, field: u64) -> Result { if self.large_file { Ok(spec::ZIP64_BYTES_THR_U32) } else { @@ -538,100 +488,6 @@ impl ZipFileData { } } - pub(crate) fn local_block(&self, file_name_raw: &[u8]) -> ZipResult { - let (compressed_size, uncompressed_size) = if self.is_using_data_descriptor() { - (0, 0) - } else { - ( - self.clamp_size_field(self.compressed_size)?, - self.clamp_size_field(self.uncompressed_size)?, - ) - }; - let extra_field_length: u16 = self - .extra_field_len() - .try_into() - .map_err(|_| invalid!("Extra data field is too large"))?; - - let last_modified_time = self - .last_modified_time - .unwrap_or_else(DateTime::default_for_write); - let compression_method = if self.aes_mode.is_some() { - CompressionMethod::AES.serialize_to_u16() - } else { - self.compression_method.serialize_to_u16() - }; - Ok(ZipLocalEntryBlock { - version_made_by: self.version_needed(), - flags: self.flags(file_name_raw), - compression_method, - last_mod_time: last_modified_time.timepart(), - last_mod_date: last_modified_time.datepart(), - crc32: self.crc32, - compressed_size, - uncompressed_size, - file_name_length: file_name_raw - .len() - .try_into() - .map_err(std::io::Error::other)?, - extra_field_length, - }) - } - - pub(crate) fn block(&self, file_name_raw: &[u8]) -> ZipResult { - let compressed_size = self.clamp_size_field(self.compressed_size)?; - let uncompressed_size = self.clamp_size_field(self.uncompressed_size)?; - let offset = self - .header_start - .min(spec::ZIP64_BYTES_THR) - .try_into() - .map_err(std::io::Error::other)?; - let extra_field_len: u16 = self - .extra_field_len() - .try_into() - .map_err(std::io::Error::other)?; - let central_extra_field_len: u16 = self - .central_extra_field_len() - .try_into() - .map_err(std::io::Error::other)?; - let last_modified_time = self - .last_modified_time - .unwrap_or_else(DateTime::default_for_write); - let version_to_extract = self.version_needed(); - let version_made_by = u16::from(self.version_made_by).max(version_to_extract); - let compression_method = if self.aes_mode.is_some() { - CompressionMethod::AES.serialize_to_u16() - } else { - self.compression_method.serialize_to_u16() - }; - Ok(ZipCentralEntryBlock { - version_made_by: ((self.system as u16) << 8) | version_made_by, - version_to_extract, - flags: self.flags(file_name_raw), - compression_method, - last_mod_time: last_modified_time.timepart(), - last_mod_date: last_modified_time.datepart(), - crc32: self.crc32, - compressed_size, - uncompressed_size, - file_name_length: file_name_raw - .len() - .try_into() - .map_err(std::io::Error::other)?, - extra_field_length: extra_field_len.checked_add(central_extra_field_len).ok_or( - invalid!("Extra field length in central directory exceeds 64KiB"), - )?, - file_comment_length: self - .file_comment - .len() - .try_into() - .map_err(std::io::Error::other)?, - disk_number: 0, - internal_file_attributes: 0, - external_file_attributes: self.external_attributes, - offset, - }) - } - pub(crate) fn write_data_descriptor( &self, writer: &mut W, @@ -724,8 +580,6 @@ mod tests { crc32: 0, compressed_size: 0, uncompressed_size: 0, - extra_field: None, - central_extra_field: None, file_comment: String::with_capacity(0).into_boxed_str(), header_start: 0, extra_data_start: None, @@ -734,8 +588,7 @@ mod tests { external_attributes: 0, large_file: false, aes_mode: None, - aes_extra_data_start: 0, - extra_fields: Vec::new(), + ..ZipFileData::default() }; assert_eq!( data.file_name_sanitized(&file_name), diff --git a/src/write.rs b/src/write.rs index 0439fd115..4ffed3f3a 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,17 +1,20 @@ //! Writing a ZIP archive +use crate::ExtraField; +use crate::ZIP64_BYTES_THR; use crate::compression::CompressionMethod; use crate::datetime::DateTime; use crate::extra_fields::AexEncryption; use crate::extra_fields::CustomExtraField; +use crate::extra_fields::ExtraFields; use crate::extra_fields::UsedExtraField; use crate::extra_fields::Zip64ExtendedInformation; use crate::format::flags::ZipFlags; -use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; +use crate::read::{Config, ZipArchive, ZipFile}; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::{ self, FixedSizeBlock, Magic, Zip32CDEBlock, Zip64CentralDirectoryEnd, - Zip64CentralDirectoryEndLocator, ZipLocalEntryBlock, + Zip64CentralDirectoryEndLocator, ZipCentralEntryBlock, ZipLocalEntryBlock, }; use crate::types::EncryptWith; use crate::types::{AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, ffi}; @@ -22,9 +25,9 @@ use core::mem::{self, offset_of, size_of}; use core::str::{Utf8Error, from_utf8}; use crc32fast::Hasher; use indexmap::IndexMap; +use std::io::ErrorKind; use std::io::{self, Read, Seek, Write}; use std::io::{BufReader, SeekFrom}; -use std::io::{Cursor, ErrorKind}; use std::path::Path; use std::sync::Arc; @@ -290,8 +293,6 @@ mod sealed { pub trait FileOptionExtension: Default + Sealed { /// Extra Data fn extra_fields(&self) -> Option<&Arc>>; - /// Central Extra Data - fn central_only_extra_fields(&self) -> Option>; /// File Comment fn file_comment(&self) -> Option<&str>; /// Take File Comment (moves ownership) @@ -302,9 +303,6 @@ mod sealed { fn extra_fields(&self) -> Option<&Arc>> { None } - fn central_only_extra_fields(&self) -> Option> { - None - } fn file_comment(&self) -> Option<&str> { None } @@ -318,15 +316,6 @@ mod sealed { fn extra_fields(&self) -> Option<&Arc>> { Some(&self.extra_fields) } - fn central_only_extra_fields(&self) -> Option> { - Some( - self.extra_fields - .iter() - .filter(|x| x.central_only) - .flat_map(|x| x.serialize()) - .collect(), - ) - } fn file_comment(&self) -> Option<&str> { self.file_comment.as_ref().map(Box::as_ref) } @@ -370,11 +359,43 @@ impl ExtendedFileOptions { /// `u16::MAX`. If adding this field would exceed that limit or produce an /// invalid extra data structure, an error is returned and no data is /// added. + #[deprecated = "use add_extra_field()"] pub fn add_extra_data>( &mut self, header_id: u16, data: D, central_only: bool, + ) -> ZipResult<()> { + self.add_extra_field(header_id, data, central_only) + } + /// Adds an extra field, unless we detect that it's invalid. + /// + /// # Parameters + /// + /// * `header_id` – The 2‑byte identifier of the ZIP extra field to add. + /// This value determines the type/format of `data` and should either be + /// one of the standard ZIP extra field IDs defined by the ZIP + /// specification or an application‑specific (vendor) ID. + /// * `data` – The raw payload for the extra field, without the leading + /// header ID or length; those are derived from `header_id` and + /// `data.len()` and written automatically. + /// * `central_only` – Controls where the extra field is stored: + /// * When `true`, the field is appended only to the central directory + /// extra data (`central_extra_data`), and the corresponding local file + /// header is left unchanged. + /// * When `false`, the field is appended to the local file header extra + /// data (`extra_data`) and may also be reflected in the central + /// directory, depending on how the ZIP is written. + /// + /// The combined size of all extra data (local + central) must not exceed + /// `u16::MAX`. If adding this field would exceed that limit or produce an + /// invalid extra data structure, an error is returned and no data is + /// added. + pub fn add_extra_field>( + &mut self, + header_id: u16, + data: D, + central_only: bool, ) -> ZipResult<()> { let data = data.as_ref(); let len = data.len() + 4; @@ -390,54 +411,6 @@ impl ExtendedFileOptions { Ok(()) } } - - fn validate_extra_fields(data: &[u8], disallow_zip64: bool) -> ZipResult<()> { - let len = data.len() as u64; - if len == 0 { - return Ok(()); - } - if len > u64::from(u16::MAX) { - return Err(ZipError::Io(io::Error::other( - "Extra-data field can't exceed u16::MAX bytes", - ))); - } - let mut data = Cursor::new(data); - let mut pos = data.position(); - while pos < len { - if len - data.position() < 4 { - return Err(ZipError::Io(io::Error::other( - "Extra-data field doesn't have room for ID and length", - ))); - } - #[cfg(not(feature = "unreserved"))] - { - use crate::{ - extra_fields::{EXTRA_FIELD_MAPPING, UsedExtraField}, - unstable::LittleEndianReadExt, - }; - let header_id = data.read_u16_le()?; - // Some extra fields are authorized - if let Err(()) = UsedExtraField::try_from(header_id) - && EXTRA_FIELD_MAPPING.contains(&header_id) - { - return Err(ZipError::Io(io::Error::other(format!( - "Extra data header ID {header_id:#06} (0x{header_id:x}) \ - requires crate feature \"unreserved\"", - )))); - } - data.seek(SeekFrom::Current(-2))?; - } - parse_single_extra_field( - &mut ZipFileData::default(), - &mut data, - pos, - disallow_zip64, - &mut Vec::new(), - )?; - pos = data.position(); - } - Ok(()) - } } impl Debug for ExtendedFileOptions { @@ -475,7 +448,7 @@ impl<'k, 'n, 'a: 'k + 'n> arbitrary::Arbitrary<'a> for FileOptions<'k, 'n, Exten } u.arbitrary_loop(Some(0), Some(10), |u| { options - .add_extra_data( + .add_extra_field( u.int_in_range(2..=u16::MAX)?, Box::<[u8]>::arbitrary(u)?, bool::arbitrary(u)?, @@ -485,7 +458,7 @@ impl<'k, 'n, 'a: 'k + 'n> arbitrary::Arbitrary<'a> for FileOptions<'k, 'n, Exten })?; let len = u.arbitrary_len::()?; options.name = Some(u.bytes(len)?); - ZipWriter::new(Cursor::new(Vec::new())) + ZipWriter::new(std::io::Cursor::new(Vec::new())) .start_file("", options.clone()) .map_err(|_| arbitrary::Error::IncorrectFormat)?; Ok(options) @@ -681,19 +654,37 @@ impl FileOptions<'_, '_, ExtendedFileOptions> { } /// Adds an extra data field. + #[deprecated = "use add_extra_field()"] pub fn add_extra_data>( &mut self, header_id: u16, data: D, central_only: bool, + ) -> ZipResult<()> { + self.add_extra_field(header_id, data, central_only) + } + + /// Adds an extra field. + pub fn add_extra_field>( + &mut self, + header_id: u16, + data: D, + central_only: bool, ) -> ZipResult<()> { self.extended_options - .add_extra_data(header_id, data, central_only) + .add_extra_field(header_id, data, central_only) } - /// Removes the extra data fields. + /// Removes the extra fields. #[must_use] - pub fn clear_extra_data(mut self) -> Self { + #[deprecated = "use clear_extra_fields"] + pub fn clear_extra_data(self) -> Self { + self.clear_extra_fields() + } + + /// Removes the extra fields. + #[must_use] + pub fn clear_extra_fields(mut self) -> Self { if !self.extended_options.extra_fields.is_empty() { self.extended_options.extra_fields = Arc::new(vec![]); } @@ -903,37 +894,16 @@ impl ZipWriter { let mut new_data = src_data.clone(); let dest_name_raw = dest_name.as_bytes(); new_data.header_start = write_position; - let extra_fields_start = write_position - + (size_of::() + size_of::()) as u64 - + dest_name_raw.len() as u64; - new_data.extra_data_start = Some(extra_fields_start); - if let Some(extra) = &src_data.extra_field { - let stripped = strip_alignment_extra_field(extra, false); - if stripped.is_empty() { - new_data.extra_field = None; - } else { - new_data.extra_field = Some(Arc::from(stripped.into_boxed_slice())); - } - } - - let mut data_start = extra_fields_start; - if let Some(extra) = &new_data.extra_field { - data_start += extra.len() as u64; - } - 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(dest_name_raw)?; let index = self.insert_file_data(dest_name_raw, new_data)?; - let new_data = &self.files[index]; + let new_data = &mut self.files[index]; let result: io::Result<()> = { let plain_writer = self.inner.try_inner_mut()?; - block.write(plain_writer)?; - plain_writer.write_all(dest_name_raw)?; - if let Some(data) = &new_data.extra_field { - plain_writer.write_all(data)?; - } - debug_assert_eq!(data_start, plain_writer.stream_position()?); + new_data.write_local_header(write_position, plain_writer, dest_name_raw, None)?; + debug_assert_eq!( + new_data.data_start.get(), + Some(&plain_writer.stream_position()?) + ); self.writing_to_file = true; plain_writer.write_all(©)?; if self.flush_on_finish_file { @@ -1150,28 +1120,42 @@ impl ZipWriter { compressed_size: 0, uncompressed_size: 0, }); - + #[cfg(not(feature = "unreserved"))] + { + use crate::extra_fields::EXTRA_FIELD_MAPPING; + if let Some(extra_fields) = options.extended_options.extra_fields() { + for x in extra_fields.iter() { + let header_id = x.header_id; + if UsedExtraField::try_from(header_id).is_err() + && EXTRA_FIELD_MAPPING.contains(&header_id) + { + return Err(ZipError::Io(io::Error::other(format!( + "Extra data header ID {header_id:#06} (0x{header_id:x}) \ + requires crate feature \"unreserved\"", + )))); + } + } + } + } + #[cfg_attr(not(feature = "aes-crypto"), allow(unused_mut))] let mut extra_fields = match options.extended_options.extra_fields() { - Some(data) => data.to_vec(), + Some(data) => data.iter().map(|x| ExtraField::Custom(x.clone())).collect(), None => vec![], }; - if let Some(zip64_block) = Zip64ExtendedInformation::new_local(options.large_file) { - let mut serialized_zip64 = Vec::with_capacity(zip64_block.full_size()); - zip64_block.write(&mut serialized_zip64)?; - let zip64_block = CustomExtraField::new( - false, - Zip64ExtendedInformation::MAGIC.as_u16(), - &serialized_zip64[4..], - ); - extra_fields.insert(0, zip64_block); + let entry_len: usize = mem::size_of::() + + mem::size_of::() + + file_name_raw.len() + + extra_fields.iter().map(|x| x.size(false)).sum::(); + if entry_len >= (u16::MAX as usize) { + return Err(invalid!( + "ZipLocalBlock + filename + extra fields must be less than 64KiB when combined" + )); } // Figure out the underlying compression_method and aes mode when using // AES encryption. // Preserve AES method for raw copies without needing a password let compression_method = options.compression_method; - #[allow(unused_mut)] - let mut aes_extra_field_start = 0; let aes_mode_options = match options.encrypt_with { #[cfg(feature = "aes-crypto")] Some(EncryptWith::Aes { @@ -1181,79 +1165,26 @@ impl ZipWriter { }) => { // Write AES encryption extra data. // For raw copies of AES entries, write the correct AES extra data immediately - let aex_extra_field = AexEncryption::new(vendor_version, mode, compression_method); - let mut buf = [0u8; AexEncryption::EXTRA_FIELD_SIZE as usize]; - aex_extra_field.write_data(&mut buf.as_mut_slice())?; - - let extra_fields_len: usize = - extra_fields.iter().map(|x| x.len_with_header()).sum(); - aes_extra_field_start = extra_fields_len as u64; - let aex = - CustomExtraField::new(false, UsedExtraField::AeXEncryption.as_u16(), &buf); - extra_fields.push(aex); + extra_fields.push(ExtraField::AeXEncryption(AexEncryption::new( + vendor_version, + mode, + compression_method, + ))); Some((mode, vendor_version)) } _ => None, }; - - let header_end = header_start - + (size_of::() + size_of::()) as u64 - + file_name_raw.len() as u64; - - if options.alignment > 1 { - let extra_fields_len: usize = extra_fields.iter().map(|x| x.len_with_header()).sum(); - let extra_fields_end = header_end + extra_fields_len as u64; - let align = u64::from(options.alignment); - let unaligned_header_bytes = extra_fields_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; - } - let extra_fields_len: usize = - extra_fields.iter().map(|x| x.len_with_header()).sum(); - let new_len = extra_fields_len + pad_length; - if new_len > u16::MAX as usize { - // Alignment is impossible without exceeding extra field size limits. - // Skip alignment. - } else { - // Add an extra field to the extra_field, 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(); - let alignment_extra_field = CustomExtraField::new( - true, - UsedExtraField::DataStreamAlignment.as_u16(), - &pad_body, - ); - extra_fields.push(alignment_extra_field); - let extra_fields_len: usize = - extra_fields.iter().map(|x| x.len_with_header()).sum(); - debug_assert_eq!((extra_fields_len as u64 + header_end) % align, 0); - } - } - } - let extra_fields_len: usize = extra_fields.iter().map(|x| x.len_with_header()).sum(); - let central_extra_fields = options.extended_options.central_only_extra_fields(); - if let Some(data) = central_extra_fields { - if extra_fields_len + data.len() > u16::MAX as usize { - return Err(invalid!( - "Extra data and central extra data must be less than 64KiB when combined" - )); - } - ExtendedFileOptions::validate_extra_fields(&data, true)?; - } - let extra_fields: Vec = extra_fields.iter().flat_map(|x| x.serialize()).collect(); let mut file = ZipFileData::initialize_local_block( file_name_raw, &options, &raw_values, header_start, None, - aes_extra_field_start, compression_method, aes_mode_options, - &extra_fields, + ExtraFields { + inner: extra_fields, + }, ); if let Some(comment) = options.extended_options.take_file_comment() { if comment.len() > u16::MAX as usize { @@ -1268,21 +1199,12 @@ impl ZipWriter { file.flags |= ZipFlags::UsingDataDescriptor.as_u16(); } 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_name_raw, file)?; self.writing_to_file = true; let result: ZipResult<()> = { - ExtendedFileOptions::validate_extra_fields(&extra_fields, false)?; let file = &mut self.files[index]; - let block = file.local_block(file_name_raw)?; let writer = self.inner.try_inner_mut()?; - block.write(writer)?; - // file name - writer.write_all(file_name_raw)?; - if extra_fields_len > 0 { - writer.write_all(&extra_fields)?; - file.extra_field = Some(Arc::from(extra_fields.into_boxed_slice())); - } + file.write_local_header(header_start, writer, file_name_raw, Some(options.alignment))?; Ok(()) }; self.ok_or_abort_file(result)?; @@ -1343,7 +1265,7 @@ impl ZipWriter { _ => {} } let file = &mut self.files[index]; - debug_assert!(file.data_start.get().is_none()); + //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(); @@ -1903,7 +1825,7 @@ impl ZipWriter { let mut version_needed = u16::from(MIN_VERSION); let central_start = writer.stream_position()?; - for (filename_raw, file) in &self.files { + for (filename_raw, file) in &mut self.files { file.write_central_directory_header(writer, filename_raw)?; version_needed = version_needed.max(file.version_needed()); } @@ -2407,35 +2329,40 @@ fn update_aes_extra_field( // seek operations, so we gate this behind using_data_descriptor. // // C.f. https://www.winzip.com/en/support/aes-encryption/#crc-faq - *version = if bytes_written < 20 { + let new_version = if bytes_written < 20 { AesVendorVersion::Ae2 } else { AesVendorVersion::Ae1 }; + *version = new_version; + + // edit the extra field + if let Some(ExtraField::AeXEncryption(AexEncryption { + aes_vendor_version, + aes_extra_field_start, + .. + })) = &mut file + .extra_fields + .inner + .iter_mut() + .find(|f| matches!(f, ExtraField::AeXEncryption { .. })) + { + *aes_vendor_version = new_version; + let extra_field_start = file + .extra_data_start + .ok_or_else(|| std::io::Error::other("Cannot get the extra data start"))?; - let extra_field_start = file - .extra_data_start - .ok_or_else(|| std::io::Error::other("Cannot get the extra data start"))?; - - writer.seek(SeekFrom::Start( - extra_field_start + file.aes_extra_data_start, - ))?; - - let aes_extra_field = AexEncryption::new(*version, *aes_mode, inner_compression_method); - let mut buf = [0u8; AexEncryption::FULL_SIZE]; - aes_extra_field.write(&mut buf.as_mut_slice())?; - writer.write_all(&buf)?; + if let Some(aes_start) = aes_extra_field_start { + writer.seek(SeekFrom::Start(extra_field_start + *aes_start as u64))?; + } else { + return Err(invalid!("The AES extra field should have a known start")); + } - let aes_extra_field_start = file.aes_extra_data_start as usize; - let Some(ref mut extra_field) = file.extra_field else { - return Err(invalid!( - "update_aes_extra_field called on a file that has no extra-data field" - )); - }; - let mut vec = extra_field.to_vec(); - vec[aes_extra_field_start..aes_extra_field_start + AexEncryption::FULL_SIZE] - .copy_from_slice(&buf); - *extra_field = Arc::from(vec.into_boxed_slice()); + let aes_extra_field = AexEncryption::new(*version, *aes_mode, inner_compression_method); + let mut buf = [0u8; AexEncryption::FULL_SIZE]; + aes_extra_field.write(&mut buf.as_mut_slice())?; + writer.write_all(&buf)?; + } Ok(()) } @@ -2489,83 +2416,272 @@ impl ZipFileData { writer.seek(SeekFrom::Start(zip64_extra_field_start))?; zip64_block.write(writer)?; - if let Some(extra_field) = &mut self.extra_field { - let slice = Arc::make_mut(extra_field); - let mut cursor = Cursor::new(&mut slice[0..20]); - zip64_block.write(&mut cursor)?; - } Ok(()) } pub(crate) fn write_central_directory_header( - &self, + &mut self, writer: &mut T, file_name_raw: &[u8], ) -> ZipResult<()> { - let mut block = self.block(file_name_raw)?; - let stripped_extra = if let Some(extra) = &self.extra_field { - strip_alignment_extra_field(extra, true) + let mut is_zip64 = false; + for one_extra_field in self.extra_fields.inner.iter_mut() { + if let ExtraField::Zip64ExtendedInformation { + compressed_size, + uncompressed_size, + header_start, + } = one_extra_field + { + *compressed_size = Some(self.compressed_size); + *uncompressed_size = Some(self.uncompressed_size); + if self.header_start >= ZIP64_BYTES_THR { + *header_start = Some(self.header_start); + } + is_zip64 = true; + } + } + if !is_zip64 { + // check if needed and crash if needed + if let Some(zip64_block) = Zip64ExtendedInformation::central_header( + self.large_file, + self.uncompressed_size, + self.compressed_size, + self.header_start, + ) { + if zip64_block.uncompressed_size.is_some() || zip64_block.compressed_size.is_some() + { + self.extra_fields.inner.insert( + 0, + ExtraField::Zip64ExtendedInformation { + compressed_size: zip64_block.compressed_size, + uncompressed_size: zip64_block.uncompressed_size, + header_start: zip64_block.header_start, + }, + ); + //return Err(invalid!("Should have used large file")); + } else { + self.extra_fields.inner.insert( + 0, + ExtraField::Zip64ExtendedInformation { + compressed_size: None, + uncompressed_size: None, + header_start: zip64_block.header_start, + }, + ); + } + } + } + let central_extra_fields = self.extra_fields.central_extra_fields(); + let extra_field_len: usize = self + .extra_fields + .central_extra_fields() + .map(|x| x.size(false)) + .sum(); + let compressed_size = if self.large_file { + spec::ZIP64_BYTES_THR as u32 } else { - Vec::new() + self.compressed_size + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)? }; - let central_len = self.central_extra_field_len(); - let zip64_extra_field_block = Zip64ExtendedInformation::central_header( - self.large_file, - self.uncompressed_size, - self.compressed_size, - self.header_start, - ); - let zip64_block_len = if let Some(zip64) = zip64_extra_field_block { - zip64.full_size() + let uncompressed_size = if self.large_file { + spec::ZIP64_BYTES_THR as u32 } else { - 0 + self.uncompressed_size + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)? }; - let total_extra_len = zip64_block_len + stripped_extra.len() + central_len; - block.extra_field_length = u16::try_from(total_extra_len) + let offset = self + .header_start + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)?; + let last_modified_time = self + .last_modified_time + .unwrap_or_else(DateTime::default_for_write); + let version_to_extract = self.version_needed(); + let version_made_by = u16::from(self.version_made_by).max(version_to_extract); + let compression_method = if self.aes_mode.is_some() { + CompressionMethod::AES.serialize_to_u16() + } else { + self.compression_method.serialize_to_u16() + }; + let extra_field_length = u16::try_from(extra_field_len) .map_err(|_| invalid!("Extra field length in central directory exceeds 64KiB"))?; + let block = ZipCentralEntryBlock { + version_made_by: ((self.system as u16) << 8) | version_made_by, + version_to_extract, + flags: self.flags(file_name_raw), + compression_method, + last_mod_time: last_modified_time.timepart(), + last_mod_date: last_modified_time.datepart(), + crc32: self.crc32, + compressed_size, + uncompressed_size, + file_name_length: file_name_raw + .len() + .try_into() + .map_err(std::io::Error::other)?, + extra_field_length, + file_comment_length: self + .file_comment + .len() + .try_into() + .map_err(std::io::Error::other)?, + disk_number: 0, + internal_file_attributes: 0, + external_file_attributes: self.external_attributes, + offset, + }; block.write(writer)?; // file name writer.write_all(file_name_raw)?; // extra field - if let Some(zip64_extra_field) = zip64_extra_field_block { - zip64_extra_field.write(writer)?; - } - if !stripped_extra.is_empty() { - writer.write_all(&stripped_extra)?; - } - if let Some(central_extra_field) = &self.central_extra_field { - writer.write_all(central_extra_field)?; + for one_extra_field in central_extra_fields { + one_extra_field.write(writer, false)?; } // file comment writer.write_all(self.file_comment.as_bytes())?; - Ok(()) } -} -pub(crate) fn strip_alignment_extra_field(extra_field: &[u8], remove_zip64: bool) -> Vec { - let mut new_extra = Vec::with_capacity(extra_field.len()); - let mut cursor = 0; - while cursor + 4 <= extra_field.len() { - let tag = u16::from_le_bytes([extra_field[cursor], extra_field[cursor + 1]]); - let len = u16::from_le_bytes([extra_field[cursor + 2], extra_field[cursor + 3]]) as usize; - if cursor + 4 + len > extra_field.len() { - new_extra.extend_from_slice(&extra_field[cursor..]); - break; + pub(crate) fn write_local_header( + &mut self, + header_start: u64, + writer: &mut T, + file_name_raw: &[u8], + alignment_opt: Option, + ) -> ZipResult<()> { + self.extra_fields + .inner + .retain(|field| !matches!(field, ExtraField::Zip64ExtendedInformation { .. })); + if self.large_file { + // add a zip64 extra field which is going to be edited when we know the size + // the update function need the extra field to be at index 0 + self.extra_fields.inner.insert( + 0, + ExtraField::Zip64ExtendedInformation { + compressed_size: Some(u64::MAX), + uncompressed_size: Some(u64::MAX), + header_start: None, + }, + ); } - - if tag != UsedExtraField::DataStreamAlignment.as_u16() - && !(tag == UsedExtraField::Zip64ExtendedInfo.as_u16() && remove_zip64) + let (compressed_size, uncompressed_size) = if self.is_using_data_descriptor() { + (0, 0) + } else { + ( + self.clamp_size_field(self.compressed_size)?, + self.clamp_size_field(self.uncompressed_size)?, + ) + }; + let mut extra_field_len: usize = self + .extra_fields + .local_extra_fields() + .map(|x| x.size(true)) + .sum(); + let last_modified_time = self + .last_modified_time + .unwrap_or_else(DateTime::default_for_write); + let compression_method = if self.aes_mode.is_some() { + CompressionMethod::AES.serialize_to_u16() + } else { + self.compression_method.serialize_to_u16() + }; + let header_end = header_start + + (size_of::() + size_of::()) as u64 + + file_name_raw.len() as u64; + let opt_alignment_field = if let Some(alignment) = alignment_opt + && alignment > 1 { - new_extra.extend_from_slice(&extra_field[cursor..cursor + 4 + len]); + let extra_field_end = header_end + extra_field_len as u64; + let align = u64::from(alignment); + let unaligned_header_bytes = extra_field_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; + } + extra_field_len = self + .extra_fields + .local_extra_fields() + .map(|x| x.size(true)) + .sum(); + let new_len = extra_field_len + pad_length; + if new_len > u16::MAX as usize { + // Alignment is impossible without exceeding extra field size limits. + // Skip alignment. + None + } else { + // Add an extra field to the extra_field, 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]] = alignment.to_le_bytes(); + let alignment_extra_field = CustomExtraField::new( + true, + UsedExtraField::DataStreamAlignment.as_u16(), + &pad_body, + ); + let alignment_extra_field = ExtraField::Custom(alignment_extra_field); + extra_field_len = self + .extra_fields + .local_extra_fields() + .map(|x| x.size(true)) + .sum::() + + alignment_extra_field.size(true); + debug_assert_eq!((extra_field_len as u64 + header_end) % align, 0); + Some(alignment_extra_field) + } + } else { + None + } + } else { + None + }; + self.extra_data_start = Some(header_end); + let data_start = header_end + extra_field_len as u64; + self.data_start.take(); + self.data_start.get_or_init(|| data_start); + let block = ZipLocalEntryBlock { + version_made_by: self.version_needed(), + flags: self.flags(file_name_raw), + compression_method, + last_mod_time: last_modified_time.timepart(), + last_mod_date: last_modified_time.datepart(), + crc32: self.crc32, + compressed_size, + uncompressed_size, + file_name_length: file_name_raw + .len() + .try_into() + .map_err(std::io::Error::other)?, + extra_field_length: extra_field_len + .try_into() + .map_err(|_| invalid!("Extra data field is too large"))?, + }; + block.write(writer)?; + writer.write_all(file_name_raw)?; + let local_extra_fields = self.extra_fields.local_extra_fields_mut(); + let mut bytes_written = 0; + for one_extra_field in local_extra_fields { + one_extra_field.write(&mut *writer, true)?; + if let ExtraField::AeXEncryption(AexEncryption { + aes_extra_field_start, + .. + }) = one_extra_field + { + *aes_extra_field_start = Some(bytes_written); + } + bytes_written += one_extra_field.size(true); } - cursor += 4 + len; - } - if cursor < extra_field.len() { - new_extra.extend_from_slice(&extra_field[cursor..]); + if let Some(alignment_extra_field) = opt_alignment_field { + alignment_extra_field.write(writer, true)?; + } + Ok(()) } - new_extra } /// Wrapper around a [Write] implementation that implements the [Seek] trait, but where seeking @@ -3299,29 +3415,28 @@ mod tests { #[cfg(all(feature = "_deflate-any", feature = "aes-crypto"))] #[test] - fn test_fuzz_failure_2024_05_08() -> ZipResult<()> { + fn test_fuzz_failure_2024_05_08() { let mut first_writer = ZipWriter::new(Cursor::new(Vec::new())); let mut second_writer = ZipWriter::new(Cursor::new(Vec::new())); let options = SimpleFileOptions::default() .compression_method(Stored) .with_alignment(46036); - second_writer.add_symlink("\0", "", options)?; - let second_archive = second_writer.finish_into_readable()?.into_inner(); - let mut second_writer = ZipWriter::new_append(second_archive)?; + second_writer.add_symlink("\0", "", options).unwrap(); + let second_archive = second_writer.finish_into_readable().unwrap().into_inner(); + let mut second_writer = ZipWriter::new_append(second_archive).unwrap(); let options = SimpleFileOptions::default() .compression_method(CompressionMethod::Deflated) .large_file(true) .with_alignment(46036) .with_aes_encryption(crate::AesMode::Aes128, "\0\0"); - second_writer.add_symlink("", "", options)?; - let second_archive = second_writer.finish_into_readable()?.into_inner(); - let mut second_writer = ZipWriter::new_append(second_archive)?; + second_writer.add_symlink("", "", options).unwrap(); + let second_archive = second_writer.finish_into_readable().unwrap().into_inner(); + let mut second_writer = ZipWriter::new_append(second_archive).unwrap(); let options = SimpleFileOptions::default().compression_method(Stored); - second_writer.start_file(" ", options)?; - let second_archive = second_writer.finish_into_readable()?; - first_writer.merge_archive(second_archive)?; - let _ = ZipArchive::new(first_writer.finish()?)?; - Ok(()) + second_writer.start_file(" ", options).unwrap(); + let second_archive = second_writer.finish_into_readable().unwrap(); + first_writer.merge_archive(second_archive).unwrap(); + let _ = ZipArchive::new(first_writer.finish().unwrap()).unwrap(); } #[cfg(all(feature = "_bzip2_any", not(miri)))] @@ -3420,8 +3535,6 @@ mod tests { extended_options: ExtendedFileOptions { extra_fields: vec![ CustomExtraField::new_from_raw(true, &[1, 41, 4, 0, 1, 255, 245, 117]).unwrap(), - CustomExtraField::new_from_raw(true, &[117, 112, 5, 0, 80, 255, 149, 255, 247]) - .unwrap(), ] .into(), file_comment: None, @@ -3619,6 +3732,12 @@ mod tests { #[cfg(all(feature = "_deflate-any", feature = "aes-crypto"))] #[test] + /// Created on + /// https://github.com/zip-rs/zip2/commit/2a035f520104b7df343b02726c35ebd53ac2e15a + /// Then changed in + /// https://github.com/zip-rs/zip2/commit/16aa9bcddf58887849aaa36caf6e377eae3636dd + /// Because of + /// https://github.com/zip-rs/zip2/commit/e3ccaf6e005a855e87d2244a5ccdff9c18279b0c fn test_fuzz_crash_2024_06_14d() -> ZipResult<()> { use crate::AesMode::Aes256; use crate::types::AesVendorVersion; @@ -3656,10 +3775,14 @@ mod tests { alignment: 0xFFFF, ..Default::default() }; - assert!(writer.add_directory_from_path("", options).is_err()); + assert!(writer.add_directory_from_path("", options).is_ok()); Ok(()) } + // Created on + // https://github.com/zip-rs/zip2/commit/e23f676c40ffef9ac62ad8a82cf4bcdd43b7a4e7 + // Then changed + // https://github.com/zip-rs/zip2/commit/e3ccaf6e005a855e87d2244a5ccdff9c18279b0c #[test] fn test_fuzz_crash_2024_06_14e() -> ZipResult<()> { use crate::write::CustomExtraField; @@ -3691,7 +3814,7 @@ mod tests { alignment: 0xFFFF, ..Default::default() }; - assert!(writer.add_directory_from_path("", options).is_err()); + assert!(writer.add_directory_from_path("", options).is_ok()); let _ = writer.finish_into_readable()?; Ok(()) } @@ -4580,4 +4703,12 @@ mod tests { // we have the feature "unreserved" so the parsing will succeed assert!(writer.start_file_from_path("", options).is_ok()); } + + #[test] + fn test_max_len_extra_field() { + use crate::spec::Magic; + use crate::spec::ZipLocalEntryBlock; + assert_eq!(std::mem::size_of::(), 4); + assert_eq!(std::mem::size_of::(), 26); + } } diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs index 0a357f005..e8be9dead 100644 --- a/tests/end_to_end.rs +++ b/tests/end_to_end.rs @@ -139,7 +139,7 @@ fn write_test_archive(file: &mut Cursor>, method: CompressionMethod, sha zip.write_all(b"Hello, World!\n").unwrap(); options - .add_extra_data(0xbeef, EXTRA_DATA.to_owned().into_boxed_slice(), false) + .add_extra_field(0xbeef, EXTRA_DATA.to_owned().into_boxed_slice(), false) .unwrap(); zip.start_file("test_with_extra_data/🐢.txt", options) @@ -170,18 +170,19 @@ fn check_test_archive(zip_file: R) -> ZipResult Vec { let local_header = [ @@ -143,3 +143,38 @@ fn test_crc32_extra_field_comment() { assert!(archive.is_ok()); archive.unwrap(); } + +#[test] +fn test_extra_field_too_long() { + use std::io::Cursor; + use zip::ZipWriter; + // u16::MAX = 65535 + // Size of extra field header and length = 4 + // Magic = 4 + // ZipLocalHeader = 26 + // filename = 1 + let tests = [ + // should NOT fail since value is less than u16::MAX + (vec![1; 65535 - 4 - 4 - 26 - 1 - 1], false), + // should fail since value is exactly u16::MAX + (vec![1; 65535 - 4 - 4 - 26 - 1], true), + // should fail since value is more than u16::MAX + (vec![1; 65535 - 4 - 4 - 26], true), + ]; + for (extra_field, should_fail) in tests { + let mut writer = ZipWriter::new(Cursor::new(Vec::new())); + writer.set_flush_on_finish_file(false); + let mut options = FileOptions::default(); + eprintln!( + "Extra_field_len = {}, total = {}", + extra_field.len(), + extra_field.len() + 4 + 4 + 26 + 1 + ); + options.add_extra_field(0x1e51, extra_field, true).unwrap(); + if should_fail { + writer.start_file("a", options).unwrap_err(); + } else { + writer.start_file("a", options).unwrap(); + } + } +}