From 3ff65cd0d05940c54af61d6ebfe455d99aed8df7 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 18:01:47 -0600 Subject: [PATCH 01/52] feat: remove magic --- src/extra_fields/aex_encryption.rs | 17 +++---- src/spec.rs | 76 ++++++++++++++---------------- src/types.rs | 36 -------------- 3 files changed, 42 insertions(+), 87 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index 17f223dc8..e9daaab39 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -4,11 +4,11 @@ use crate::AesMode; use crate::CompressionMethod; use crate::extra_fields::UsedExtraField; use crate::result::ZipError; -use crate::spec::FixedSizeBlock; use crate::spec::Pod; use crate::to_and_from_le; use crate::types::AesVendorVersion; use crate::{from_le, to_le}; +use std::io::Write; #[derive(Copy, Clone)] #[repr(packed, C)] @@ -21,19 +21,16 @@ pub(crate) struct AexEncryption { compression_method: u16, } -unsafe impl Pod for AexEncryption {} +unsafe impl Pod for AexEncryption{} -impl FixedSizeBlock for AexEncryption { - type Magic = u16; - const MAGIC: Self::Magic = UsedExtraField::AeXEncryption.as_u16(); +impl AexEncryption { - fn magic(self) -> Self::Magic { - Self::MAGIC + pub(crate) fn write(self, writer: &mut T) -> ZipResult<()> { + let block = self.to_le(); + writer.write_all(block.as_bytes())?; + Ok(()) } - const WRONG_MAGIC_ERROR: ZipError = - ZipError::InvalidArchive(std::borrow::Cow::Borrowed("Wrong AES header ID")); - to_and_from_le![ (header_id, u16), (data_size, u16), diff --git a/src/spec.rs b/src/spec.rs index 71990982c..9e5a222de 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -202,11 +202,28 @@ pub(crate) unsafe trait Pod: Copy + 'static { } } +#[derive(Copy, Clone)] +#[repr(C, packed)] +struct BlockWithMagic { + magic: Magic, + inner: T, +} +unsafe impl Pod for BlockWithMagic {} + +impl BlockWithMagic { + fn to_le(mut self) -> Self { + self.magic = self.magic.to_le(); + self.inner = self.inner.to_le(); + self + } +} + pub(crate) trait FixedSizeBlock: Pod { - type Magic: Copy + Eq; - const MAGIC: Self::Magic; + const MAGIC: Magic; - fn magic(self) -> Self::Magic; + fn magic(self) -> Magic { + Self::MAGIC + } const WRONG_MAGIC_ERROR: ZipError; @@ -214,16 +231,20 @@ pub(crate) trait FixedSizeBlock: Pod { fn from_le(self) -> Self; fn parse(reader: &mut R) -> ZipResult { - let mut block = Self::zeroed(); - if let Err(e) = reader.read_exact(block.as_bytes_mut()) { + let mut block_with_magic = BlockWithMagic::zeroed(); + if let Err(e) = reader.read_exact(block_with_magic.as_bytes_mut()) { if e.kind() == io::ErrorKind::UnexpectedEof { return Err(invalid!("Unexpected end of {}", type_name::())); } return Err(e.into()); } + let BlockWithMagic { + magic, + inner: block, + } = block_with_magic; let block = Self::from_le(block); - if block.magic() != Self::MAGIC { + if magic != Self::MAGIC { return Err(Self::WRONG_MAGIC_ERROR); } Ok(block) @@ -232,8 +253,14 @@ pub(crate) trait FixedSizeBlock: Pod { fn to_le(self) -> Self; fn write(self, writer: &mut T) -> ZipResult<()> { - let block = self.to_le(); + let block = BlockWithMagic { + magic: self.magic(), + inner: self, + }; + let block = block.to_le(); writer.write_all(block.as_bytes())?; + let BlockWithMagic { inner, ..} = block; + eprintln!("{} {}", block.as_bytes().len(), inner.as_bytes().len()); Ok(()) } } @@ -290,7 +317,6 @@ macro_rules! to_and_from_le { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct Zip32CDEBlock { - magic: Magic, pub disk_number: u16, pub disk_with_central_directory: u16, pub number_of_files_on_this_disk: u16, @@ -303,18 +329,11 @@ pub(crate) struct Zip32CDEBlock { unsafe impl Pod for Zip32CDEBlock {} impl FixedSizeBlock for Zip32CDEBlock { - type Magic = Magic; const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_END_SIGNATURE; - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid digital signature header"); to_and_from_le![ - (magic, Magic), (disk_number, u16), (disk_with_central_directory, u16), (number_of_files_on_this_disk, u16), @@ -348,7 +367,6 @@ impl Zip32CentralDirectoryEnd { zip_file_comment, } = self; let block = Zip32CDEBlock { - magic: Zip32CDEBlock::MAGIC, disk_number, disk_with_central_directory, number_of_files_on_this_disk, @@ -414,7 +432,6 @@ impl Zip32CentralDirectoryEnd { #[derive(Copy, Clone)] #[repr(packed, C)] pub(crate) struct Zip64CDELocatorBlock { - magic: Magic, pub disk_with_central_directory: u32, pub end_of_central_directory_offset: u64, pub number_of_disks: u32, @@ -423,18 +440,11 @@ pub(crate) struct Zip64CDELocatorBlock { unsafe impl Pod for Zip64CDELocatorBlock {} impl FixedSizeBlock for Zip64CDELocatorBlock { - type Magic = Magic; const MAGIC: Magic = Magic::ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE; - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 locator digital signature header"); to_and_from_le![ - (magic, Magic), (disk_with_central_directory, u32), (end_of_central_directory_offset, u64), (number_of_disks, u32), @@ -471,7 +481,6 @@ impl Zip64CentralDirectoryEndLocator { number_of_disks, } = self; Zip64CDELocatorBlock { - magic: Zip64CDELocatorBlock::MAGIC, disk_with_central_directory, end_of_central_directory_offset, number_of_disks, @@ -486,7 +495,6 @@ impl Zip64CentralDirectoryEndLocator { #[derive(Copy, Clone)] #[repr(packed, C)] pub(crate) struct Zip64CDEBlock { - magic: Magic, pub record_size: u64, pub version_made_by: u16, pub version_needed_to_extract: u16, @@ -501,17 +509,11 @@ pub(crate) struct Zip64CDEBlock { unsafe impl Pod for Zip64CDEBlock {} impl FixedSizeBlock for Zip64CDEBlock { - type Magic = Magic; const MAGIC: Magic = Magic::ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE; - fn magic(self) -> Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid digital signature header"); to_and_from_le![ - (magic, Magic), (record_size, u64), (version_made_by, u16), (version_needed_to_extract, u16), @@ -601,7 +603,6 @@ impl Zip64CentralDirectoryEnd { ( Zip64CDEBlock { - magic: Zip64CDEBlock::MAGIC, record_size, version_made_by, version_needed_to_extract, @@ -875,30 +876,23 @@ mod test { #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[repr(packed, C)] pub struct TestBlock { - magic: Magic, pub file_name_length: u16, } unsafe impl Pod for TestBlock {} impl FixedSizeBlock for TestBlock { - type Magic = Magic; const MAGIC: Magic = Magic::literal(0x01111); - fn magic(self) -> Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("unreachable"); - to_and_from_le![(magic, Magic), (file_name_length, u16)]; + to_and_from_le![(file_name_length, u16)]; } /// Demonstrate that a block object can be safely written to memory and deserialized back out. #[test] fn block_serde() { let block = TestBlock { - magic: TestBlock::MAGIC, file_name_length: 3, }; let mut c = Cursor::new(Vec::new()); diff --git a/src/types.rs b/src/types.rs index f51d65eae..5e83c5e04 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1001,7 +1001,6 @@ impl ZipFileData { .last_modified_time .unwrap_or_else(DateTime::default_for_write); Ok(ZipLocalEntryBlock { - magic: ZipLocalEntryBlock::MAGIC, version_made_by: self.version_needed(), flags: self.flags(), compression_method: self.compression_method.serialize_to_u16(), @@ -1055,7 +1054,6 @@ impl ZipFileData { let version_to_extract = self.version_needed(); let version_made_by = u16::from(self.version_made_by).max(version_to_extract); Ok(ZipCentralEntryBlock { - magic: ZipCentralEntryBlock::MAGIC, version_made_by: ((self.system as u16) << 8) | version_made_by, version_to_extract, flags: self.flags(), @@ -1108,7 +1106,6 @@ impl ZipFileData { pub(crate) fn data_descriptor_block(&self) -> ZipDataDescriptorBlock { ZipDataDescriptorBlock { - magic: ZipDataDescriptorBlock::MAGIC, crc32: self.crc32, compressed_size: self.compressed_size as u32, uncompressed_size: self.uncompressed_size as u32, @@ -1117,7 +1114,6 @@ impl ZipFileData { pub(crate) fn zip64_data_descriptor_block(&self) -> Zip64DataDescriptorBlock { Zip64DataDescriptorBlock { - magic: Zip64DataDescriptorBlock::MAGIC, crc32: self.crc32, compressed_size: self.compressed_size, uncompressed_size: self.uncompressed_size, @@ -1128,7 +1124,6 @@ impl ZipFileData { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipCentralEntryBlock { - magic: spec::Magic, pub version_made_by: u16, pub version_to_extract: u16, pub flags: u16, @@ -1150,18 +1145,11 @@ pub(crate) struct ZipCentralEntryBlock { unsafe impl Pod for ZipCentralEntryBlock {} impl FixedSizeBlock for ZipCentralEntryBlock { - type Magic = Magic; const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE; - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header"); to_and_from_le![ - (magic, Magic), (version_made_by, u16), (version_to_extract, u16), (flags, u16), @@ -1184,7 +1172,6 @@ impl FixedSizeBlock for ZipCentralEntryBlock { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipLocalEntryBlock { - magic: spec::Magic, pub version_made_by: u16, pub flags: u16, pub compression_method: u16, @@ -1200,18 +1187,11 @@ pub(crate) struct ZipLocalEntryBlock { unsafe impl Pod for ZipLocalEntryBlock {} impl FixedSizeBlock for ZipLocalEntryBlock { - type Magic = Magic; const MAGIC: Magic = Magic::LOCAL_FILE_HEADER_SIGNATURE; - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header"); to_and_from_le![ - (magic, Magic), (version_made_by, u16), (flags, u16), (compression_method, u16), @@ -1228,7 +1208,6 @@ impl FixedSizeBlock for ZipLocalEntryBlock { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipDataDescriptorBlock { - magic: spec::Magic, pub crc32: u32, pub compressed_size: u32, pub uncompressed_size: u32, @@ -1237,18 +1216,11 @@ pub(crate) struct ZipDataDescriptorBlock { unsafe impl Pod for ZipDataDescriptorBlock {} impl FixedSizeBlock for ZipDataDescriptorBlock { - type Magic = Magic; const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE; - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid data descriptor header"); to_and_from_le![ - (magic, Magic), (crc32, u32), (compressed_size, u32), (uncompressed_size, u32), @@ -1258,7 +1230,6 @@ impl FixedSizeBlock for ZipDataDescriptorBlock { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct Zip64DataDescriptorBlock { - magic: spec::Magic, pub crc32: u32, pub compressed_size: u64, pub uncompressed_size: u64, @@ -1267,18 +1238,11 @@ pub(crate) struct Zip64DataDescriptorBlock { unsafe impl Pod for Zip64DataDescriptorBlock {} impl FixedSizeBlock for Zip64DataDescriptorBlock { - type Magic = Magic; const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE; - #[inline] - fn magic(self) -> spec::Magic { - self.magic - } - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header"); to_and_from_le![ - (magic, spec::Magic), (crc32, u32), (compressed_size, u64), (uncompressed_size, u64), From 1810460aef256a9de9c10a680e3a698408c1f026 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 18:36:07 -0600 Subject: [PATCH 02/52] add size of magic --- src/extra_fields/aex_encryption.rs | 3 +-- src/spec.rs | 6 +++--- src/types.rs | 2 +- src/write.rs | 9 +++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index e9daaab39..177001ac1 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -9,6 +9,7 @@ use crate::to_and_from_le; use crate::types::AesVendorVersion; use crate::{from_le, to_le}; use std::io::Write; +use crate::result::ZipResult; #[derive(Copy, Clone)] #[repr(packed, C)] @@ -39,9 +40,7 @@ impl AexEncryption { (aes_mode, u8), (compression_method, u16) ]; -} -impl AexEncryption { pub(crate) fn new( version: AesVendorVersion, aes_mode: AesMode, diff --git a/src/spec.rs b/src/spec.rs index 9e5a222de..c8b247202 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -697,11 +697,11 @@ pub(crate) fn find_central_directory( reader: &mut (impl Read + Seek + ?Sized), eocd_offset: u64, ) -> ZipResult<(u64, Zip64CentralDirectoryEndLocator)> { - if eocd_offset < mem::size_of::() as u64 { + if eocd_offset < (mem::size_of::() + mem::size_of::()) as u64 { return Err(invalid!("EOCD64 Locator does not fit in file")); } - let locator64_offset = eocd_offset - mem::size_of::() as u64; + let locator64_offset = eocd_offset - (mem::size_of::() + mem::size_of::()) as u64; reader.seek(io::SeekFrom::Start(locator64_offset))?; let locator64 = Zip64CentralDirectoryEndLocator::parse(reader); @@ -830,7 +830,7 @@ pub(crate) fn find_central_directory( < eocd64 .number_of_files .saturating_mul( - mem::size_of::() as u64 + (mem::size_of::() + mem::size_of::()) as u64 ) .saturating_add(eocd64.central_directory_offset) { diff --git a/src/types.rs b/src/types.rs index 5e83c5e04..eb9e6016f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -638,7 +638,7 @@ impl ZipFileData { // easily overflow a u16. u64::from(block.file_name_length) + u64::from(block.extra_field_length); let data_start = - self.header_start + size_of::() as u64 + variable_fields_len; + self.header_start + (size_of::() + size_of::()) as u64 + variable_fields_len; // Set the value so we don't have to read it again. match self.data_start.set(data_start) { diff --git a/src/write.rs b/src/write.rs index f974336f6..c9d7b18c5 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,5 +1,6 @@ //! Writing a ZIP archive +use crate::spec::Magic; use crate::compression::CompressionMethod; use crate::extra_fields::AexEncryption; use crate::extra_fields::UsedExtraField; @@ -902,7 +903,7 @@ impl ZipWriter { new_data.file_name_raw = dest_name_raw.into(); new_data.header_start = write_position; let extra_data_start = write_position - + size_of::() as u64 + + (mem::size_of::() + size_of::()) as u64 + new_data.file_name_raw.len() as u64; new_data.extra_data_start = Some(extra_data_start); if let Some(extra) = &src_data.extra_field { @@ -1212,7 +1213,7 @@ impl ZipWriter { )?; } let header_end = - header_start + size_of::() as u64 + name.to_string().len() as u64; + header_start + (mem::size_of::() + size_of::()) as u64 + name.to_string().len() as u64; if options.alignment > 1 { let extra_data_end = header_end + extra_data.len() as u64; @@ -1853,7 +1854,7 @@ impl ZipWriter { writer.seek(SeekFrom::Start(central_start))?; writer.write_u32_le(0)?; writer.seek(SeekFrom::Start( - footer_end - size_of::() as u64 - self.comment.len() as u64, + footer_end - (mem::size_of::() + size_of::()) as u64 - self.comment.len() as u64, ))?; writer.write_u32_le(0)?; @@ -2443,7 +2444,7 @@ impl ZipFileData { ))?; let zip64_extra_field_start = self.header_start - + size_of::() as u64 + + (size_of::() + size_of::()) as u64 + self.file_name_raw.len() as u64; writer.seek(SeekFrom::Start(zip64_extra_field_start))?; From 81a10276f1bdd8e19fadc2784c4dbcaf8bbae32e Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 21:33:02 -0600 Subject: [PATCH 03/52] cargo fmt --- src/extra_fields/aex_encryption.rs | 5 ++--- src/spec.rs | 13 +++++++++---- src/types.rs | 5 +++-- src/write.rs | 13 ++++++++----- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index 177001ac1..30d174a09 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -4,12 +4,12 @@ use crate::AesMode; use crate::CompressionMethod; use crate::extra_fields::UsedExtraField; use crate::result::ZipError; +use crate::result::ZipResult; use crate::spec::Pod; use crate::to_and_from_le; use crate::types::AesVendorVersion; use crate::{from_le, to_le}; use std::io::Write; -use crate::result::ZipResult; #[derive(Copy, Clone)] #[repr(packed, C)] @@ -22,10 +22,9 @@ pub(crate) struct AexEncryption { compression_method: u16, } -unsafe impl Pod for AexEncryption{} +unsafe impl Pod for AexEncryption {} impl AexEncryption { - pub(crate) fn write(self, writer: &mut T) -> ZipResult<()> { let block = self.to_le(); writer.write_all(block.as_bytes())?; diff --git a/src/spec.rs b/src/spec.rs index c8b247202..21ee5e699 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -259,7 +259,7 @@ pub(crate) trait FixedSizeBlock: Pod { }; let block = block.to_le(); writer.write_all(block.as_bytes())?; - let BlockWithMagic { inner, ..} = block; + let BlockWithMagic { inner, .. } = block; eprintln!("{} {}", block.as_bytes().len(), inner.as_bytes().len()); Ok(()) } @@ -697,11 +697,14 @@ pub(crate) fn find_central_directory( reader: &mut (impl Read + Seek + ?Sized), eocd_offset: u64, ) -> ZipResult<(u64, Zip64CentralDirectoryEndLocator)> { - if eocd_offset < (mem::size_of::() + mem::size_of::()) as u64 { + if eocd_offset + < (mem::size_of::() + mem::size_of::()) as u64 + { return Err(invalid!("EOCD64 Locator does not fit in file")); } - let locator64_offset = eocd_offset - (mem::size_of::() + mem::size_of::()) as u64; + let locator64_offset = eocd_offset + - (mem::size_of::() + mem::size_of::()) as u64; reader.seek(io::SeekFrom::Start(locator64_offset))?; let locator64 = Zip64CentralDirectoryEndLocator::parse(reader); @@ -830,7 +833,9 @@ pub(crate) fn find_central_directory( < eocd64 .number_of_files .saturating_mul( - (mem::size_of::() + mem::size_of::()) as u64 + (mem::size_of::() + + mem::size_of::()) + as u64, ) .saturating_add(eocd64.central_directory_offset) { diff --git a/src/types.rs b/src/types.rs index eb9e6016f..e8638fd74 100644 --- a/src/types.rs +++ b/src/types.rs @@ -637,8 +637,9 @@ impl ZipFileData { // Each of these fields must be converted to u64 before adding, as the result may // easily overflow a u16. u64::from(block.file_name_length) + u64::from(block.extra_field_length); - let data_start = - self.header_start + (size_of::() + size_of::()) as u64 + variable_fields_len; + let data_start = self.header_start + + (size_of::() + size_of::()) as u64 + + variable_fields_len; // Set the value so we don't have to read it again. match self.data_start.set(data_start) { diff --git a/src/write.rs b/src/write.rs index c9d7b18c5..c0f8eb8ad 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,12 +1,12 @@ //! Writing a ZIP archive -use crate::spec::Magic; use crate::compression::CompressionMethod; use crate::extra_fields::AexEncryption; use crate::extra_fields::UsedExtraField; use crate::extra_fields::Zip64ExtendedInformation; use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; use crate::result::{ZipError, ZipResult, invalid}; +use crate::spec::Magic; use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock}; use crate::types::ffi::S_IFLNK; use crate::types::{ @@ -903,7 +903,7 @@ impl ZipWriter { new_data.file_name_raw = dest_name_raw.into(); new_data.header_start = write_position; let extra_data_start = write_position - + (mem::size_of::() + size_of::()) as u64 + + (size_of::() + size_of::()) as u64 + new_data.file_name_raw.len() as u64; new_data.extra_data_start = Some(extra_data_start); if let Some(extra) = &src_data.extra_field { @@ -1212,8 +1212,9 @@ impl ZipWriter { &body, )?; } - let header_end = - header_start + (mem::size_of::() + size_of::()) as u64 + name.to_string().len() as u64; + let header_end = header_start + + (size_of::() + size_of::()) as u64 + + name.to_string().len() as u64; if options.alignment > 1 { let extra_data_end = header_end + extra_data.len() as u64; @@ -1854,7 +1855,9 @@ impl ZipWriter { writer.seek(SeekFrom::Start(central_start))?; writer.write_u32_le(0)?; writer.seek(SeekFrom::Start( - footer_end - (mem::size_of::() + size_of::()) as u64 - self.comment.len() as u64, + footer_end + - (size_of::() + size_of::()) as u64 + - self.comment.len() as u64, ))?; writer.write_u32_le(0)?; From 28cb5ee9ba77c65dc55d323642636b7e8dc83135 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 21:51:22 -0600 Subject: [PATCH 04/52] change name --- src/write.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/write.rs b/src/write.rs index c0f8eb8ad..306b3352f 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2437,7 +2437,7 @@ impl ZipFileData { } fn update_local_zip64_extra_field(&mut self, writer: &mut T) -> ZipResult<()> { - let block = Zip64ExtendedInformation::local_header( + let zip64_block = Zip64ExtendedInformation::local_header( self.large_file, self.uncompressed_size, self.compressed_size, @@ -2451,17 +2451,8 @@ impl ZipFileData { + self.file_name_raw.len() as u64; writer.seek(SeekFrom::Start(zip64_extra_field_start))?; - let block = block.serialize(); - writer.write_all(&block)?; - - let Some(ref mut extra_field) = self.extra_field else { - return Err(invalid!( - "update_local_zip64_extra_field called on a file that has no extra-data field" - )); - }; - let extra_field = Arc::make_mut(extra_field); - extra_field[..block.len()].copy_from_slice(&block); - + let zip64_block = zip64_block.serialize(); + writer.write_all(&zip64_block)?; Ok(()) } From 1ab12f137982172b44a9376a645266806ed83285 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 22:00:27 -0600 Subject: [PATCH 05/52] cleaner to use size_of --- src/write.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/write.rs b/src/write.rs index 306b3352f..fb7256786 100644 --- a/src/write.rs +++ b/src/write.rs @@ -396,7 +396,7 @@ impl ExtendedFileOptions { header_id: u16, data: &[u8], ) -> Result<(), ZipError> { - vec.reserve_exact(data.len() + 4); + vec.reserve_exact(data.len() + size_of::() + size_of::()); vec.write_u16_le(header_id)?; vec.write_u16_le(data.len() as u16)?; vec.write_all(data)?; From 9fd3225c28170af9ba97df441730e86b1a1c7c6e Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 23:13:58 -0600 Subject: [PATCH 06/52] update code --- src/extra_fields/aex_encryption.rs | 34 ++++++++++--------- src/read.rs | 31 +++++++---------- src/read/stream.rs | 54 +++++++++++++++++++++++++++--- src/spec.rs | 1 + src/write.rs | 11 +++--- 5 files changed, 86 insertions(+), 45 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index 30d174a09..da393bc1f 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -3,13 +3,9 @@ use crate::AesMode; use crate::CompressionMethod; use crate::extra_fields::UsedExtraField; -use crate::result::ZipError; -use crate::result::ZipResult; use crate::spec::Pod; -use crate::to_and_from_le; +use crate::to_le; use crate::types::AesVendorVersion; -use crate::{from_le, to_le}; -use std::io::Write; #[derive(Copy, Clone)] #[repr(packed, C)] @@ -25,20 +21,26 @@ pub(crate) struct AexEncryption { unsafe impl Pod for AexEncryption {} impl AexEncryption { - pub(crate) fn write(self, writer: &mut T) -> ZipResult<()> { + pub(crate) fn serialize(self) -> Vec { let block = self.to_le(); - writer.write_all(block.as_bytes())?; - Ok(()) + block.as_bytes().to_vec() } - to_and_from_le![ - (header_id, u16), - (data_size, u16), - (version, u16), - (vendor_id, u16), - (aes_mode, u8), - (compression_method, u16) - ]; + #[inline(always)] + fn to_le(mut self) -> Self { + to_le![ + self, + [ + (header_id, u16), + (data_size, u16), + (version, u16), + (vendor_id, u16), + (aes_mode, u8), + (compression_method, u16) + ] + ]; + self + } pub(crate) fn new( version: AesVendorVersion, diff --git a/src/read.rs b/src/read.rs index e45cc11f4..f8dde1f56 100644 --- a/src/read.rs +++ b/src/read.rs @@ -5,6 +5,7 @@ use crate::cp437::FromCp437; use crate::crc32::Crc32Reader; use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs, UsedExtraField}; use crate::result::{ZipError, ZipResult, invalid}; +use crate::spec::Magic; use crate::spec::{ self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, Pod, ZIP64_BYTES_THR, ZipFlags, }; @@ -2161,15 +2162,18 @@ pub fn read_zipfile_from_stream(reader: &mut R) -> ZipResult()]; + reader.read_exact(&mut magic_buf)?; - match block.magic().from_le() { + match Magic::from_le_bytes(magic_buf) { spec::Magic::LOCAL_FILE_HEADER_SIGNATURE => (), spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None), _ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR), } + let mut block = ZipLocalEntryBlock::zeroed(); + reader.read_exact(block.as_bytes_mut())?; + let block = block.from_le(); let mut result = ZipFileData::from_local_block(block, reader)?; @@ -2290,15 +2294,18 @@ pub fn read_zipfile_from_stream_with_compressed_size( reader: &mut R, compressed_size: u64, ) -> ZipResult>> { - let mut block = ZipLocalEntryBlock::zeroed(); - reader.read_exact(block.as_bytes_mut())?; + let mut magic_buf = [0; size_of::()]; + reader.read_exact(&mut magic_buf)?; - match block.magic().from_le() { + match Magic::from_le_bytes(magic_buf) { spec::Magic::LOCAL_FILE_HEADER_SIGNATURE => (), spec::Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None), _ => return Err(ZipLocalEntryBlock::WRONG_MAGIC_ERROR), } + let mut block = ZipLocalEntryBlock::zeroed(); + reader.read_exact(block.as_bytes_mut())?; + let block = block.from_le(); let mut result = ZipFileData::from_local_block(block, reader)?; @@ -2381,18 +2388,6 @@ mod test { assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77); } - #[test] - fn zip_read_streaming() { - use super::read_zipfile_from_stream; - - let mut reader = Cursor::new(include_bytes!("../tests/data/mimetype.zip")); - loop { - if read_zipfile_from_stream(&mut reader).unwrap().is_none() { - break; - } - } - } - #[test] fn zip_clone() { use super::ZipArchive; diff --git a/src/read/stream.rs b/src/read/stream.rs index 416f11a50..c8abac168 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -269,7 +269,7 @@ mod test { } #[test] - fn zip_read_streaming() { + fn zip_read_streaming_visitor() { let reader = ZipStreamReader::new(Cursor::new(include_bytes!("../../tests/data/mimetype.zip"))); @@ -393,12 +393,58 @@ mod test { #[test] fn test_can_create_destination() -> ZipResult<()> { - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../../tests/data/mimetype.zip")); - let reader = ZipStreamReader::new(v.as_slice()); + let v = include_bytes!("../../tests/data/mimetype.zip"); + let reader = ZipStreamReader::new(v.as_ref()); let dest = TempDir::with_prefix("stream_test_can_create_destination").unwrap(); reader.extract(&dest)?; assert!(dest.path().join("mimetype").exists()); Ok(()) } + + #[test] + fn zip_read_streaming() { + use super::read_zipfile_from_stream; + + let mut reader = Cursor::new(include_bytes!("../../tests/data/mimetype.zip")); + loop { + if read_zipfile_from_stream(&mut reader).unwrap().is_none() { + break; + } + } + } + + #[test] + #[cfg(feature = "deflate")] + fn zip_read_streaming_compressed() { + use crate::read::read_zipfile_from_stream_with_compressed_size; + use std::io::Write; + + let compression_method = crate::CompressionMethod::Deflated; + let options = crate::write::SimpleFileOptions::default() + .compression_method(compression_method) + .unix_permissions(0o755); + + let mut bytes = Vec::new(); + let mut writer = crate::ZipWriter::new(std::io::Cursor::new(&mut bytes)); + writer.start_file("file.txt", options).unwrap(); + write!(&mut writer, "{}", "test-".repeat(100)).unwrap(); + writer.finish().unwrap(); + eprintln!("{:x?}", bytes); + + let compressed_size = u32::from_le_bytes(bytes[18..22].try_into().unwrap()); + let uncompressed_size = u32::from_le_bytes(bytes[22..26].try_into().unwrap()); + + assert_eq!(compressed_size, 14); + assert_eq!(uncompressed_size as usize, "test-".len() * 100); + + let mut reader = Cursor::new(bytes); + loop { + if read_zipfile_from_stream_with_compressed_size(&mut reader, compressed_size as u64) + .unwrap() + .is_none() + { + break; + } + } + } } diff --git a/src/spec.rs b/src/spec.rs index 21ee5e699..c26a2cf81 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -33,6 +33,7 @@ impl Magic { } #[allow(clippy::wrong_self_convention)] + #[allow(unused)] #[inline(always)] pub fn from_le(self) -> Self { Self(u32::from_le(self.0)) diff --git a/src/write.rs b/src/write.rs index fb7256786..7234e2b99 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2385,12 +2385,9 @@ fn update_aes_extra_data( extra_data_start + file.aes_extra_data_start, ))?; - let mut buf = [0u8; size_of::()]; - let aes_extra_field = AexEncryption::new(*version, *aes_mode, *compression_method); - - aes_extra_field.write(&mut buf.as_mut())?; - writer.write_all(&buf)?; + let buf = &aes_extra_field.serialize(); + writer.write_all(buf)?; let aes_extra_data_start = file.aes_extra_data_start as usize; let Some(ref mut extra_field) = file.extra_field else { @@ -2400,7 +2397,7 @@ fn update_aes_extra_data( }; let extra_field = Arc::make_mut(extra_field); extra_field[aes_extra_data_start..aes_extra_data_start + size_of::()] - .copy_from_slice(&buf); + .copy_from_slice(buf); Ok(()) } @@ -2411,7 +2408,7 @@ impl ZipFileData { writer: &mut T, ) -> ZipResult<()> { writer.seek(SeekFrom::Start( - self.header_start + offset_of!(ZipLocalEntryBlock, crc32) as u64, + self.header_start + (size_of::() + offset_of!(ZipLocalEntryBlock, crc32)) as u64, ))?; writer.write_u32_le(self.crc32)?; if self.large_file { From 9e679e35411a60de32129945aa3a220ed9a25deb Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 23:34:00 -0600 Subject: [PATCH 07/52] remove eprintln --- src/spec.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/spec.rs b/src/spec.rs index c26a2cf81..3f63bfeff 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -260,8 +260,6 @@ pub(crate) trait FixedSizeBlock: Pod { }; let block = block.to_le(); writer.write_all(block.as_bytes())?; - let BlockWithMagic { inner, .. } = block; - eprintln!("{} {}", block.as_bytes().len(), inner.as_bytes().len()); Ok(()) } } From 5e2b3087a487bcee89a3a3027d0cc880e61f2b18 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Tue, 17 Mar 2026 23:39:15 -0600 Subject: [PATCH 08/52] put as boxed --- src/extra_fields/aex_encryption.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index da393bc1f..7026b8ecb 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -21,9 +21,9 @@ pub(crate) struct AexEncryption { unsafe impl Pod for AexEncryption {} impl AexEncryption { - pub(crate) fn serialize(self) -> Vec { + pub(crate) fn serialize(self) -> Box<[u8]> { let block = self.to_le(); - block.as_bytes().to_vec() + block.as_bytes().into() } #[inline(always)] From ac727ab7eebfefd354a7a70021cf7f8fb2a11b1b Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 09:53:34 -0600 Subject: [PATCH 09/52] Update read.rs --- src/read.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/read.rs b/src/read.rs index e45cc11f4..7b7262b40 100644 --- a/src/read.rs +++ b/src/read.rs @@ -368,7 +368,7 @@ pub(crate) fn make_crypto_reader<'a, R: Read + ?Sized>( #[allow(deprecated)] { if let CompressionMethod::Unsupported(_) = data.compression_method { - return unsupported_zip_error("Compression method not supported"); + return Err(ZipError::UnsupportedArchive("Compression method not supported")); } } @@ -731,11 +731,11 @@ impl ZipArchive { }; if dir_info.disk_number != dir_info.disk_with_central_directory { - return unsupported_zip_error("Support for multi-disk files is not implemented"); + return Err(ZipError::UnsupportedArchive("Support for multi-disk files is not implemented")); } if file_capacity.saturating_mul(size_of::()) > isize::MAX as usize { - return unsupported_zip_error("Oversized central directory"); + return Err(ZipError::UnsupportedArchive("Oversized central directory")); } let mut files = Vec::with_capacity(file_capacity); @@ -1387,10 +1387,6 @@ pub struct AesInfo { pub salt: Vec, } -const fn unsupported_zip_error(detail: &'static str) -> ZipResult { - Err(ZipError::UnsupportedArchive(detail)) -} - /// Parse a central directory entry to collect the information for the file. pub(crate) fn central_header_to_zip_file( reader: &mut R, From 10869c977b4f7b490c6f3ee5956e9aaf1767de2a Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 10:38:08 -0600 Subject: [PATCH 10/52] Move to specs --- src/types.rs | 160 --------------------------------------------------- 1 file changed, 160 deletions(-) diff --git a/src/types.rs b/src/types.rs index f51d65eae..4be6a1967 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1125,166 +1125,6 @@ impl ZipFileData { } } -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct ZipCentralEntryBlock { - magic: spec::Magic, - pub version_made_by: u16, - pub version_to_extract: u16, - pub flags: u16, - pub compression_method: u16, - pub last_mod_time: u16, - pub last_mod_date: u16, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, - pub file_name_length: u16, - pub extra_field_length: u16, - pub file_comment_length: u16, - pub disk_number: u16, - pub internal_file_attributes: u16, - pub external_file_attributes: u32, - pub offset: u32, -} - -unsafe impl Pod for ZipCentralEntryBlock {} - -impl FixedSizeBlock for ZipCentralEntryBlock { - type Magic = Magic; - const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE; - - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header"); - - to_and_from_le![ - (magic, Magic), - (version_made_by, u16), - (version_to_extract, u16), - (flags, u16), - (compression_method, u16), - (last_mod_time, u16), - (last_mod_date, u16), - (crc32, u32), - (compressed_size, u32), - (uncompressed_size, u32), - (file_name_length, u16), - (extra_field_length, u16), - (file_comment_length, u16), - (disk_number, u16), - (internal_file_attributes, u16), - (external_file_attributes, u32), - (offset, u32), - ]; -} - -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct ZipLocalEntryBlock { - magic: spec::Magic, - pub version_made_by: u16, - pub flags: u16, - pub compression_method: u16, - pub last_mod_time: u16, - pub last_mod_date: u16, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, - pub file_name_length: u16, - pub extra_field_length: u16, -} - -unsafe impl Pod for ZipLocalEntryBlock {} - -impl FixedSizeBlock for ZipLocalEntryBlock { - type Magic = Magic; - const MAGIC: Magic = Magic::LOCAL_FILE_HEADER_SIGNATURE; - - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header"); - - to_and_from_le![ - (magic, Magic), - (version_made_by, u16), - (flags, u16), - (compression_method, u16), - (last_mod_time, u16), - (last_mod_date, u16), - (crc32, u32), - (compressed_size, u32), - (uncompressed_size, u32), - (file_name_length, u16), - (extra_field_length, u16), - ]; -} - -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct ZipDataDescriptorBlock { - magic: spec::Magic, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, -} - -unsafe impl Pod for ZipDataDescriptorBlock {} - -impl FixedSizeBlock for ZipDataDescriptorBlock { - type Magic = Magic; - const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE; - - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid data descriptor header"); - - to_and_from_le![ - (magic, Magic), - (crc32, u32), - (compressed_size, u32), - (uncompressed_size, u32), - ]; -} - -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct Zip64DataDescriptorBlock { - magic: spec::Magic, - pub crc32: u32, - pub compressed_size: u64, - pub uncompressed_size: u64, -} - -unsafe impl Pod for Zip64DataDescriptorBlock {} - -impl FixedSizeBlock for Zip64DataDescriptorBlock { - type Magic = Magic; - const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE; - - #[inline] - fn magic(self) -> spec::Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header"); - - to_and_from_le![ - (magic, spec::Magic), - (crc32, u32), - (compressed_size, u64), - (uncompressed_size, u64), - ]; -} - /// The encryption specification used to encrypt a file with AES. /// /// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2 From f74100bad41e33bb65788d4349e6256c1c8b4ecb Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 10:39:29 -0600 Subject: [PATCH 11/52] move to spec.rs --- src/spec.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/spec.rs b/src/spec.rs index 71990982c..ff5d4a48d 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -287,6 +287,166 @@ macro_rules! to_and_from_le { }; } +#[derive(Copy, Clone, Debug)] +#[repr(packed, C)] +pub(crate) struct ZipCentralEntryBlock { + magic: spec::Magic, + pub version_made_by: u16, + pub version_to_extract: u16, + pub flags: u16, + pub compression_method: u16, + pub last_mod_time: u16, + pub last_mod_date: u16, + pub crc32: u32, + pub compressed_size: u32, + pub uncompressed_size: u32, + pub file_name_length: u16, + pub extra_field_length: u16, + pub file_comment_length: u16, + pub disk_number: u16, + pub internal_file_attributes: u16, + pub external_file_attributes: u32, + pub offset: u32, +} + +unsafe impl Pod for ZipCentralEntryBlock {} + +impl FixedSizeBlock for ZipCentralEntryBlock { + type Magic = Magic; + const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE; + + #[inline(always)] + fn magic(self) -> Magic { + self.magic + } + + const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header"); + + to_and_from_le![ + (magic, Magic), + (version_made_by, u16), + (version_to_extract, u16), + (flags, u16), + (compression_method, u16), + (last_mod_time, u16), + (last_mod_date, u16), + (crc32, u32), + (compressed_size, u32), + (uncompressed_size, u32), + (file_name_length, u16), + (extra_field_length, u16), + (file_comment_length, u16), + (disk_number, u16), + (internal_file_attributes, u16), + (external_file_attributes, u32), + (offset, u32), + ]; +} + +#[derive(Copy, Clone, Debug)] +#[repr(packed, C)] +pub(crate) struct ZipLocalEntryBlock { + magic: spec::Magic, + pub version_made_by: u16, + pub flags: u16, + pub compression_method: u16, + pub last_mod_time: u16, + pub last_mod_date: u16, + pub crc32: u32, + pub compressed_size: u32, + pub uncompressed_size: u32, + pub file_name_length: u16, + pub extra_field_length: u16, +} + +unsafe impl Pod for ZipLocalEntryBlock {} + +impl FixedSizeBlock for ZipLocalEntryBlock { + type Magic = Magic; + const MAGIC: Magic = Magic::LOCAL_FILE_HEADER_SIGNATURE; + + #[inline(always)] + fn magic(self) -> Magic { + self.magic + } + + const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header"); + + to_and_from_le![ + (magic, Magic), + (version_made_by, u16), + (flags, u16), + (compression_method, u16), + (last_mod_time, u16), + (last_mod_date, u16), + (crc32, u32), + (compressed_size, u32), + (uncompressed_size, u32), + (file_name_length, u16), + (extra_field_length, u16), + ]; +} + +#[derive(Copy, Clone, Debug)] +#[repr(packed, C)] +pub(crate) struct ZipDataDescriptorBlock { + magic: spec::Magic, + pub crc32: u32, + pub compressed_size: u32, + pub uncompressed_size: u32, +} + +unsafe impl Pod for ZipDataDescriptorBlock {} + +impl FixedSizeBlock for ZipDataDescriptorBlock { + type Magic = Magic; + const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE; + + #[inline(always)] + fn magic(self) -> Magic { + self.magic + } + + const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid data descriptor header"); + + to_and_from_le![ + (magic, Magic), + (crc32, u32), + (compressed_size, u32), + (uncompressed_size, u32), + ]; +} + +#[derive(Copy, Clone, Debug)] +#[repr(packed, C)] +pub(crate) struct Zip64DataDescriptorBlock { + magic: spec::Magic, + pub crc32: u32, + pub compressed_size: u64, + pub uncompressed_size: u64, +} + +unsafe impl Pod for Zip64DataDescriptorBlock {} + +impl FixedSizeBlock for Zip64DataDescriptorBlock { + type Magic = Magic; + const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE; + + #[inline] + fn magic(self) -> spec::Magic { + self.magic + } + + const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header"); + + to_and_from_le![ + (magic, spec::Magic), + (crc32, u32), + (compressed_size, u64), + (uncompressed_size, u64), + ]; +} + #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct Zip32CDEBlock { From 171bac0933dd4694439f8c433db5ea44499db77f Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 10:41:38 -0600 Subject: [PATCH 12/52] Update types.rs --- src/types.rs | 393 --------------------------------------------------- 1 file changed, 393 deletions(-) diff --git a/src/types.rs b/src/types.rs index 4be6a1967..56a3f5464 100644 --- a/src/types.rs +++ b/src/types.rs @@ -156,399 +156,6 @@ pub type SimpleFileOptions = FileOptions<'static, ()>; impl FileOptions<'static, ()> { const DEFAULT_FILE_PERMISSION: u32 = 0o100_644; } -/// Representation of a moment in time. -/// -/// Zip files use an old format from DOS to store timestamps, -/// with its own set of peculiarities. -/// For example, it has a resolution of 2 seconds! -/// -/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`], -/// or read from one with [`ZipFile::last_modified`](crate::read::ZipFile::last_modified). -/// -/// # Warning -/// -/// Because there is no timezone associated with the [`DateTime`], they should ideally only -/// be used for user-facing descriptions. -/// -/// Modern zip files store more precise timestamps; see [`crate::extra_fields::ExtendedTimestamp`] -/// for details. -#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct DateTime { - datepart: u16, - timepart: u16, -} - -impl Debug for DateTime { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if *self == Self::default() { - return f.write_str("DateTime::default()"); - } - f.write_fmt(format_args!( - "DateTime::from_date_and_time({}, {}, {}, {}, {}, {})?", - self.year(), - self.month(), - self.day(), - self.hour(), - self.minute(), - self.second() - )) - } -} - -impl DateTime { - /// Constructs a default datetime of 1980-01-01 00:00:00. - pub const DEFAULT: Self = DateTime { - datepart: 0b0000_0000_0010_0001, - timepart: 0, - }; - - /// Returns the current time if possible, otherwise the default of 1980-01-01. - #[cfg(feature = "time")] - #[must_use] - pub fn default_for_write() -> Self { - let now = time::OffsetDateTime::now_utc(); - time::PrimitiveDateTime::new(now.date(), now.time()) - .try_into() - .unwrap_or_else(|_| DateTime::default()) - } - - /// Returns the current time if possible, otherwise the default of 1980-01-01. - #[cfg(not(feature = "time"))] - #[must_use] - pub fn default_for_write() -> Self { - DateTime::default() - } -} - -#[cfg(feature = "_arbitrary")] -impl arbitrary::Arbitrary<'_> for DateTime { - fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { - // DOS time format stores seconds divided by 2 in a 5-bit field (0..=29), - // so the maximum representable second value is 58. - const MAX_DOS_SECONDS: u16 = 58; - - let year: u16 = u.int_in_range(1980..=2107)?; - let month: u16 = u.int_in_range(1..=12)?; - let day: u16 = u.int_in_range(1..=31)?; - let datepart = day | (month << 5) | ((year - 1980) << 9); - let hour: u16 = u.int_in_range(0..=23)?; - let minute: u16 = u.int_in_range(0..=59)?; - let second: u16 = u.int_in_range(0..=MAX_DOS_SECONDS)?; - let timepart = (second >> 1) | (minute << 5) | (hour << 11); - Ok(DateTime { datepart, timepart }) - } -} - -#[cfg(feature = "chrono")] -impl TryFrom for DateTime { - type Error = DateTimeRangeError; - - fn try_from(value: chrono::NaiveDateTime) -> Result { - use chrono::{Datelike, Timelike}; - - DateTime::from_date_and_time( - value.year().try_into()?, - value.month().try_into()?, - value.day().try_into()?, - value.hour().try_into()?, - value.minute().try_into()?, - value.second().try_into()?, - ) - } -} - -#[cfg(feature = "chrono")] -impl TryFrom for chrono::NaiveDateTime { - type Error = DateTimeRangeError; - - fn try_from(value: DateTime) -> Result { - let date = chrono::NaiveDate::from_ymd_opt( - value.year().into(), - value.month().into(), - value.day().into(), - ) - .ok_or(DateTimeRangeError)?; - let time = chrono::NaiveTime::from_hms_opt( - value.hour().into(), - value.minute().into(), - value.second().into(), - ) - .ok_or(DateTimeRangeError)?; - Ok(chrono::NaiveDateTime::new(date, time)) - } -} - -#[cfg(feature = "jiff-02")] -impl TryFrom for DateTime { - type Error = DateTimeRangeError; - - fn try_from(value: jiff::civil::DateTime) -> Result { - Self::from_date_and_time( - value.year().try_into()?, - value.month() as u8, - value.day() as u8, - value.hour() as u8, - value.minute() as u8, - value.second() as u8, - ) - } -} - -#[cfg(feature = "jiff-02")] -impl TryFrom for jiff::civil::DateTime { - type Error = jiff::Error; - - fn try_from(value: DateTime) -> Result { - Self::new( - value.year() as i16, - value.month() as i8, - value.day() as i8, - value.hour() as i8, - value.minute() as i8, - value.second() as i8, - 0, - ) - } -} - -impl TryFrom<(u16, u16)> for DateTime { - type Error = DateTimeRangeError; - - #[inline] - fn try_from(values: (u16, u16)) -> Result { - Self::try_from_msdos(values.0, values.1) - } -} - -impl From for (u16, u16) { - #[inline] - fn from(dt: DateTime) -> Self { - (dt.datepart(), dt.timepart()) - } -} - -impl Default for DateTime { - /// Constructs an 'default' datetime of 1980-01-01 00:00:00 - fn default() -> DateTime { - DateTime::DEFAULT - } -} - -impl fmt::Display for DateTime { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", - self.year(), - self.month(), - self.day(), - self.hour(), - self.minute(), - self.second() - ) - } -} - -impl DateTime { - /// Converts an msdos (u16, u16) pair to a `DateTime` object - /// - /// # Safety - /// The caller must ensure the date and time are valid. - #[must_use] - pub const unsafe fn from_msdos_unchecked(datepart: u16, timepart: u16) -> DateTime { - DateTime { datepart, timepart } - } - - pub(crate) fn is_leap_year(year: u16) -> bool { - year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) - } - - /// Converts an msdos (u16, u16) pair to a `DateTime` object if it represents a valid date and - /// time. - pub fn try_from_msdos(datepart: u16, timepart: u16) -> Result { - let seconds = (timepart & 0b0000_0000_0001_1111) << 1; - let minutes = (timepart & 0b0000_0111_1110_0000) >> 5; - let hours = (timepart & 0b1111_1000_0000_0000) >> 11; - let days = datepart & 0b0000_0000_0001_1111; - let months = (datepart & 0b0000_0001_1110_0000) >> 5; - let years = (datepart & 0b1111_1110_0000_0000) >> 9; - Self::from_date_and_time( - years.checked_add(1980).ok_or(DateTimeRangeError)?, - months.try_into()?, - days.try_into()?, - hours.try_into()?, - minutes.try_into()?, - seconds.try_into()?, - ) - } - - /// Constructs a `DateTime` from a specific date and time - /// - /// The bounds are: - /// * year: [1980, 2107] - /// * month: [1, 12] - /// * day: [1, 28..=31] - /// * hour: [0, 23] - /// * minute: [0, 59] - /// * second: [0, 60] (rounded down to even and to [0, 58] due to ZIP format limitation) - pub fn from_date_and_time( - year: u16, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> Result { - if (1980..=2107).contains(&year) - && (1..=12).contains(&month) - && (1..=31).contains(&day) - && hour <= 23 - && minute <= 59 - && second <= 60 - { - // DOS/ZIP timestamp stores seconds/2 in 5 bits and cannot represent 59 or 60 seconds (incl. leap seconds) - let second = second.min(58); - let max_day = match month { - 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, - 4 | 6 | 9 | 11 => 30, - 2 if Self::is_leap_year(year) => 29, - 2 => 28, - _ => unreachable!(), - }; - if day > max_day { - return Err(DateTimeRangeError); - } - let datepart = u16::from(day) | (u16::from(month) << 5) | ((year - 1980) << 9); - let timepart = - (u16::from(second) >> 1) | (u16::from(minute) << 5) | (u16::from(hour) << 11); - Ok(DateTime { datepart, timepart }) - } else { - Err(DateTimeRangeError) - } - } - - /// Indicates whether this date and time can be written to a zip archive. - #[must_use] - pub fn is_valid(&self) -> bool { - Self::try_from_msdos(self.datepart, self.timepart).is_ok() - } - - /// Gets the time portion of this datetime in the msdos representation - #[must_use] - pub const fn timepart(&self) -> u16 { - self.timepart - } - - /// Gets the date portion of this datetime in the msdos representation - #[must_use] - pub const fn datepart(&self) -> u16 { - self.datepart - } - - /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018. - #[must_use] - pub const fn year(&self) -> u16 { - (self.datepart >> 9) + 1980 - } - - /// Get the month, where 1 = january and 12 = december - /// - /// # Warning - /// - /// When read from a zip file, this may not be a reasonable value - #[must_use] - pub const fn month(&self) -> u8 { - ((self.datepart & 0b0000_0001_1110_0000) >> 5) as u8 - } - - /// Get the day - /// - /// # Warning - /// - /// When read from a zip file, this may not be a reasonable value - #[must_use] - pub const fn day(&self) -> u8 { - (self.datepart & 0b0000_0000_0001_1111) as u8 - } - - /// Get the hour - /// - /// # Warning - /// - /// When read from a zip file, this may not be a reasonable value - #[must_use] - pub const fn hour(&self) -> u8 { - (self.timepart >> 11) as u8 - } - - /// Get the minute - /// - /// # Warning - /// - /// When read from a zip file, this may not be a reasonable value - #[must_use] - pub const fn minute(&self) -> u8 { - ((self.timepart & 0b0000_0111_1110_0000) >> 5) as u8 - } - - /// Get the second - /// - /// # Warning - /// - /// When read from a zip file, this may not be a reasonable value - #[must_use] - pub const fn second(&self) -> u8 { - ((self.timepart & 0b0000_0000_0001_1111) << 1) as u8 - } -} - -#[cfg(all(feature = "time", feature = "deprecated-time"))] -impl TryFrom for DateTime { - type Error = DateTimeRangeError; - - fn try_from(dt: time::OffsetDateTime) -> Result { - Self::try_from(time::PrimitiveDateTime::new(dt.date(), dt.time())) - } -} - -#[cfg(feature = "time")] -impl TryFrom for DateTime { - type Error = DateTimeRangeError; - - fn try_from(dt: time::PrimitiveDateTime) -> Result { - Self::from_date_and_time( - dt.year().try_into()?, - dt.month().into(), - dt.day(), - dt.hour(), - dt.minute(), - dt.second(), - ) - } -} - -#[cfg(all(feature = "time", feature = "deprecated-time"))] -impl TryFrom for time::OffsetDateTime { - type Error = time::error::ComponentRange; - - fn try_from(dt: DateTime) -> Result { - time::PrimitiveDateTime::try_from(dt).map(time::PrimitiveDateTime::assume_utc) - } -} - -#[cfg(feature = "time")] -impl TryFrom for time::PrimitiveDateTime { - type Error = time::error::ComponentRange; - - fn try_from(dt: DateTime) -> Result { - use time::{Date, Month, Time}; - let date = - Date::from_calendar_date(i32::from(dt.year()), Month::try_from(dt.month())?, dt.day())?; - let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?; - Ok(time::PrimitiveDateTime::new(date, time)) - } -} pub const MIN_VERSION: u8 = 10; pub const DEFAULT_VERSION: u8 = 45; From 5533849bdd442baf462ffe72970a97f6989c1dbb Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 10:42:13 -0600 Subject: [PATCH 13/52] Create datetime.rs --- src/datetime.rs | 395 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 src/datetime.rs diff --git a/src/datetime.rs b/src/datetime.rs new file mode 100644 index 000000000..3ae7c18c2 --- /dev/null +++ b/src/datetime.rs @@ -0,0 +1,395 @@ +//! Code related to Date time in zip files + +/// Representation of a moment in time. +/// +/// Zip files use an old format from DOS to store timestamps, +/// with its own set of peculiarities. +/// For example, it has a resolution of 2 seconds! +/// +/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`], +/// or read from one with [`ZipFile::last_modified`](crate::read::ZipFile::last_modified). +/// +/// # Warning +/// +/// Because there is no timezone associated with the [`DateTime`], they should ideally only +/// be used for user-facing descriptions. +/// +/// Modern zip files store more precise timestamps; see [`crate::extra_fields::ExtendedTimestamp`] +/// for details. +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DateTime { + datepart: u16, + timepart: u16, +} + +impl Debug for DateTime { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if *self == Self::default() { + return f.write_str("DateTime::default()"); + } + f.write_fmt(format_args!( + "DateTime::from_date_and_time({}, {}, {}, {}, {}, {})?", + self.year(), + self.month(), + self.day(), + self.hour(), + self.minute(), + self.second() + )) + } +} + +impl DateTime { + /// Constructs a default datetime of 1980-01-01 00:00:00. + pub const DEFAULT: Self = DateTime { + datepart: 0b0000_0000_0010_0001, + timepart: 0, + }; + + /// Returns the current time if possible, otherwise the default of 1980-01-01. + #[cfg(feature = "time")] + #[must_use] + pub fn default_for_write() -> Self { + let now = time::OffsetDateTime::now_utc(); + time::PrimitiveDateTime::new(now.date(), now.time()) + .try_into() + .unwrap_or_else(|_| DateTime::default()) + } + + /// Returns the current time if possible, otherwise the default of 1980-01-01. + #[cfg(not(feature = "time"))] + #[must_use] + pub fn default_for_write() -> Self { + DateTime::default() + } +} + +#[cfg(feature = "_arbitrary")] +impl arbitrary::Arbitrary<'_> for DateTime { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + // DOS time format stores seconds divided by 2 in a 5-bit field (0..=29), + // so the maximum representable second value is 58. + const MAX_DOS_SECONDS: u16 = 58; + + let year: u16 = u.int_in_range(1980..=2107)?; + let month: u16 = u.int_in_range(1..=12)?; + let day: u16 = u.int_in_range(1..=31)?; + let datepart = day | (month << 5) | ((year - 1980) << 9); + let hour: u16 = u.int_in_range(0..=23)?; + let minute: u16 = u.int_in_range(0..=59)?; + let second: u16 = u.int_in_range(0..=MAX_DOS_SECONDS)?; + let timepart = (second >> 1) | (minute << 5) | (hour << 11); + Ok(DateTime { datepart, timepart }) + } +} + +#[cfg(feature = "chrono")] +impl TryFrom for DateTime { + type Error = DateTimeRangeError; + + fn try_from(value: chrono::NaiveDateTime) -> Result { + use chrono::{Datelike, Timelike}; + + DateTime::from_date_and_time( + value.year().try_into()?, + value.month().try_into()?, + value.day().try_into()?, + value.hour().try_into()?, + value.minute().try_into()?, + value.second().try_into()?, + ) + } +} + +#[cfg(feature = "chrono")] +impl TryFrom for chrono::NaiveDateTime { + type Error = DateTimeRangeError; + + fn try_from(value: DateTime) -> Result { + let date = chrono::NaiveDate::from_ymd_opt( + value.year().into(), + value.month().into(), + value.day().into(), + ) + .ok_or(DateTimeRangeError)?; + let time = chrono::NaiveTime::from_hms_opt( + value.hour().into(), + value.minute().into(), + value.second().into(), + ) + .ok_or(DateTimeRangeError)?; + Ok(chrono::NaiveDateTime::new(date, time)) + } +} + +#[cfg(feature = "jiff-02")] +impl TryFrom for DateTime { + type Error = DateTimeRangeError; + + fn try_from(value: jiff::civil::DateTime) -> Result { + Self::from_date_and_time( + value.year().try_into()?, + value.month() as u8, + value.day() as u8, + value.hour() as u8, + value.minute() as u8, + value.second() as u8, + ) + } +} + +#[cfg(feature = "jiff-02")] +impl TryFrom for jiff::civil::DateTime { + type Error = jiff::Error; + + fn try_from(value: DateTime) -> Result { + Self::new( + value.year() as i16, + value.month() as i8, + value.day() as i8, + value.hour() as i8, + value.minute() as i8, + value.second() as i8, + 0, + ) + } +} + +impl TryFrom<(u16, u16)> for DateTime { + type Error = DateTimeRangeError; + + #[inline] + fn try_from(values: (u16, u16)) -> Result { + Self::try_from_msdos(values.0, values.1) + } +} + +impl From for (u16, u16) { + #[inline] + fn from(dt: DateTime) -> Self { + (dt.datepart(), dt.timepart()) + } +} + +impl Default for DateTime { + /// Constructs an 'default' datetime of 1980-01-01 00:00:00 + fn default() -> DateTime { + DateTime::DEFAULT + } +} + +impl fmt::Display for DateTime { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + self.year(), + self.month(), + self.day(), + self.hour(), + self.minute(), + self.second() + ) + } +} + +impl DateTime { + /// Converts an msdos (u16, u16) pair to a `DateTime` object + /// + /// # Safety + /// The caller must ensure the date and time are valid. + #[must_use] + pub const unsafe fn from_msdos_unchecked(datepart: u16, timepart: u16) -> DateTime { + DateTime { datepart, timepart } + } + + pub(crate) fn is_leap_year(year: u16) -> bool { + year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) + } + + /// Converts an msdos (u16, u16) pair to a `DateTime` object if it represents a valid date and + /// time. + pub fn try_from_msdos(datepart: u16, timepart: u16) -> Result { + let seconds = (timepart & 0b0000_0000_0001_1111) << 1; + let minutes = (timepart & 0b0000_0111_1110_0000) >> 5; + let hours = (timepart & 0b1111_1000_0000_0000) >> 11; + let days = datepart & 0b0000_0000_0001_1111; + let months = (datepart & 0b0000_0001_1110_0000) >> 5; + let years = (datepart & 0b1111_1110_0000_0000) >> 9; + Self::from_date_and_time( + years.checked_add(1980).ok_or(DateTimeRangeError)?, + months.try_into()?, + days.try_into()?, + hours.try_into()?, + minutes.try_into()?, + seconds.try_into()?, + ) + } + + /// Constructs a `DateTime` from a specific date and time + /// + /// The bounds are: + /// * year: [1980, 2107] + /// * month: [1, 12] + /// * day: [1, 28..=31] + /// * hour: [0, 23] + /// * minute: [0, 59] + /// * second: [0, 60] (rounded down to even and to [0, 58] due to ZIP format limitation) + pub fn from_date_and_time( + year: u16, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> Result { + if (1980..=2107).contains(&year) + && (1..=12).contains(&month) + && (1..=31).contains(&day) + && hour <= 23 + && minute <= 59 + && second <= 60 + { + // DOS/ZIP timestamp stores seconds/2 in 5 bits and cannot represent 59 or 60 seconds (incl. leap seconds) + let second = second.min(58); + let max_day = match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if Self::is_leap_year(year) => 29, + 2 => 28, + _ => unreachable!(), + }; + if day > max_day { + return Err(DateTimeRangeError); + } + let datepart = u16::from(day) | (u16::from(month) << 5) | ((year - 1980) << 9); + let timepart = + (u16::from(second) >> 1) | (u16::from(minute) << 5) | (u16::from(hour) << 11); + Ok(DateTime { datepart, timepart }) + } else { + Err(DateTimeRangeError) + } + } + + /// Indicates whether this date and time can be written to a zip archive. + #[must_use] + pub fn is_valid(&self) -> bool { + Self::try_from_msdos(self.datepart, self.timepart).is_ok() + } + + /// Gets the time portion of this datetime in the msdos representation + #[must_use] + pub const fn timepart(&self) -> u16 { + self.timepart + } + + /// Gets the date portion of this datetime in the msdos representation + #[must_use] + pub const fn datepart(&self) -> u16 { + self.datepart + } + + /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018. + #[must_use] + pub const fn year(&self) -> u16 { + (self.datepart >> 9) + 1980 + } + + /// Get the month, where 1 = january and 12 = december + /// + /// # Warning + /// + /// When read from a zip file, this may not be a reasonable value + #[must_use] + pub const fn month(&self) -> u8 { + ((self.datepart & 0b0000_0001_1110_0000) >> 5) as u8 + } + + /// Get the day + /// + /// # Warning + /// + /// When read from a zip file, this may not be a reasonable value + #[must_use] + pub const fn day(&self) -> u8 { + (self.datepart & 0b0000_0000_0001_1111) as u8 + } + + /// Get the hour + /// + /// # Warning + /// + /// When read from a zip file, this may not be a reasonable value + #[must_use] + pub const fn hour(&self) -> u8 { + (self.timepart >> 11) as u8 + } + + /// Get the minute + /// + /// # Warning + /// + /// When read from a zip file, this may not be a reasonable value + #[must_use] + pub const fn minute(&self) -> u8 { + ((self.timepart & 0b0000_0111_1110_0000) >> 5) as u8 + } + + /// Get the second + /// + /// # Warning + /// + /// When read from a zip file, this may not be a reasonable value + #[must_use] + pub const fn second(&self) -> u8 { + ((self.timepart & 0b0000_0000_0001_1111) << 1) as u8 + } +} + +#[cfg(all(feature = "time", feature = "deprecated-time"))] +impl TryFrom for DateTime { + type Error = DateTimeRangeError; + + fn try_from(dt: time::OffsetDateTime) -> Result { + Self::try_from(time::PrimitiveDateTime::new(dt.date(), dt.time())) + } +} + +#[cfg(feature = "time")] +impl TryFrom for DateTime { + type Error = DateTimeRangeError; + + fn try_from(dt: time::PrimitiveDateTime) -> Result { + Self::from_date_and_time( + dt.year().try_into()?, + dt.month().into(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second(), + ) + } +} + +#[cfg(all(feature = "time", feature = "deprecated-time"))] +impl TryFrom for time::OffsetDateTime { + type Error = time::error::ComponentRange; + + fn try_from(dt: DateTime) -> Result { + time::PrimitiveDateTime::try_from(dt).map(time::PrimitiveDateTime::assume_utc) + } +} + +#[cfg(feature = "time")] +impl TryFrom for time::PrimitiveDateTime { + type Error = time::error::ComponentRange; + + fn try_from(dt: DateTime) -> Result { + use time::{Date, Month, Time}; + let date = + Date::from_calendar_date(i32::from(dt.year()), Month::try_from(dt.month())?, dt.day())?; + let time = Time::from_hms(dt.hour(), dt.minute(), dt.second())?; + Ok(time::PrimitiveDateTime::new(date, time)) + } +} From 87c775a9739760c9127d8c628f68720a3c328c0e Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 10:44:39 -0600 Subject: [PATCH 14/52] Update types.rs --- src/types.rs | 391 +-------------------------------------------------- 1 file changed, 1 insertion(+), 390 deletions(-) diff --git a/src/types.rs b/src/types.rs index 56a3f5464..0d1c4d478 100644 --- a/src/types.rs +++ b/src/types.rs @@ -862,393 +862,4 @@ mod test { assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd")); } - #[test] - #[allow(clippy::unusual_byte_groupings)] - fn datetime_default() { - use super::DateTime; - let dt = DateTime::default(); - assert_eq!(dt.timepart(), 0); - assert_eq!(dt.datepart(), 0b0000000_0001_00001); - } - - #[test] - #[allow(clippy::unusual_byte_groupings)] - fn datetime_max() { - use super::DateTime; - let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap(); - assert_eq!(dt.timepart(), 0b10111_111011_11101); - assert_eq!(dt.datepart(), 0b1111111_1100_11111); - } - - #[test] - fn datetime_equality() { - use super::DateTime; - - let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap(); - assert_eq!( - dt, - DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap() - ); - assert_ne!(dt, DateTime::default()); - } - - #[test] - fn datetime_order() { - use std::cmp::Ordering; - - use super::DateTime; - - let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap(); - assert_eq!( - dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()), - Ordering::Equal - ); - // year - assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap()); - assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap()); - // month - assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap()); - assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap()); - // day - assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap()); - assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap()); - // hour - assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap()); - assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap()); - // minute - assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap()); - assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap()); - // second - assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap()); - assert_eq!( - dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()), - Ordering::Equal - ); - assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap()); - assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap()); - } - - #[test] - fn datetime_display() { - use super::DateTime; - - assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00"); - assert_eq!( - format!( - "{}", - DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap() - ), - "2018-11-17 10:38:30" - ); - assert_eq!( - format!( - "{}", - DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap() - ), - "2107-12-31 23:59:58" - ); - } - - #[test] - fn datetime_bounds() { - use super::DateTime; - - assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok()); - assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err()); - assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err()); - - assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err()); - - assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok()); - - // leap year: divisible by 4 - assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok()); - // leap year: divisible by 100 and by 400 - assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok()); - // common year: divisible by 100 but not by 400 - assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err()); - } - - use crate::types::{System, ZipFileData}; - use std::{path::PathBuf, sync::OnceLock}; - - #[cfg(all(feature = "time", feature = "deprecated-time"))] - #[test] - fn datetime_try_from_offset_datetime() { - use time::macros::datetime; - - use super::DateTime; - - // 2018-11-17 10:38:30 - let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30 UTC)).unwrap(); - assert_eq!(dt.year(), 2018); - assert_eq!(dt.month(), 11); - assert_eq!(dt.day(), 17); - assert_eq!(dt.hour(), 10); - assert_eq!(dt.minute(), 38); - assert_eq!(dt.second(), 30); - } - - #[cfg(feature = "time")] - #[test] - fn datetime_try_from_primitive_datetime() { - use time::macros::datetime; - - use super::DateTime; - - // 2018-11-17 10:38:30 - let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap(); - assert_eq!(dt.year(), 2018); - assert_eq!(dt.month(), 11); - assert_eq!(dt.day(), 17); - assert_eq!(dt.hour(), 10); - assert_eq!(dt.minute(), 38); - assert_eq!(dt.second(), 30); - } - - #[cfg(feature = "time")] - #[test] - fn datetime_try_from_bounds() { - use super::DateTime; - use time::macros::datetime; - - // 1979-12-31 23:59:59 - assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err()); - - // 1980-01-01 00:00:00 - assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok()); - - // 2107-12-31 23:59:59 - assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok()); - - // 2108-01-01 00:00:00 - assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err()); - } - - #[cfg(all(feature = "time", feature = "deprecated-time"))] - #[test] - fn offset_datetime_try_from_datetime() { - use time::OffsetDateTime; - use time::macros::datetime; - - use super::DateTime; - - // 2018-11-17 10:38:30 UTC - let dt = - OffsetDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap(); - assert_eq!(dt, datetime!(2018-11-17 10:38:30 UTC)); - } - - #[cfg(feature = "time")] - #[test] - fn primitive_datetime_try_from_datetime() { - use time::PrimitiveDateTime; - use time::macros::datetime; - - use super::DateTime; - - // 2018-11-17 10:38:30 - let dt = - PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap(); - assert_eq!(dt, datetime!(2018-11-17 10:38:30)); - } - - #[cfg(all(feature = "time", feature = "deprecated-time"))] - #[test] - fn offset_datetime_try_from_bounds() { - use super::DateTime; - use time::OffsetDateTime; - - // 1980-00-00 00:00:00 - assert!( - OffsetDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }) - .is_err() - ); - - // 2107-15-31 31:63:62 - assert!( - OffsetDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }) - .is_err() - ); - } - - #[cfg(feature = "time")] - #[test] - fn primitive_datetime_try_from_bounds() { - use super::DateTime; - use time::PrimitiveDateTime; - - // 1980-00-00 00:00:00 - assert!( - PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }) - .is_err() - ); - - // 2107-15-31 31:63:62 - assert!( - PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }) - .is_err() - ); - } - - #[cfg(feature = "jiff-02")] - #[test] - fn datetime_try_from_civil_datetime() { - use jiff::civil; - - use super::DateTime; - - // 2018-11-17 10:38:30 - let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).unwrap(); - assert_eq!(dt.year(), 2018); - assert_eq!(dt.month(), 11); - assert_eq!(dt.day(), 17); - assert_eq!(dt.hour(), 10); - assert_eq!(dt.minute(), 38); - assert_eq!(dt.second(), 30); - } - - #[cfg(feature = "jiff-02")] - #[test] - fn datetime_try_from_civil_datetime_bounds() { - use jiff::civil; - - use super::DateTime; - - // 1979-12-31 23:59:59 - assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err()); - - // 1980-01-01 00:00:00 - assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok()); - - // 2107-12-31 23:59:59 - assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok()); - - // 2108-01-01 00:00:00 - assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err()); - } - - #[cfg(feature = "jiff-02")] - #[test] - fn civil_datetime_try_from_datetime() { - use jiff::civil; - - use super::DateTime; - - // 2018-11-17 10:38:30 UTC - let dt = - civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap(); - assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0)); - } - - #[cfg(feature = "jiff-02")] - #[test] - fn civil_datetime_try_from_datetime_bounds() { - use jiff::civil; - - use super::DateTime; - - // 1980-00-00 00:00:00 - assert!( - civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }) - .is_err() - ); - - // 2107-15-31 31:63:62 - assert!( - civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }) - .is_err() - ); - } - - #[test] - fn time_conversion() { - use super::DateTime; - let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap(); - assert_eq!(dt.year(), 2018); - assert_eq!(dt.month(), 11); - assert_eq!(dt.day(), 17); - assert_eq!(dt.hour(), 10); - assert_eq!(dt.minute(), 38); - assert_eq!(dt.second(), 30); - - let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap(); - assert_eq!(dt.year(), 2018); - assert_eq!(dt.month(), 11); - assert_eq!(dt.day(), 17); - assert_eq!(dt.hour(), 10); - assert_eq!(dt.minute(), 38); - assert_eq!(dt.second(), 30); - - assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF)); - } - - #[test] - fn time_out_of_bounds() { - use super::DateTime; - let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }; - assert_eq!(dt.year(), 2107); - assert_eq!(dt.month(), 15); - assert_eq!(dt.day(), 31); - assert_eq!(dt.hour(), 31); - assert_eq!(dt.minute(), 63); - assert_eq!(dt.second(), 62); - - let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }; - assert_eq!(dt.year(), 1980); - assert_eq!(dt.month(), 0); - assert_eq!(dt.day(), 0); - assert_eq!(dt.hour(), 0); - assert_eq!(dt.minute(), 0); - assert_eq!(dt.second(), 0); - } - - #[cfg(feature = "time")] - #[test] - fn time_at_january() { - use super::DateTime; - use time::{OffsetDateTime, PrimitiveDateTime}; - - // 2020-01-01 00:00:00 - let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap(); - - assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok()); - } - - #[test] - fn test_is_leap_year() { - use crate::DateTime; - assert!(DateTime::is_leap_year(2000)); - assert!(!DateTime::is_leap_year(2026)); - assert!(!DateTime::is_leap_year(2027)); - assert!(DateTime::is_leap_year(2028)); - assert!(DateTime::is_leap_year(1600)); - assert!(DateTime::is_leap_year(2400)); - assert!(!DateTime::is_leap_year(1900)); - assert!(!DateTime::is_leap_year(2100)); - } -} +} \ No newline at end of file From 99cc870ab261ed21b464b9e7e69e8ab740538067 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 10:45:35 -0600 Subject: [PATCH 15/52] Update datetime.rs --- src/datetime.rs | 394 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) diff --git a/src/datetime.rs b/src/datetime.rs index 3ae7c18c2..8ac7ba583 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -393,3 +393,397 @@ impl TryFrom for time::PrimitiveDateTime { Ok(time::PrimitiveDateTime::new(date, time)) } } + + +#[cfg(tests)] +mod tests { +#[test] + #[allow(clippy::unusual_byte_groupings)] + fn datetime_default() { + use super::DateTime; + let dt = DateTime::default(); + assert_eq!(dt.timepart(), 0); + assert_eq!(dt.datepart(), 0b0000000_0001_00001); + } + + #[test] + #[allow(clippy::unusual_byte_groupings)] + fn datetime_max() { + use super::DateTime; + let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap(); + assert_eq!(dt.timepart(), 0b10111_111011_11101); + assert_eq!(dt.datepart(), 0b1111111_1100_11111); + } + + #[test] + fn datetime_equality() { + use super::DateTime; + + let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap(); + assert_eq!( + dt, + DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap() + ); + assert_ne!(dt, DateTime::default()); + } + + #[test] + fn datetime_order() { + use std::cmp::Ordering; + + use super::DateTime; + + let dt = DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap(); + assert_eq!( + dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap()), + Ordering::Equal + ); + // year + assert!(dt < DateTime::from_date_and_time(2019, 11, 17, 10, 38, 30).unwrap()); + assert!(dt > DateTime::from_date_and_time(2017, 11, 17, 10, 38, 30).unwrap()); + // month + assert!(dt < DateTime::from_date_and_time(2018, 12, 17, 10, 38, 30).unwrap()); + assert!(dt > DateTime::from_date_and_time(2018, 10, 17, 10, 38, 30).unwrap()); + // day + assert!(dt < DateTime::from_date_and_time(2018, 11, 18, 10, 38, 30).unwrap()); + assert!(dt > DateTime::from_date_and_time(2018, 11, 16, 10, 38, 30).unwrap()); + // hour + assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 11, 38, 30).unwrap()); + assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 9, 38, 30).unwrap()); + // minute + assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 39, 30).unwrap()); + assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 37, 30).unwrap()); + // second + assert!(dt < DateTime::from_date_and_time(2018, 11, 17, 10, 38, 32).unwrap()); + assert_eq!( + dt.cmp(&DateTime::from_date_and_time(2018, 11, 17, 10, 38, 31).unwrap()), + Ordering::Equal + ); + assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 29).unwrap()); + assert!(dt > DateTime::from_date_and_time(2018, 11, 17, 10, 38, 28).unwrap()); + } + + #[test] + fn datetime_display() { + use super::DateTime; + + assert_eq!(format!("{}", DateTime::default()), "1980-01-01 00:00:00"); + assert_eq!( + format!( + "{}", + DateTime::from_date_and_time(2018, 11, 17, 10, 38, 30).unwrap() + ), + "2018-11-17 10:38:30" + ); + assert_eq!( + format!( + "{}", + DateTime::from_date_and_time(2107, 12, 31, 23, 59, 58).unwrap() + ), + "2107-12-31 23:59:58" + ); + } + + #[test] + fn datetime_bounds() { + use super::DateTime; + + assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok()); + assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err()); + assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err()); + + assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err()); + + assert!(DateTime::from_date_and_time(2018, 1, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 2, 28, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 2, 29, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2018, 3, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 4, 30, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 4, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2018, 5, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 6, 30, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 6, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2018, 7, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 8, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 9, 30, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 9, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2018, 10, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 11, 30, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(2018, 11, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2018, 12, 31, 0, 0, 0).is_ok()); + + // leap year: divisible by 4 + assert!(DateTime::from_date_and_time(2024, 2, 29, 0, 0, 0).is_ok()); + // leap year: divisible by 100 and by 400 + assert!(DateTime::from_date_and_time(2000, 2, 29, 0, 0, 0).is_ok()); + // common year: divisible by 100 but not by 400 + assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err()); + } + + use crate::types::{System, ZipFileData}; + use std::{path::PathBuf, sync::OnceLock}; + + #[cfg(all(feature = "time", feature = "deprecated-time"))] + #[test] + fn datetime_try_from_offset_datetime() { + use time::macros::datetime; + + use super::DateTime; + + // 2018-11-17 10:38:30 + let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30 UTC)).unwrap(); + assert_eq!(dt.year(), 2018); + assert_eq!(dt.month(), 11); + assert_eq!(dt.day(), 17); + assert_eq!(dt.hour(), 10); + assert_eq!(dt.minute(), 38); + assert_eq!(dt.second(), 30); + } + + #[cfg(feature = "time")] + #[test] + fn datetime_try_from_primitive_datetime() { + use time::macros::datetime; + + use super::DateTime; + + // 2018-11-17 10:38:30 + let dt = DateTime::try_from(datetime!(2018-11-17 10:38:30)).unwrap(); + assert_eq!(dt.year(), 2018); + assert_eq!(dt.month(), 11); + assert_eq!(dt.day(), 17); + assert_eq!(dt.hour(), 10); + assert_eq!(dt.minute(), 38); + assert_eq!(dt.second(), 30); + } + + #[cfg(feature = "time")] + #[test] + fn datetime_try_from_bounds() { + use super::DateTime; + use time::macros::datetime; + + // 1979-12-31 23:59:59 + assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59)).is_err()); + + // 1980-01-01 00:00:00 + assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00)).is_ok()); + + // 2107-12-31 23:59:59 + assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59)).is_ok()); + + // 2108-01-01 00:00:00 + assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00)).is_err()); + } + + #[cfg(all(feature = "time", feature = "deprecated-time"))] + #[test] + fn offset_datetime_try_from_datetime() { + use time::OffsetDateTime; + use time::macros::datetime; + + use super::DateTime; + + // 2018-11-17 10:38:30 UTC + let dt = + OffsetDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap(); + assert_eq!(dt, datetime!(2018-11-17 10:38:30 UTC)); + } + + #[cfg(feature = "time")] + #[test] + fn primitive_datetime_try_from_datetime() { + use time::PrimitiveDateTime; + use time::macros::datetime; + + use super::DateTime; + + // 2018-11-17 10:38:30 + let dt = + PrimitiveDateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap(); + assert_eq!(dt, datetime!(2018-11-17 10:38:30)); + } + + #[cfg(all(feature = "time", feature = "deprecated-time"))] + #[test] + fn offset_datetime_try_from_bounds() { + use super::DateTime; + use time::OffsetDateTime; + + // 1980-00-00 00:00:00 + assert!( + OffsetDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }) + .is_err() + ); + + // 2107-15-31 31:63:62 + assert!( + OffsetDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }) + .is_err() + ); + } + + #[cfg(feature = "time")] + #[test] + fn primitive_datetime_try_from_bounds() { + use super::DateTime; + use time::PrimitiveDateTime; + + // 1980-00-00 00:00:00 + assert!( + PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }) + .is_err() + ); + + // 2107-15-31 31:63:62 + assert!( + PrimitiveDateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }) + .is_err() + ); + } + + #[cfg(feature = "jiff-02")] + #[test] + fn datetime_try_from_civil_datetime() { + use jiff::civil; + + use super::DateTime; + + // 2018-11-17 10:38:30 + let dt = DateTime::try_from(civil::datetime(2018, 11, 17, 10, 38, 30, 0)).unwrap(); + assert_eq!(dt.year(), 2018); + assert_eq!(dt.month(), 11); + assert_eq!(dt.day(), 17); + assert_eq!(dt.hour(), 10); + assert_eq!(dt.minute(), 38); + assert_eq!(dt.second(), 30); + } + + #[cfg(feature = "jiff-02")] + #[test] + fn datetime_try_from_civil_datetime_bounds() { + use jiff::civil; + + use super::DateTime; + + // 1979-12-31 23:59:59 + assert!(DateTime::try_from(civil::datetime(1979, 12, 31, 23, 59, 59, 0)).is_err()); + + // 1980-01-01 00:00:00 + assert!(DateTime::try_from(civil::datetime(1980, 1, 1, 0, 0, 0, 0)).is_ok()); + + // 2107-12-31 23:59:59 + assert!(DateTime::try_from(civil::datetime(2107, 12, 31, 23, 59, 59, 0)).is_ok()); + + // 2108-01-01 00:00:00 + assert!(DateTime::try_from(civil::datetime(2108, 1, 1, 0, 0, 0, 0)).is_err()); + } + + #[cfg(feature = "jiff-02")] + #[test] + fn civil_datetime_try_from_datetime() { + use jiff::civil; + + use super::DateTime; + + // 2018-11-17 10:38:30 UTC + let dt = + civil::DateTime::try_from(DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap()).unwrap(); + assert_eq!(dt, civil::datetime(2018, 11, 17, 10, 38, 30, 0)); + } + + #[cfg(feature = "jiff-02")] + #[test] + fn civil_datetime_try_from_datetime_bounds() { + use jiff::civil; + + use super::DateTime; + + // 1980-00-00 00:00:00 + assert!( + civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }) + .is_err() + ); + + // 2107-15-31 31:63:62 + assert!( + civil::DateTime::try_from(unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }) + .is_err() + ); + } + + #[test] + fn time_conversion() { + use super::DateTime; + let dt = DateTime::try_from_msdos(0x4D71, 0x54CF).unwrap(); + assert_eq!(dt.year(), 2018); + assert_eq!(dt.month(), 11); + assert_eq!(dt.day(), 17); + assert_eq!(dt.hour(), 10); + assert_eq!(dt.minute(), 38); + assert_eq!(dt.second(), 30); + + let dt = DateTime::try_from((0x4D71, 0x54CF)).unwrap(); + assert_eq!(dt.year(), 2018); + assert_eq!(dt.month(), 11); + assert_eq!(dt.day(), 17); + assert_eq!(dt.hour(), 10); + assert_eq!(dt.minute(), 38); + assert_eq!(dt.second(), 30); + + assert_eq!(<(u16, u16)>::from(dt), (0x4D71, 0x54CF)); + } + + #[test] + fn time_out_of_bounds() { + use super::DateTime; + let dt = unsafe { DateTime::from_msdos_unchecked(0xFFFF, 0xFFFF) }; + assert_eq!(dt.year(), 2107); + assert_eq!(dt.month(), 15); + assert_eq!(dt.day(), 31); + assert_eq!(dt.hour(), 31); + assert_eq!(dt.minute(), 63); + assert_eq!(dt.second(), 62); + + let dt = unsafe { DateTime::from_msdos_unchecked(0x0000, 0x0000) }; + assert_eq!(dt.year(), 1980); + assert_eq!(dt.month(), 0); + assert_eq!(dt.day(), 0); + assert_eq!(dt.hour(), 0); + assert_eq!(dt.minute(), 0); + assert_eq!(dt.second(), 0); + } + + #[cfg(feature = "time")] + #[test] + fn time_at_january() { + use super::DateTime; + use time::{OffsetDateTime, PrimitiveDateTime}; + + // 2020-01-01 00:00:00 + let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap(); + + assert!(DateTime::try_from(PrimitiveDateTime::new(clock.date(), clock.time())).is_ok()); + } + + #[test] + fn test_is_leap_year() { + use crate::DateTime; + assert!(DateTime::is_leap_year(2000)); + assert!(!DateTime::is_leap_year(2026)); + assert!(!DateTime::is_leap_year(2027)); + assert!(DateTime::is_leap_year(2028)); + assert!(DateTime::is_leap_year(1600)); + assert!(DateTime::is_leap_year(2400)); + assert!(!DateTime::is_leap_year(1900)); + assert!(!DateTime::is_leap_year(2100)); + } +} From 093b1bc711e2cb59e755b55f03d8b199cfc63bd1 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:05:45 -0600 Subject: [PATCH 16/52] Update read.rs --- src/read.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/read.rs b/src/read.rs index 7b7262b40..f7ce81205 100644 --- a/src/read.rs +++ b/src/read.rs @@ -368,7 +368,9 @@ pub(crate) fn make_crypto_reader<'a, R: Read + ?Sized>( #[allow(deprecated)] { if let CompressionMethod::Unsupported(_) = data.compression_method { - return Err(ZipError::UnsupportedArchive("Compression method not supported")); + return Err(ZipError::UnsupportedArchive( + "Compression method not supported" + )); } } @@ -731,7 +733,9 @@ impl ZipArchive { }; if dir_info.disk_number != dir_info.disk_with_central_directory { - return Err(ZipError::UnsupportedArchive("Support for multi-disk files is not implemented")); + return Err(ZipError::UnsupportedArchive( + "Support for multi-disk files is not implemented" + )); } if file_capacity.saturating_mul(size_of::()) > isize::MAX as usize { From 5bed49079556e635749157cecbe2c85e1bc8e429 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:06:40 -0600 Subject: [PATCH 17/52] Update types.rs --- src/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 0d1c4d478..2a056d75d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -861,5 +861,4 @@ mod test { }; assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd")); } - } \ No newline at end of file From 620a5f2057af90301dc0af77409414c0cd75df57 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:08:09 -0600 Subject: [PATCH 18/52] Update spec.rs --- src/spec.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spec.rs b/src/spec.rs index ff5d4a48d..82f582c39 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -290,7 +290,7 @@ macro_rules! to_and_from_le { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipCentralEntryBlock { - magic: spec::Magic, + magic: Magic, pub version_made_by: u16, pub version_to_extract: u16, pub flags: u16, @@ -346,7 +346,7 @@ impl FixedSizeBlock for ZipCentralEntryBlock { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipLocalEntryBlock { - magic: spec::Magic, + magic: Magic, pub version_made_by: u16, pub flags: u16, pub compression_method: u16, @@ -390,7 +390,7 @@ impl FixedSizeBlock for ZipLocalEntryBlock { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipDataDescriptorBlock { - magic: spec::Magic, + magic: Magic, pub crc32: u32, pub compressed_size: u32, pub uncompressed_size: u32, @@ -420,7 +420,7 @@ impl FixedSizeBlock for ZipDataDescriptorBlock { #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct Zip64DataDescriptorBlock { - magic: spec::Magic, + magic: Magic, pub crc32: u32, pub compressed_size: u64, pub uncompressed_size: u64, @@ -430,7 +430,7 @@ unsafe impl Pod for Zip64DataDescriptorBlock {} impl FixedSizeBlock for Zip64DataDescriptorBlock { type Magic = Magic; - const MAGIC: spec::Magic = spec::Magic::DATA_DESCRIPTOR_SIGNATURE; + const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE; #[inline] fn magic(self) -> spec::Magic { From 7c2b93a04bae95a327bc025c70258ef56e04cf83 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:09:44 -0600 Subject: [PATCH 19/52] Update spec.rs --- src/spec.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spec.rs b/src/spec.rs index 82f582c39..6f42a28f0 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -433,14 +433,14 @@ impl FixedSizeBlock for Zip64DataDescriptorBlock { const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE; #[inline] - fn magic(self) -> spec::Magic { + fn magic(self) -> Magic { self.magic } const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header"); to_and_from_le![ - (magic, spec::Magic), + (magic, Magic), (crc32, u32), (compressed_size, u64), (uncompressed_size, u64), From e65718576e55d815947720c71acca581f5873e35 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:14:10 -0600 Subject: [PATCH 20/52] Update read.rs --- src/read.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/read.rs b/src/read.rs index f7ce81205..0aa3dbe80 100644 --- a/src/read.rs +++ b/src/read.rs @@ -369,7 +369,7 @@ pub(crate) fn make_crypto_reader<'a, R: Read + ?Sized>( { if let CompressionMethod::Unsupported(_) = data.compression_method { return Err(ZipError::UnsupportedArchive( - "Compression method not supported" + "Compression method not supported", )); } } @@ -734,7 +734,7 @@ impl ZipArchive { if dir_info.disk_number != dir_info.disk_with_central_directory { return Err(ZipError::UnsupportedArchive( - "Support for multi-disk files is not implemented" + "Support for multi-disk files is not implemented", )); } From d01b5294d1ccc75db1eac08ba726b5fad4fcff23 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:14:26 -0600 Subject: [PATCH 21/52] Update types.rs --- src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 2a056d75d..14c4ac268 100644 --- a/src/types.rs +++ b/src/types.rs @@ -861,4 +861,4 @@ mod test { }; assert_eq!(data.file_name_sanitized(), PathBuf::from("path/etc/passwd")); } -} \ No newline at end of file +} From 9181199530f88b3c0ea2807d01a0ede083f42e04 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:18:09 -0600 Subject: [PATCH 22/52] Update types.rs --- src/types.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types.rs b/src/types.rs index 14c4ac268..e0e747dda 100644 --- a/src/types.rs +++ b/src/types.rs @@ -830,6 +830,9 @@ mod test { #[test] fn sanitize() { + use super::{System, ZipFileData}; + use std::{path::PathBuf, sync::Once lock}; + let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string(); let data = ZipFileData { system: System::Dos, From 069e5e6871803d632a10982e63f8e232ae9d0386 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:19:20 -0600 Subject: [PATCH 23/52] Update datetime.rs --- src/datetime.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/datetime.rs b/src/datetime.rs index 8ac7ba583..cc35f65a3 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -397,7 +397,7 @@ impl TryFrom for time::PrimitiveDateTime { #[cfg(tests)] mod tests { -#[test] + #[test] #[allow(clippy::unusual_byte_groupings)] fn datetime_default() { use super::DateTime; @@ -528,9 +528,6 @@ mod tests { assert!(DateTime::from_date_and_time(2100, 2, 29, 0, 0, 0).is_err()); } - use crate::types::{System, ZipFileData}; - use std::{path::PathBuf, sync::OnceLock}; - #[cfg(all(feature = "time", feature = "deprecated-time"))] #[test] fn datetime_try_from_offset_datetime() { From 897d9ef24a6dd98494af25908da6f72fc6b1bf7c Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:22:11 -0600 Subject: [PATCH 24/52] Update lib.rs --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 1c5438abd..c2544c1ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,7 @@ mod aes_ctr; mod compression; mod cp437; mod crc32; +mod datetime; pub mod extra_fields; mod path; pub mod read; From 2424622b181da22eb6ee92f8f3fe8ec7104e80b3 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:28:30 -0600 Subject: [PATCH 25/52] Update types.rs --- src/types.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/types.rs b/src/types.rs index e0e747dda..acb631d19 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,7 +4,6 @@ use crate::CompressionMethod; use crate::cp437::FromCp437; use crate::extra_fields::ExtraField; use crate::path::{enclosed_name, file_name_sanitized}; -use crate::result::DateTimeRangeError; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::is_dir; use crate::spec::{self, FixedSizeBlock, Magic, Pod, ZipFlags}; @@ -831,7 +830,7 @@ mod test { #[test] fn sanitize() { use super::{System, ZipFileData}; - use std::{path::PathBuf, sync::Once lock}; + use std::{path::PathBuf, sync::OnceLock}; let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string(); let data = ZipFileData { From 9c0f1a9b7622d1148a451837549aee3fd837dc33 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:30:03 -0600 Subject: [PATCH 26/52] Update datetime.rs --- src/datetime.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/datetime.rs b/src/datetime.rs index cc35f65a3..e00c61c08 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,4 +1,7 @@ -//! Code related to Date time in zip files +//! Code related to `DateTime` in zip files + +use crate::result::DateTimeRangeError; +use core::fmt; /// Representation of a moment in time. /// @@ -22,8 +25,8 @@ pub struct DateTime { timepart: u16, } -impl Debug for DateTime { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +impl fmt::Debug for DateTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if *self == Self::default() { return f.write_str("DateTime::default()"); } From c99429db721a88ffdcfab0657cce959d9cabf9ac Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:55:48 -0600 Subject: [PATCH 27/52] Update datetime.rs --- src/datetime.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/datetime.rs b/src/datetime.rs index e00c61c08..9cfb51227 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -397,7 +397,6 @@ impl TryFrom for time::PrimitiveDateTime { } } - #[cfg(tests)] mod tests { #[test] From 5ca716380ff53349c64862ca87885487aa7761b8 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 13:56:21 -0600 Subject: [PATCH 28/52] Update lib.rs --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c2544c1ac..f35e88f5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,10 +15,11 @@ #![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) #![allow(clippy::multiple_crate_versions)] // https://github.com/rust-lang/rust-clippy/issues/16440 pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; +pub use crate::datetime::DateTime; pub use crate::read::HasZipMetadata; pub use crate::read::{ZipArchive, ZipReadOptions}; pub use crate::spec::{ZIP64_BYTES_THR, ZIP64_ENTRY_THR}; -pub use crate::types::{AesMode, DateTime, System}; +pub use crate::types::{AesMode, System}; pub use crate::write::ZipWriter; #[cfg(feature = "aes-crypto")] From 084cfb216e9a45e5b484cd0e2e59d520f995480c Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 14:00:30 -0600 Subject: [PATCH 29/52] Update read.rs --- src/read.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/read.rs b/src/read.rs index 0aa3dbe80..8587ffdfa 100644 --- a/src/read.rs +++ b/src/read.rs @@ -3,14 +3,14 @@ use crate::compression::{CompressionMethod, Decompressor}; use crate::cp437::FromCp437; use crate::crc32::Crc32Reader; +use crate::datetime::DateTime; use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs, UsedExtraField}; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::{ - self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, Pod, ZIP64_BYTES_THR, ZipFlags, + self, CentralDirectoryEndInfo, DataAndPosition, Pod, ZIP64_BYTES_THR, ZipCentralEntryBlock, ZipFlags, ZipLocalEntryBlock, }; use crate::types::{ - AesMode, AesVendorVersion, DateTime, SimpleFileOptions, System, ZipCentralEntryBlock, - ZipFileData, ZipLocalEntryBlock, + AesMode, AesVendorVersion, SimpleFileOptions, System, ZipFileData }; use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator}; use core::mem::{replace, size_of}; @@ -2052,7 +2052,7 @@ impl<'a, R: Read + ?Sized> ZipFile<'a, R> { .unix_permissions(self.unix_mode().unwrap_or(0o644) | S_IFREG) .last_modified_time( self.last_modified() - .filter(super::types::DateTime::is_valid) + .filter(DateTime::is_valid) .unwrap_or_else(DateTime::default_for_write), ); From efe7f8faeb9efce74682bea454f213b19bd7edc2 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 14:01:47 -0600 Subject: [PATCH 30/52] Update stream.rs --- src/read/stream.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/read/stream.rs b/src/read/stream.rs index 416f11a50..468d2ed89 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -1,8 +1,8 @@ use super::{ - ZipCentralEntryBlock, ZipFile, ZipFileData, ZipResult, central_header_to_zip_file_inner, + ZipFile, ZipFileData, ZipResult, central_header_to_zip_file_inner, make_symlink, read_zipfile_from_stream, }; -use crate::spec::FixedSizeBlock; +use crate::spec::{FixedSizeBlock, ZipCentralEntryBlock}; use indexmap::IndexMap; use std::io::{self, Read}; use std::path::{Path, PathBuf}; From 96f87dfd04fae140456ec356d0d97d19cbf5cbab Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 14:06:09 -0600 Subject: [PATCH 31/52] Update types.rs --- src/types.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/types.rs b/src/types.rs index acb631d19..9ed17494e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,15 +2,16 @@ use crate::CompressionMethod; use crate::cp437::FromCp437; +use crate::datetime::DateTime; use crate::extra_fields::ExtraField; use crate::path::{enclosed_name, file_name_sanitized}; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::is_dir; -use crate::spec::{self, FixedSizeBlock, Magic, Pod, ZipFlags}; +use crate::spec::{self, FixedSizeBlock, ZipCentralEntryBlock, ZipDataDescriptorBlock,Zip64DataDescriptorBlock, ZipFlags, ZipLocalEntryBlock}; use crate::types::ffi::S_IFDIR; use crate::write::FileOptionExtension; use crate::zipcrypto::EncryptWith; -use core::fmt::{self, Debug, Formatter}; +use core::fmt::Debug; use std::ffi::OsStr; use std::fmt::Display; use std::io::{Read, Seek, SeekFrom}; From b48a8ddcb3f9de5dcae08781652e206055f0bb52 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 14:07:23 -0600 Subject: [PATCH 32/52] Update write.rs --- src/write.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/write.rs b/src/write.rs index 25dff7c83..9b8d4631e 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,15 +1,16 @@ //! Writing a ZIP archive use crate::compression::CompressionMethod; +use crate::datetime::DateTime; use crate::extra_fields::AexEncryption; use crate::extra_fields::UsedExtraField; use crate::extra_fields::Zip64ExtendedInformation; use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; use crate::result::{ZipError, ZipResult, invalid}; -use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock}; +use crate::spec::{self, FixedSizeBlock, ZipLocalEntryBlock, Zip32CDEBlock}; use crate::types::ffi::S_IFLNK; use crate::types::{ - AesVendorVersion, DateTime, MIN_VERSION, System, ZipFileData, ZipLocalEntryBlock, ZipRawValues, + AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, ffi, }; use core::default::Default; From 7c62f70c3d0f1eb1754e985c5b3624e7748a612b Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 14:09:36 -0600 Subject: [PATCH 33/52] Update spec.rs --- src/spec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spec.rs b/src/spec.rs index 6f42a28f0..1fadbd84b 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -989,7 +989,7 @@ pub(crate) fn find_central_directory( < eocd64 .number_of_files .saturating_mul( - mem::size_of::() as u64 + mem::size_of::() as u64 ) .saturating_add(eocd64.central_directory_offset) { From 7323f917588b89d380149e3e6ece3b1a4ed58c1b Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 17:10:21 -0600 Subject: [PATCH 34/52] cargo fmt --- src/read.rs | 7 +++---- src/read/stream.rs | 4 ++-- src/spec.rs | 4 +--- src/types.rs | 5 ++++- src/write.rs | 7 ++----- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/read.rs b/src/read.rs index 8587ffdfa..a9ea51074 100644 --- a/src/read.rs +++ b/src/read.rs @@ -7,11 +7,10 @@ use crate::datetime::DateTime; use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs, UsedExtraField}; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::{ - self, CentralDirectoryEndInfo, DataAndPosition, Pod, ZIP64_BYTES_THR, ZipCentralEntryBlock, ZipFlags, ZipLocalEntryBlock, -}; -use crate::types::{ - AesMode, AesVendorVersion, SimpleFileOptions, System, ZipFileData + self, CentralDirectoryEndInfo, DataAndPosition, Pod, ZIP64_BYTES_THR, ZipCentralEntryBlock, + ZipFlags, ZipLocalEntryBlock, }; +use crate::types::{AesMode, AesVendorVersion, SimpleFileOptions, System, ZipFileData}; use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator}; use core::mem::{replace, size_of}; use core::ops::{Deref, Range}; diff --git a/src/read/stream.rs b/src/read/stream.rs index 468d2ed89..206c89c00 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -1,6 +1,6 @@ use super::{ - ZipFile, ZipFileData, ZipResult, central_header_to_zip_file_inner, - make_symlink, read_zipfile_from_stream, + ZipFile, ZipFileData, ZipResult, central_header_to_zip_file_inner, make_symlink, + read_zipfile_from_stream, }; use crate::spec::{FixedSizeBlock, ZipCentralEntryBlock}; use indexmap::IndexMap; diff --git a/src/spec.rs b/src/spec.rs index 1fadbd84b..6b7a9e85f 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -988,9 +988,7 @@ pub(crate) fn find_central_directory( if eocd64_offset < eocd64 .number_of_files - .saturating_mul( - mem::size_of::() as u64 - ) + .saturating_mul(mem::size_of::() as u64) .saturating_add(eocd64.central_directory_offset) { local_error = diff --git a/src/types.rs b/src/types.rs index 9ed17494e..d63812519 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,7 +7,10 @@ use crate::extra_fields::ExtraField; use crate::path::{enclosed_name, file_name_sanitized}; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::is_dir; -use crate::spec::{self, FixedSizeBlock, ZipCentralEntryBlock, ZipDataDescriptorBlock,Zip64DataDescriptorBlock, ZipFlags, ZipLocalEntryBlock}; +use crate::spec::{ + self, FixedSizeBlock, Zip64DataDescriptorBlock, ZipCentralEntryBlock, ZipDataDescriptorBlock, + ZipFlags, ZipLocalEntryBlock, +}; use crate::types::ffi::S_IFDIR; use crate::write::FileOptionExtension; use crate::zipcrypto::EncryptWith; diff --git a/src/write.rs b/src/write.rs index 9b8d4631e..b88183ae3 100644 --- a/src/write.rs +++ b/src/write.rs @@ -7,12 +7,9 @@ use crate::extra_fields::UsedExtraField; use crate::extra_fields::Zip64ExtendedInformation; use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; use crate::result::{ZipError, ZipResult, invalid}; -use crate::spec::{self, FixedSizeBlock, ZipLocalEntryBlock, Zip32CDEBlock}; +use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock, ZipLocalEntryBlock}; use crate::types::ffi::S_IFLNK; -use crate::types::{ - AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, - ffi, -}; +use crate::types::{AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, ffi}; use core::default::Default; use core::fmt::{Debug, Formatter}; use core::marker::PhantomData; From 85446f00ce9380519bb1364caac35a1035cebdcd Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 17:23:28 -0600 Subject: [PATCH 35/52] cargo fmt --- src/write.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/write.rs b/src/write.rs index f8a032581..84ae043a8 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2585,8 +2585,8 @@ mod test { use super::{ExtendedFileOptions, FileOptions, FullFileOptions, ZipWriter}; use crate::CompressionMethod::Stored; use crate::compression::CompressionMethod; - use crate::result::ZipResult; use crate::datetime::DateTime; + use crate::result::ZipResult; use crate::types::System; use crate::write::EncryptWith::ZipCrypto; use crate::write::SimpleFileOptions; From ceed8b21a9c91e085795298b2ab000b9395d511a Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 17:57:34 -0600 Subject: [PATCH 36/52] no vec --- src/extra_fields/aex_encryption.rs | 7 +------ src/write.rs | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index 7026b8ecb..3519d6549 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -21,13 +21,8 @@ pub(crate) struct AexEncryption { unsafe impl Pod for AexEncryption {} impl AexEncryption { - pub(crate) fn serialize(self) -> Box<[u8]> { - let block = self.to_le(); - block.as_bytes().into() - } - #[inline(always)] - fn to_le(mut self) -> Self { + pub(crate) fn to_le(mut self) -> Self { to_le![ self, [ diff --git a/src/write.rs b/src/write.rs index 03d52260e..807809672 100644 --- a/src/write.rs +++ b/src/write.rs @@ -7,7 +7,7 @@ use crate::extra_fields::UsedExtraField; use crate::extra_fields::Zip64ExtendedInformation; use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; use crate::result::{ZipError, ZipResult, invalid}; -use crate::spec::{self, FixedSizeBlock, Magic, Zip32CDEBlock, ZipLocalEntryBlock}; +use crate::spec::{self, FixedSizeBlock, Magic, Pod, Zip32CDEBlock, ZipLocalEntryBlock}; use crate::types::ffi::S_IFLNK; use crate::types::{AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, ffi}; use core::default::Default; @@ -2382,8 +2382,8 @@ fn update_aes_extra_data( extra_data_start + file.aes_extra_data_start, ))?; - let aes_extra_field = AexEncryption::new(*version, *aes_mode, *compression_method); - let buf = &aes_extra_field.serialize(); + let aes_extra_field = AexEncryption::new(*version, *aes_mode, *compression_method).to_le(); + let buf = aes_extra_field.as_bytes(); writer.write_all(buf)?; let aes_extra_data_start = file.aes_extra_data_start as usize; From f6bb26d8817918a3da3b6c2db34de6d676f53ca4 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 18:04:37 -0600 Subject: [PATCH 37/52] refactor: clean up part of the code --- src/read.rs | 20 +++++++++----------- src/types.rs | 16 ++++++---------- src/write.rs | 4 ++-- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/read.rs b/src/read.rs index 80531d9f0..1c72fc990 100644 --- a/src/read.rs +++ b/src/read.rs @@ -4,13 +4,17 @@ use crate::compression::{CompressionMethod, Decompressor}; use crate::cp437::FromCp437; use crate::crc32::Crc32Reader; use crate::datetime::DateTime; +use crate::extra_fields::UnicodeExtraField; use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs, UsedExtraField}; +use crate::result::ZipError::InvalidPassword; use crate::result::{ZipError, ZipResult, invalid}; +use crate::spec::is_dir; use crate::spec::{ self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, ZIP64_BYTES_THR, ZipCentralEntryBlock, ZipFlags, }; -use crate::types::{AesMode, AesVendorVersion, SimpleFileOptions, System, ZipFileData}; +use crate::types::{AesMode, AesVendorVersion, SimpleFileOptions, System, ZipFileData, ffi}; +use crate::unstable::{LittleEndianReadExt, path_to_string}; use crate::zipcrypto::{ZipCryptoReader, ZipCryptoReaderValid, ZipCryptoValidator}; use core::mem::{replace, size_of}; use core::ops::{Deref, Range}; @@ -22,7 +26,6 @@ use std::path::{Component, Path, PathBuf}; use std::sync::{Arc, OnceLock}; mod config; - pub use config::{ArchiveOffset, Config}; /// Provides high level API for reading from a stream. @@ -32,6 +35,8 @@ pub use stream::read_zipfile_from_stream_with_compressed_size; pub(crate) mod magic_finder; +pub use zip_archive::ZipArchive; + /// Immutable metadata about a `ZipArchive`. #[derive(Debug)] pub struct ZipArchiveMetadata { @@ -109,13 +114,6 @@ pub(crate) mod zip_archive { } } -use crate::extra_fields::UnicodeExtraField; -use crate::result::ZipError::InvalidPassword; -use crate::spec::is_dir; -use crate::types::ffi::{S_IFLNK, S_IFREG}; -use crate::unstable::{LittleEndianReadExt, path_to_string}; -pub use zip_archive::ZipArchive; - #[allow(clippy::large_enum_variant)] pub(crate) enum CryptoReader<'a, R: Read + ?Sized> { Plaintext(io::Take<&'a mut R>), @@ -2004,7 +2002,7 @@ impl<'a, R: Read + ?Sized> ZipFile<'a, R> { /// Returns whether the file is actually a symbolic link pub fn is_symlink(&self) -> bool { self.unix_mode() - .is_some_and(|mode| mode & S_IFLNK == S_IFLNK) + .is_some_and(|mode| mode & ffi::S_IFLNK == ffi::S_IFLNK) } /// Returns whether the file is a normal file (i.e. not a directory or symlink) @@ -2050,7 +2048,7 @@ impl<'a, R: Read + ?Sized> ZipFile<'a, R> { let mut options = SimpleFileOptions::default() .large_file(self.compressed_size().max(self.size()) > ZIP64_BYTES_THR) .compression_method(self.compression()) - .unix_permissions(self.unix_mode().unwrap_or(0o644) | S_IFREG) + .unix_permissions(self.unix_mode().unwrap_or(0o644) | ffi::S_IFREG) .last_modified_time( self.last_modified() .filter(DateTime::is_valid) diff --git a/src/types.rs b/src/types.rs index d63812519..fec05f0b7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -11,7 +11,6 @@ use crate::spec::{ self, FixedSizeBlock, Zip64DataDescriptorBlock, ZipCentralEntryBlock, ZipDataDescriptorBlock, ZipFlags, ZipLocalEntryBlock, }; -use crate::types::ffi::S_IFDIR; use crate::write::FileOptionExtension; use crate::zipcrypto::EncryptWith; use core::fmt::Debug; @@ -352,7 +351,7 @@ impl ZipFileData { 45 } else if self .unix_mode() - .is_some_and(|mode| mode & S_IFDIR == S_IFDIR) + .is_some_and(|mode| mode & ffi::S_IFDIR == ffi::S_IFDIR) { // file is directory 20 @@ -812,23 +811,20 @@ mod test { #[test] fn unix_mode_robustness() { use super::{System, ZipFileData}; - use crate::types::ffi::S_IFLNK; + use crate::types::ffi; let mut data = ZipFileData { system: System::Dos, - external_attributes: (S_IFLNK | 0o777) << 16, + external_attributes: (ffi::S_IFLNK | 0o777) << 16, ..ZipFileData::default() }; - assert_eq!(data.unix_mode(), Some(S_IFLNK | 0o777)); + assert_eq!(data.unix_mode(), Some(ffi::S_IFLNK | 0o777)); data.system = System::Unknown; - assert_eq!(data.unix_mode(), Some(S_IFLNK | 0o777)); + assert_eq!(data.unix_mode(), Some(ffi::S_IFLNK | 0o777)); data.external_attributes = 0x10; // DOS directory bit data.system = System::Dos; - assert_eq!( - data.unix_mode().unwrap() & 0o170000, - crate::types::ffi::S_IFDIR - ); + assert_eq!(data.unix_mode().unwrap() & 0o170000, ffi::S_IFDIR); } #[test] diff --git a/src/write.rs b/src/write.rs index 84ae043a8..a5301a957 100644 --- a/src/write.rs +++ b/src/write.rs @@ -8,7 +8,6 @@ use crate::extra_fields::Zip64ExtendedInformation; use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock, ZipLocalEntryBlock}; -use crate::types::ffi::S_IFLNK; use crate::types::{AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, ffi}; use core::default::Default; use core::fmt::{Debug, Formatter}; @@ -1806,7 +1805,8 @@ impl ZipWriter { *options .permissions .as_mut() - .ok_or_else(|| std::io::Error::other("Cannot get permissions as mutable"))? |= S_IFLNK; + .ok_or_else(|| std::io::Error::other("Cannot get permissions as mutable"))? |= + ffi::S_IFLNK; // The symlink target is stored as file content. And compressing the target path // likely wastes space. So always store. options.compression_method = Stored; From ae5e3e0db4a726126e2bf5b1467e70c00cea6b99 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 18:07:54 -0600 Subject: [PATCH 38/52] use core::fmt --- src/compression.rs | 2 +- src/extra_fields/mod.rs | 4 ++-- src/types.rs | 4 ++-- src/write.rs | 8 ++++---- src/zipcrypto.rs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/compression.rs b/src/compression.rs index ef87e9e47..e9087765b 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -304,7 +304,7 @@ pub(crate) enum Decompressor { } impl Debug for Decompressor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Stored(_) => write!(f, "StoredDecompressor"), #[cfg(feature = "deflate-flate2")] diff --git a/src/extra_fields/mod.rs b/src/extra_fields/mod.rs index 8a42fd37c..7bfe5effb 100644 --- a/src/extra_fields/mod.rs +++ b/src/extra_fields/mod.rs @@ -1,6 +1,6 @@ //! Types for extra fields -use std::fmt::Display; +use core::fmt::Display; mod aex_encryption; mod extended_timestamp; @@ -81,7 +81,7 @@ impl From for u16 { } impl Display for UsedExtraField { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "0x{:04X}", *self as u16) } } diff --git a/src/types.rs b/src/types.rs index fec05f0b7..47dc0fb78 100644 --- a/src/types.rs +++ b/src/types.rs @@ -15,7 +15,7 @@ use crate::write::FileOptionExtension; use crate::zipcrypto::EncryptWith; use core::fmt::Debug; use std::ffi::OsStr; -use std::fmt::Display; +use core::fmt::Display; use std::io::{Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; @@ -765,7 +765,7 @@ pub enum AesMode { } impl Display for AesMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Aes128 => write!(f, "AES-128"), Self::Aes192 => write!(f, "AES-192"), diff --git a/src/write.rs b/src/write.rs index a5301a957..3a51c29f6 100644 --- a/src/write.rs +++ b/src/write.rs @@ -54,7 +54,7 @@ impl MaybeEncrypted { } impl Debug for MaybeEncrypted { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { // Don't print W, since it may be a huge Vec f.write_str(match self { MaybeEncrypted::Unencrypted(_) => "Unencrypted", @@ -105,7 +105,7 @@ enum GenericZipWriter { } impl Debug for GenericZipWriter { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self { Self::Closed => f.write_str("Closed"), Self::Storer(w) => f.write_fmt(format_args!("Storer({w:?})")), @@ -184,7 +184,7 @@ pub(crate) mod zip_writer { } impl Debug for ZipWriter { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( "ZipWriter {{files: {:?}, stats: {:?}, writing_to_file: {}, writing_raw: {}, comment: {:?}, flush_on_finish_file: {}}}", self.files, self.stats, self.writing_to_file, self.writing_raw, @@ -443,7 +443,7 @@ impl ExtendedFileOptions { } impl Debug for ExtendedFileOptions { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), core::fmt::Error> { f.write_fmt(format_args!("ExtendedFileOptions {{extra_data: vec!{:?}.into(), central_extra_data: vec!{:?}.into()}}", self.extra_data, self.central_extra_data)) } diff --git a/src/zipcrypto.rs b/src/zipcrypto.rs index 651254b43..776e265ad 100644 --- a/src/zipcrypto.rs +++ b/src/zipcrypto.rs @@ -55,14 +55,14 @@ pub(crate) struct ZipCryptoKeys { impl Debug for ZipCryptoKeys { #[cfg(any(test, fuzzing))] - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( "ZipCryptoKeys::of({:#10x},{:#10x},{:#10x})", self.key_0, self.key_1, self.key_2 )) } #[cfg(not(any(test, fuzzing)))] - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; let mut t = DefaultHasher::new(); From 093e62d4d743b06fa7bde9262f1d58c8e7ecfa90 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 18:13:38 -0600 Subject: [PATCH 39/52] cargo fmt --- src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 47dc0fb78..61f9ecc52 100644 --- a/src/types.rs +++ b/src/types.rs @@ -14,8 +14,8 @@ use crate::spec::{ use crate::write::FileOptionExtension; use crate::zipcrypto::EncryptWith; use core::fmt::Debug; -use std::ffi::OsStr; use core::fmt::Display; +use std::ffi::OsStr; use std::io::{Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; From 8d763a735e5ef90545ab304771014592fa130a50 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 18:44:31 -0600 Subject: [PATCH 40/52] update imports --- src/extra_fields/extended_timestamp.rs | 2 +- src/legacy/reduce.rs | 4 ++-- src/legacy/shrink.rs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/extra_fields/extended_timestamp.rs b/src/extra_fields/extended_timestamp.rs index 1c0a90fd3..f522349d7 100644 --- a/src/extra_fields/extended_timestamp.rs +++ b/src/extra_fields/extended_timestamp.rs @@ -5,7 +5,7 @@ use crate::result::invalid; use crate::result::{ZipError, ZipResult}; use crate::unstable::LittleEndianReadExt; use std::io::Read; -use std::mem; +use core::mem; /// `ExtendedTimestamp` Flags #[rustfmt::skip] diff --git a/src/legacy/reduce.rs b/src/legacy/reduce.rs index 96149e0de..0d87ec317 100644 --- a/src/legacy/reduce.rs +++ b/src/legacy/reduce.rs @@ -22,7 +22,7 @@ struct FollowerSet { /// Read the follower sets from is into fsets. Returns true on success. type FollowerSetArray = [FollowerSet; u8::MAX as usize + 1]; -fn read_follower_sets( +fn read_follower_sets( is: &mut BitReader, ) -> io::Result { let mut fsets = [FollowerSet::default(); u8::MAX as usize + 1]; @@ -52,7 +52,7 @@ fn read_follower_sets( /// /// * `Ok` with the byte if it was successfully read. /// * `Err(io::Error)` on bad data or end of input. -fn read_next_byte( +fn read_next_byte( is: &mut BitReader, prev_byte: u8, fsets: &mut FollowerSetArray, diff --git a/src/legacy/shrink.rs b/src/legacy/shrink.rs index 837d2d577..7356a9590 100644 --- a/src/legacy/shrink.rs +++ b/src/legacy/shrink.rs @@ -102,7 +102,7 @@ fn unshrink_partial_clear(codetab: &mut [Codetab], queue: &mut CodeQueue) { /// Read the next code from the input stream and return it in next_code. Returns /// `Ok(None)` if the end of the stream is reached. If the stream contains invalid /// data, returns `Err`. -fn read_code( +fn read_code( is: &mut BitReader, code_size: &mut u8, codetab: &mut [Codetab], @@ -182,14 +182,14 @@ fn output_code( // table at a later point. let prefix_code = codetab[code as usize] .prefix_code - .ok_or_else(|| std::io::Error::other("Cannot get prefix code"))?; + .ok_or_else(|| io::Error::other("Cannot get prefix code"))?; if cfg!(debug_assertions) { let tab_entry = codetab[code as usize]; assert!(tab_entry.len == UNKNOWN_LEN); assert!( tab_entry .prefix_code - .ok_or_else(|| std::io::Error::other("Cannot get prefix code"))? + .ok_or_else(|| io::Error::other("Cannot get prefix code"))? as usize > CONTROL_CODE ); @@ -340,7 +340,7 @@ impl ShrinkDecoder { } impl Read for ShrinkDecoder { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + fn read(&mut self, buf: &mut [u8]) -> io::Result { if !self.stream_read { self.stream_read = true; let mut compressed_bytes = Vec::new(); From 3ff0cc09260f544e7d6fbcef78b7833be9bd743e Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 19:44:48 -0600 Subject: [PATCH 41/52] fix import --- src/datetime.rs | 5 +++-- src/unstable.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/datetime.rs b/src/datetime.rs index 9cfb51227..835022ff5 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -9,8 +9,9 @@ use core::fmt; /// with its own set of peculiarities. /// For example, it has a resolution of 2 seconds! /// -/// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`], -/// or read from one with [`ZipFile::last_modified`](crate::read::ZipFile::last_modified). +/// A [`DateTime`] can be stored directly in a zipfile with +/// [`FileOptions::last_modified_time`](crate::read::ZipFile::last_modified), or +/// read from one with [`ZipFile::last_modified`](crate::read::ZipFile::last_modified). /// /// # Warning /// diff --git a/src/unstable.rs b/src/unstable.rs index 1ef75e078..b6122c585 100644 --- a/src/unstable.rs +++ b/src/unstable.rs @@ -7,7 +7,7 @@ use std::path::{Component, MAIN_SEPARATOR, Path}; /// Provides high level API for reading from a stream. pub mod stream { - pub use crate::read::stream::*; + pub use crate::read::stream::{ZipStreamVisitor, ZipStreamReader, ZipStreamFileMetadata}; } /// Types for creating ZIP archives. pub mod write { From 66697ce25125a64f0f4be65d9dcfb1ab511cbb45 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 20:08:20 -0600 Subject: [PATCH 42/52] fix imports --- src/extra_fields/aex_encryption.rs | 3 +-- src/extra_fields/extended_timestamp.rs | 2 +- src/legacy/shrink.rs | 3 +-- src/spec.rs | 7 ++++--- src/unstable.rs | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index 17f223dc8..b5c9922af 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -6,9 +6,8 @@ use crate::extra_fields::UsedExtraField; use crate::result::ZipError; use crate::spec::FixedSizeBlock; use crate::spec::Pod; -use crate::to_and_from_le; +use crate::spec::{from_le, to_and_from_le, to_le}; use crate::types::AesVendorVersion; -use crate::{from_le, to_le}; #[derive(Copy, Clone)] #[repr(packed, C)] diff --git a/src/extra_fields/extended_timestamp.rs b/src/extra_fields/extended_timestamp.rs index f522349d7..eebe23d79 100644 --- a/src/extra_fields/extended_timestamp.rs +++ b/src/extra_fields/extended_timestamp.rs @@ -4,8 +4,8 @@ use crate::result::invalid; use crate::result::{ZipError, ZipResult}; use crate::unstable::LittleEndianReadExt; -use std::io::Read; use core::mem; +use std::io::Read; /// `ExtendedTimestamp` Flags #[rustfmt::skip] diff --git a/src/legacy/shrink.rs b/src/legacy/shrink.rs index 7356a9590..66a2e0497 100644 --- a/src/legacy/shrink.rs +++ b/src/legacy/shrink.rs @@ -189,8 +189,7 @@ fn output_code( assert!( tab_entry .prefix_code - .ok_or_else(|| io::Error::other("Cannot get prefix code"))? - as usize + .ok_or_else(|| io::Error::other("Cannot get prefix code"))? as usize > CONTROL_CODE ); } diff --git a/src/spec.rs b/src/spec.rs index 8db36e9f9..e5b66cc0e 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,4 +1,4 @@ -#![macro_use] +//! Code linked with the specifications of the zip file use crate::read::ArchiveOffset; use crate::read::magic_finder::{Backwards, Forward, MagicFinder, OptimisticMagicFinder}; @@ -239,7 +239,6 @@ pub(crate) trait FixedSizeBlock: Pod { } /// Convert all the fields of a struct *from* little-endian representations. -#[macro_export] macro_rules! from_le { ($obj:ident, $field:ident, $type:ty) => { $obj.$field = <$type>::from_le($obj.$field); @@ -252,9 +251,9 @@ macro_rules! from_le { from_le!($obj, [$($rest),+]); }; } +pub(crate) use from_le; /// Convert all the fields of a struct *into* little-endian representations. -#[macro_export] macro_rules! to_le { ($obj:ident, $field:ident, $type:ty) => { $obj.$field = <$type>::to_le($obj.$field); @@ -267,6 +266,7 @@ macro_rules! to_le { to_le!($obj, [$($rest),+]); }; } +pub(crate) use to_le; /* TODO: derive macro to generate these fields? */ /// Implement `from_le()` and `to_le()`, providing the field specification to both macros @@ -286,6 +286,7 @@ macro_rules! to_and_from_le { } }; } +pub(crate) use to_and_from_le; #[derive(Copy, Clone, Debug)] #[repr(packed, C)] diff --git a/src/unstable.rs b/src/unstable.rs index b6122c585..33cc4235f 100644 --- a/src/unstable.rs +++ b/src/unstable.rs @@ -7,7 +7,7 @@ use std::path::{Component, MAIN_SEPARATOR, Path}; /// Provides high level API for reading from a stream. pub mod stream { - pub use crate::read::stream::{ZipStreamVisitor, ZipStreamReader, ZipStreamFileMetadata}; + pub use crate::read::stream::{ZipStreamFileMetadata, ZipStreamReader, ZipStreamVisitor}; } /// Types for creating ZIP archives. pub mod write { From 4176c686652c0d815fa248e117d9ae5b9b831627 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 20:10:56 -0600 Subject: [PATCH 43/52] fix mod tests --- src/compression.rs | 2 +- src/cp437.rs | 2 +- src/crc32.rs | 2 +- src/datetime.rs | 2 +- src/extra_fields/extended_timestamp.rs | 2 +- src/read.rs | 2 +- src/read/stream.rs | 2 +- src/spec.rs | 2 +- src/types.rs | 2 +- src/write.rs | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/compression.rs b/src/compression.rs index e9087765b..1c77e60f1 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -561,7 +561,7 @@ impl Decompressor { } #[cfg(test)] -mod test { +mod tests { use super::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; #[test] diff --git a/src/cp437.rs b/src/cp437.rs index 89e27fdb7..12e5a1d34 100644 --- a/src/cp437.rs +++ b/src/cp437.rs @@ -167,7 +167,7 @@ fn to_char(input: u8) -> char { } #[cfg(test)] -mod test { +mod tests { #[test] fn to_char_valid() { for i in u8::MIN..=u8::MAX { diff --git a/src/crc32.rs b/src/crc32.rs index 917487e2c..789c80f4e 100644 --- a/src/crc32.rs +++ b/src/crc32.rs @@ -84,7 +84,7 @@ impl Read for Crc32Reader { } #[cfg(test)] -mod test { +mod tests { use std::io::Read; use super::Crc32Reader; diff --git a/src/datetime.rs b/src/datetime.rs index 835022ff5..5e396dd89 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -398,7 +398,7 @@ impl TryFrom for time::PrimitiveDateTime { } } -#[cfg(tests)] +#[cfg(test)] mod tests { #[test] #[allow(clippy::unusual_byte_groupings)] diff --git a/src/extra_fields/extended_timestamp.rs b/src/extra_fields/extended_timestamp.rs index eebe23d79..0521fd5f2 100644 --- a/src/extra_fields/extended_timestamp.rs +++ b/src/extra_fields/extended_timestamp.rs @@ -165,7 +165,7 @@ impl ExtendedTimestamp { } #[cfg(test)] -mod test { +mod tests { use super::ExtendedTimestamp; use std::io::Cursor; diff --git a/src/read.rs b/src/read.rs index 1c72fc990..11b277af7 100644 --- a/src/read.rs +++ b/src/read.rs @@ -2220,7 +2220,7 @@ fn generate_chrono_datetime(datetime: &DateTime) -> Option( } #[cfg(test)] -mod test { +mod tests { use tempfile::TempDir; use crate::ZipWriter; diff --git a/src/spec.rs b/src/spec.rs index e5b66cc0e..fafa6cea1 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1023,7 +1023,7 @@ pub(crate) fn is_dir(filename: &str) -> bool { } #[cfg(test)] -mod test { +mod tests { use std::io::Cursor; use crate::{ diff --git a/src/types.rs b/src/types.rs index 61f9ecc52..1e71509fb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -794,7 +794,7 @@ impl AesMode { } #[cfg(test)] -mod test { +mod tests { #[test] fn system() { use super::System; diff --git a/src/write.rs b/src/write.rs index 3a51c29f6..6e1c6d431 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2581,7 +2581,7 @@ impl Seek for StreamWriter { #[allow(unknown_lints)] // needless_update is new in clippy pre 1.29.0 #[allow(clippy::needless_update)] // So we can use the same FileOptions decls with and without zopfli_buffer_size #[allow(clippy::octal_escapes)] // many false positives in converted fuzz cases -mod test { +mod tests { use super::{ExtendedFileOptions, FileOptions, FullFileOptions, ZipWriter}; use crate::CompressionMethod::Stored; use crate::compression::CompressionMethod; From 7da1762a8953a310bf677ce005db30841300089e Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 23:22:52 -0600 Subject: [PATCH 44/52] resync --- src/spec.rs | 160 ---------------------------------------------------- 1 file changed, 160 deletions(-) diff --git a/src/spec.rs b/src/spec.rs index 83781cdb4..fafa6cea1 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -448,166 +448,6 @@ impl FixedSizeBlock for Zip64DataDescriptorBlock { ]; } -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct ZipCentralEntryBlock { - pub(crate) magic: Magic, - pub version_made_by: u16, - pub version_to_extract: u16, - pub flags: u16, - pub compression_method: u16, - pub last_mod_time: u16, - pub last_mod_date: u16, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, - pub file_name_length: u16, - pub extra_field_length: u16, - pub file_comment_length: u16, - pub disk_number: u16, - pub internal_file_attributes: u16, - pub external_file_attributes: u32, - pub offset: u32, -} - -unsafe impl Pod for ZipCentralEntryBlock {} - -impl FixedSizeBlock for ZipCentralEntryBlock { - type Magic = Magic; - const MAGIC: Magic = Magic::CENTRAL_DIRECTORY_HEADER_SIGNATURE; - - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid Central Directory header"); - - to_and_from_le![ - (magic, Magic), - (version_made_by, u16), - (version_to_extract, u16), - (flags, u16), - (compression_method, u16), - (last_mod_time, u16), - (last_mod_date, u16), - (crc32, u32), - (compressed_size, u32), - (uncompressed_size, u32), - (file_name_length, u16), - (extra_field_length, u16), - (file_comment_length, u16), - (disk_number, u16), - (internal_file_attributes, u16), - (external_file_attributes, u32), - (offset, u32), - ]; -} - -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct ZipLocalEntryBlock { - pub(crate) magic: Magic, - pub version_made_by: u16, - pub flags: u16, - pub compression_method: u16, - pub last_mod_time: u16, - pub last_mod_date: u16, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, - pub file_name_length: u16, - pub extra_field_length: u16, -} - -unsafe impl Pod for ZipLocalEntryBlock {} - -impl FixedSizeBlock for ZipLocalEntryBlock { - type Magic = Magic; - const MAGIC: Magic = Magic::LOCAL_FILE_HEADER_SIGNATURE; - - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid local file header"); - - to_and_from_le![ - (magic, Magic), - (version_made_by, u16), - (flags, u16), - (compression_method, u16), - (last_mod_time, u16), - (last_mod_date, u16), - (crc32, u32), - (compressed_size, u32), - (uncompressed_size, u32), - (file_name_length, u16), - (extra_field_length, u16), - ]; -} - -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct ZipDataDescriptorBlock { - pub(crate) magic: Magic, - pub crc32: u32, - pub compressed_size: u32, - pub uncompressed_size: u32, -} - -unsafe impl Pod for ZipDataDescriptorBlock {} - -impl FixedSizeBlock for ZipDataDescriptorBlock { - type Magic = Magic; - const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE; - - #[inline(always)] - fn magic(self) -> Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid data descriptor header"); - - to_and_from_le![ - (magic, Magic), - (crc32, u32), - (compressed_size, u32), - (uncompressed_size, u32), - ]; -} - -#[derive(Copy, Clone, Debug)] -#[repr(packed, C)] -pub(crate) struct Zip64DataDescriptorBlock { - pub(crate) magic: Magic, - pub crc32: u32, - pub compressed_size: u64, - pub uncompressed_size: u64, -} - -unsafe impl Pod for Zip64DataDescriptorBlock {} - -impl FixedSizeBlock for Zip64DataDescriptorBlock { - type Magic = Magic; - const MAGIC: Magic = Magic::DATA_DESCRIPTOR_SIGNATURE; - - #[inline] - fn magic(self) -> Magic { - self.magic - } - - const WRONG_MAGIC_ERROR: ZipError = invalid!("Invalid zip64 data descriptor header"); - - to_and_from_le![ - (magic, Magic), - (crc32, u32), - (compressed_size, u64), - (uncompressed_size, u64), - ]; -} - #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct Zip32CDEBlock { From c93682b6a16c5d896f4c8010157b82900db766a7 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 18 Mar 2026 23:45:56 -0600 Subject: [PATCH 45/52] resync --- src/extra_fields/aex_encryption.rs | 2 +- src/spec.rs | 5 +---- src/types.rs | 4 ++-- src/write.rs | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/extra_fields/aex_encryption.rs b/src/extra_fields/aex_encryption.rs index 3519d6549..2a1f95445 100644 --- a/src/extra_fields/aex_encryption.rs +++ b/src/extra_fields/aex_encryption.rs @@ -4,7 +4,7 @@ use crate::AesMode; use crate::CompressionMethod; use crate::extra_fields::UsedExtraField; use crate::spec::Pod; -use crate::to_le; +use crate::spec::to_le; use crate::types::AesVendorVersion; #[derive(Copy, Clone)] diff --git a/src/spec.rs b/src/spec.rs index 545bfd71f..5429f96c9 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -277,7 +277,6 @@ macro_rules! from_le { from_le!($obj, [$($rest),+]); }; } -pub(crate) use from_le; /// Convert all the fields of a struct *into* little-endian representations. macro_rules! to_le { @@ -312,7 +311,6 @@ macro_rules! to_and_from_le { } }; } -pub(crate) use to_and_from_le; #[derive(Copy, Clone, Debug)] #[repr(packed, C)] @@ -959,8 +957,7 @@ pub(crate) fn find_central_directory( < eocd64 .number_of_files .saturating_mul( - (mem::size_of::() - + mem::size_of::()) + (mem::size_of::() + mem::size_of::()) as u64, ) .saturating_add(eocd64.central_directory_offset) diff --git a/src/types.rs b/src/types.rs index 1be60de6a..ed56d8fe6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -8,8 +8,8 @@ use crate::path::{enclosed_name, file_name_sanitized}; use crate::result::{ZipError, ZipResult, invalid}; use crate::spec::is_dir; use crate::spec::{ - self, FixedSizeBlock, Magic, Zip64DataDescriptorBlock, ZipCentralEntryBlock, ZipDataDescriptorBlock, - ZipFlags, ZipLocalEntryBlock, + self, FixedSizeBlock, Magic, Zip64DataDescriptorBlock, ZipCentralEntryBlock, + ZipDataDescriptorBlock, ZipFlags, ZipLocalEntryBlock, }; use crate::write::FileOptionExtension; use crate::zipcrypto::EncryptWith; diff --git a/src/write.rs b/src/write.rs index 4b49da902..5f685556b 100644 --- a/src/write.rs +++ b/src/write.rs @@ -7,7 +7,7 @@ use crate::extra_fields::UsedExtraField; use crate::extra_fields::Zip64ExtendedInformation; use crate::read::{Config, ZipArchive, ZipFile, parse_single_extra_field}; use crate::result::{ZipError, ZipResult, invalid}; -use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock, ZipLocalEntryBlock}; +use crate::spec::{self, FixedSizeBlock, Magic, Pod, Zip32CDEBlock, ZipLocalEntryBlock}; use crate::types::{AesVendorVersion, MIN_VERSION, System, ZipFileData, ZipRawValues, ffi}; use core::default::Default; use core::fmt::{Debug, Formatter}; From f20b626683a32671d21b8929e75053db628ddaba Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 19 Mar 2026 17:54:48 -0600 Subject: [PATCH 46/52] add bench --- Cargo.toml | 4 +++ benches/read_many_entries.rs | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 benches/read_many_entries.rs diff --git a/Cargo.toml b/Cargo.toml index 3c9a277dd..0ca5064ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,10 @@ harness = false name = "read_metadata" harness = false +[[bench]] +name = "read_many_entries" +harness = false + [[bench]] name = "merge_archive" harness = false diff --git a/benches/read_many_entries.rs b/benches/read_many_entries.rs new file mode 100644 index 000000000..cc34af81f --- /dev/null +++ b/benches/read_many_entries.rs @@ -0,0 +1,47 @@ +use bencher::{benchmark_group, benchmark_main}; + +use std::io::{Cursor, Read, Write}; + +use bencher::Bencher; +use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions}; + +fn generate_random_archive(size: usize) -> Result, std::io::Error> { + let data = Vec::new(); + let mut writer = ZipWriter::new(Cursor::new(data)); + let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); + for count in 0..600 { + writer.start_file(format!("random_{}.dat", count), options)?; + let mut bytes = vec![0u8; size]; + getrandom::fill(&mut bytes) + .map_err(|e| std::io::Error::other(format!("getrandom error: {}", e)))?; + writer.write_all(&bytes)?; + } + let w = writer.finish()?; + + Ok(w.into_inner()) +} + +fn read_many_entries(bench: &mut Bencher) { + let size = 1024 * 1024; + let bytes = generate_random_archive(size) + .expect("Failed to create a random archive for the bench read_entry()"); + let mut archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); + + bench.iter(|| { + for idx in 0..archive.len() { + let mut file = archive.by_index(idx).unwrap(); + let mut buf = [0u8; 1024]; + loop { + let n = file.read(&mut buf).unwrap(); + if n == 0 { + break; + } + } + } + }); + + bench.bytes = size as u64; +} + +benchmark_group!(benches, read_many_entries); +benchmark_main!(benches); From 621208db42d865302ff24263274620bb17a8d180 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 19 Mar 2026 18:00:56 -0600 Subject: [PATCH 47/52] update bench --- benches/read_many_entries.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/benches/read_many_entries.rs b/benches/read_many_entries.rs index cc34af81f..fd76ee08d 100644 --- a/benches/read_many_entries.rs +++ b/benches/read_many_entries.rs @@ -5,11 +5,13 @@ use std::io::{Cursor, Read, Write}; use bencher::Bencher; use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions}; +const NB_FILES: usize = 200; + fn generate_random_archive(size: usize) -> Result, std::io::Error> { let data = Vec::new(); let mut writer = ZipWriter::new(Cursor::new(data)); let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); - for count in 0..600 { + for count in 0..NB_FILES { writer.start_file(format!("random_{}.dat", count), options)?; let mut bytes = vec![0u8; size]; getrandom::fill(&mut bytes) @@ -40,7 +42,7 @@ fn read_many_entries(bench: &mut Bencher) { } }); - bench.bytes = size as u64; + bench.bytes = (size * NB_FILES) as u64; } benchmark_group!(benches, read_many_entries); From 6e737ad1ecb05d3acd0e221d65c6e68917e62fa6 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 19 Mar 2026 18:39:29 -0600 Subject: [PATCH 48/52] use io sink --- benches/read_many_entries.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/benches/read_many_entries.rs b/benches/read_many_entries.rs index fd76ee08d..99e083996 100644 --- a/benches/read_many_entries.rs +++ b/benches/read_many_entries.rs @@ -32,13 +32,7 @@ fn read_many_entries(bench: &mut Bencher) { bench.iter(|| { for idx in 0..archive.len() { let mut file = archive.by_index(idx).unwrap(); - let mut buf = [0u8; 1024]; - loop { - let n = file.read(&mut buf).unwrap(); - if n == 0 { - break; - } - } + let n = std::io::copy(&mut file, &mut std::io::sink()).unwrap(); } }); From 2f6ac911d077a2180a07974d7b6e356a0e526284 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 19 Mar 2026 19:21:44 -0600 Subject: [PATCH 49/52] add bufReader --- benches/read_many_entries.rs | 56 ++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/benches/read_many_entries.rs b/benches/read_many_entries.rs index 99e083996..3f6933609 100644 --- a/benches/read_many_entries.rs +++ b/benches/read_many_entries.rs @@ -1,11 +1,13 @@ use bencher::{benchmark_group, benchmark_main}; -use std::io::{Cursor, Read, Write}; +use std::fs::File; +use std::io::{Cursor, Write}; use bencher::Bencher; use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions}; -const NB_FILES: usize = 200; +const NB_FILES: usize = 100; +const FILENAME: &str = "bench_read_many_entries.zip"; fn generate_random_archive(size: usize) -> Result, std::io::Error> { let data = Vec::new(); @@ -23,21 +25,65 @@ fn generate_random_archive(size: usize) -> Result, std::io::Error> { Ok(w.into_inner()) } +fn generate_random_archive_to_file(size: usize) -> Result<(), std::io::Error> { + let bytes = generate_random_archive(size)?; + let mut file = File::create(FILENAME)?; + file.write_all(&bytes)?; + Ok(()) +} + +fn read_many_entries_as_file(bench: &mut Bencher) { + let size = 1024 * 1024; + generate_random_archive_to_file(size) + .expect("Failed to create a random archive for the bench read_entry()"); + + bench.iter(|| { + let file = File::open(FILENAME).unwrap(); + let mut archive = ZipArchive::new(file).unwrap(); + for idx in 0..archive.len() { + let mut file = archive.by_index(idx).unwrap(); + let _n = std::io::copy(&mut file, &mut std::io::sink()).unwrap(); + } + }); + + bench.bytes = (size * NB_FILES) as u64; + std::fs::remove_file(FILENAME).unwrap(); +} + +fn read_many_entries_as_file_buf(bench: &mut Bencher) { + let size = 1024 * 1024; + generate_random_archive_to_file(size) + .expect("Failed to create a random archive for the bench read_entry()"); + + bench.iter(|| { + let file = File::open(FILENAME).unwrap(); + let file = std::io::BufReader::new(file); + let mut archive = ZipArchive::new(file).unwrap(); + for idx in 0..archive.len() { + let mut file = archive.by_index(idx).unwrap(); + let _n = std::io::copy(&mut file, &mut std::io::sink()).unwrap(); + } + }); + + bench.bytes = (size * NB_FILES) as u64; + std::fs::remove_file(FILENAME).unwrap(); +} + fn read_many_entries(bench: &mut Bencher) { let size = 1024 * 1024; let bytes = generate_random_archive(size) .expect("Failed to create a random archive for the bench read_entry()"); - let mut archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); bench.iter(|| { + let mut archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); for idx in 0..archive.len() { let mut file = archive.by_index(idx).unwrap(); - let n = std::io::copy(&mut file, &mut std::io::sink()).unwrap(); + let _n = std::io::copy(&mut file, &mut std::io::sink()).unwrap(); } }); bench.bytes = (size * NB_FILES) as u64; } -benchmark_group!(benches, read_many_entries); +benchmark_group!(benches, read_many_entries, read_many_entries_as_file, read_many_entries_as_file_buf); benchmark_main!(benches); From e42ae601dafd5d4c45986d4b5be65fa663233cc6 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 19 Mar 2026 20:43:57 -0600 Subject: [PATCH 50/52] move to becnh --- benches/read_entry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/read_entry.rs b/benches/read_entry.rs index 88be9c8ab..f8f72cd59 100644 --- a/benches/read_entry.rs +++ b/benches/read_entry.rs @@ -25,9 +25,9 @@ fn read_entry(bench: &mut Bencher) { let size = 1024 * 1024; let bytes = generate_random_archive(size) .expect("Failed to create a random archive for the bench read_entry()"); - let mut archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); bench.iter(|| { + let mut archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); let mut file = archive.by_name("random.dat").unwrap(); let mut buf = [0u8; 1024]; loop { From 26d6f9c945a0e68957309ad3ee14681fdda5c166 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 19 Mar 2026 21:20:21 -0600 Subject: [PATCH 51/52] fix endianness --- .github/workflows/ci.yaml | 19 +++++++++++++++++++ src/spec.rs | 9 ++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd74cfbfb..a136a42b9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -121,6 +121,25 @@ jobs: - run: rustup target add wasm32-unknown-unknown - run: cargo install wasm-pack --locked - run: wasm-pack test ${{ matrix.browser_flags }} --manifest-path ${{ github.workspace }}/${{ matrix.manifest }} ${{ matrix.feature_flag }} + endianness_miri: + name: "Test endianness platform" + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: target + key: ${{ runner.os }}-rust-${{ hashFiles('Cargo.toml') }} + - run: sudo apt update + - run: rustup toolchain add nightly-x86_64-unknown-linux-gnu + - run: rustup component add --toolchain nightly-x86_64-unknown-linux-gnu miri + - run: cargo +nightly miri test --target aarch64_be-unknown-linux-gnu --all --no-default-features miri: strategy: matrix: diff --git a/src/spec.rs b/src/spec.rs index c746331c4..ebe20c2a7 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -222,10 +222,6 @@ impl BlockWithMagic { pub(crate) trait FixedSizeBlock: Pod { const MAGIC: Magic; - fn magic(self) -> Magic { - Self::MAGIC - } - const WRONG_MAGIC_ERROR: ZipError; #[allow(clippy::wrong_self_convention)] @@ -243,6 +239,7 @@ pub(crate) trait FixedSizeBlock: Pod { magic, inner: block, } = block_with_magic; + let magic = Magic::from_le(magic); let block = Self::from_le(block); if magic != Self::MAGIC { @@ -255,7 +252,7 @@ pub(crate) trait FixedSizeBlock: Pod { fn write(self, writer: &mut T) -> ZipResult<()> { let block = BlockWithMagic { - magic: self.magic(), + magic: Self::MAGIC, inner: self, }; let block = block.to_le(); @@ -277,7 +274,6 @@ macro_rules! from_le { from_le!($obj, [$($rest),+]); }; } -pub(crate) use from_le; /// Convert all the fields of a struct *into* little-endian representations. macro_rules! to_le { @@ -312,7 +308,6 @@ macro_rules! to_and_from_le { } }; } -pub(crate) use to_and_from_le; #[derive(Copy, Clone, Debug)] #[repr(packed, C)] From 146183c36bac395345dc4f5ca0fe490cea0d6e3a Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 21 Mar 2026 09:03:16 -0600 Subject: [PATCH 52/52] cargo fmt --- benches/read_many_entries.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/benches/read_many_entries.rs b/benches/read_many_entries.rs index 3f6933609..5c99d1389 100644 --- a/benches/read_many_entries.rs +++ b/benches/read_many_entries.rs @@ -85,5 +85,10 @@ fn read_many_entries(bench: &mut Bencher) { bench.bytes = (size * NB_FILES) as u64; } -benchmark_group!(benches, read_many_entries, read_many_entries_as_file, read_many_entries_as_file_buf); +benchmark_group!( + benches, + read_many_entries, + read_many_entries_as_file, + read_many_entries_as_file_buf +); benchmark_main!(benches);