From 1732fef21370d6ff2360e8f314ddbf2d0d858c84 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 5 Feb 2026 20:10:04 -0700 Subject: [PATCH 01/32] fix: zip64 central header --- examples/write_dir.rs | 12 ++++----- src/types.rs | 57 +++++++++++++++++++++++++++++++------------ src/write.rs | 8 +++--- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/examples/write_dir.rs b/examples/write_dir.rs index 030dfea26..046b7bd4a 100644 --- a/examples/write_dir.rs +++ b/examples/write_dir.rs @@ -110,24 +110,24 @@ fn zip_dir( } }; let path = entry.path(); - let name = path.strip_prefix(src_dir)?; - let path_as_string = name + let path_stripped = path.strip_prefix(src_dir)?; + let path_as_string = path_stripped .to_str() .map(str::to_owned) - .ok_or_else(|| format!("{name:?} is a Non UTF-8 Path"))?; + .ok_or_else(|| format!("'{}' is a Non UTF-8 Path", path_stripped.display()))?; // Write file or directory explicitly // Some unzip tools unzip files with directory paths correctly, some do not! if path.is_file() { - println!("adding file {path:?} as {name:?} ..."); + println!("adding file '{}' as '{}' ...", path.display(), path_stripped.display()); zip.start_file(path_as_string, options)?; let mut f = File::open(path)?; std::io::copy(&mut f, &mut zip)?; - } else if !name.as_os_str().is_empty() { + } else if !path_stripped.as_os_str().is_empty() { // Only if not root! Avoids path spec / warning // and mapname conversion failed error on unzip - println!("adding dir {path_as_string:?} as {name:?} ..."); + println!("adding dir '{}' as '{}' ...", path.display(), path_stripped.display()); zip.add_directory(path_as_string, options)?; } } diff --git a/src/types.rs b/src/types.rs index e8b92235e..54dff114a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -953,7 +953,46 @@ impl ZipFileData { }) } + pub(crate) fn is_zip_64(&self) -> bool { + if self.compressed_size > spec::ZIP64_BYTES_THR { + return true; + } + if self.uncompressed_size > spec::ZIP64_BYTES_THR { + return true; + } + if self.header_start > spec::ZIP64_BYTES_THR { + return true; + } + // TODO: Also disk number (unsupported for now) + false + } + pub(crate) fn block(&self) -> ZipResult { + let is_zip_64 = self.is_zip_64(); + let compressed_size = if is_zip_64 { + spec::ZIP64_BYTES_THR as u32 + } else { + self.compressed_size + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)? + }; + let uncompressed_size = if is_zip_64 { + spec::ZIP64_BYTES_THR as u32 + } else { + self.uncompressed_size + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)? + }; + let offset = if is_zip_64 { + spec::ZIP64_BYTES_THR as u32 + } else { + self.header_start + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)? + }; let extra_field_len: u16 = self .extra_field_len() .try_into() @@ -976,16 +1015,8 @@ impl ZipFileData { last_mod_time: last_modified_time.timepart(), last_mod_date: last_modified_time.datepart(), crc32: self.crc32, - compressed_size: self - .compressed_size - .min(spec::ZIP64_BYTES_THR) - .try_into() - .map_err(std::io::Error::other)?, - uncompressed_size: self - .uncompressed_size - .min(spec::ZIP64_BYTES_THR) - .try_into() - .map_err(std::io::Error::other)?, + compressed_size, + uncompressed_size, file_name_length: self .file_name_raw .len() @@ -1002,11 +1033,7 @@ impl ZipFileData { disk_number: 0, internal_file_attributes: 0, external_file_attributes: self.external_attributes, - offset: self - .header_start - .min(spec::ZIP64_BYTES_THR) - .try_into() - .map_err(std::io::Error::other)?, + offset, }) } diff --git a/src/write.rs b/src/write.rs index 8a4ae5e50..8e0853e0d 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2232,12 +2232,13 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) Vec::new() }; let central_len = file.central_extra_field_len(); - let zip64_len = if let Some(zip64) = file.zip64_extra_field_block() { + let zip64_extra_field_block = file.zip64_extra_field_block(); + let zip64_block_len = if let Some(zip64) = zip64_extra_field_block { zip64.full_size() } else { 0 }; - block.extra_field_length = (zip64_len + stripped_extra.len() + central_len) + block.extra_field_length = (zip64_block_len + stripped_extra.len() + central_len) .try_into() .map_err(|_| invalid!("Extra field length in central directory exceeds 64KiB"))?; @@ -2245,7 +2246,7 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) // file name writer.write_all(&file.file_name_raw)?; // extra field - if let Some(zip64_extra_field) = &file.zip64_extra_field_block() { + if let Some(zip64_extra_field) = zip64_extra_field_block { writer.write_all(&zip64_extra_field.serialize())?; } if !stripped_extra.is_empty() { @@ -2286,6 +2287,7 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, ) -> ZipResult<()> { + println!("UPDATE LOCAL"); let block = file.zip64_extra_field_block().ok_or(invalid!( "Attempted to update a nonexistent ZIP64 extra field" ))?; From 93ef7f9445b3ed24b81357d97b9ad43b9478df93 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 5 Feb 2026 20:13:04 -0700 Subject: [PATCH 02/32] rm typo --- src/write.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/write.rs b/src/write.rs index 8e0853e0d..fec85bb50 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2287,7 +2287,6 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, ) -> ZipResult<()> { - println!("UPDATE LOCAL"); let block = file.zip64_extra_field_block().ok_or(invalid!( "Attempted to update a nonexistent ZIP64 extra field" ))?; From 74e8085cc6364982f780e81ac9e46addd7ba551c Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 5 Feb 2026 20:53:20 -0700 Subject: [PATCH 03/32] revert and re-fix --- src/types.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/types.rs b/src/types.rs index 54dff114a..063dd35a5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1203,13 +1203,22 @@ impl Zip64ExtraFieldBlock { header_start: u64, ) -> Option { let mut size: u16 = 0; - let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file { + // we need this field if others fields are (compressed_size, header_start) present + let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR + || compressed_size >= ZIP64_BYTES_THR + || header_start >= ZIP64_BYTES_THR + || large_file + { size += mem::size_of::() as u16; Some(uncompressed_size) } else { None }; - let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file { + // we need this field if other field (header_start) ais present + let compressed_size = if compressed_size >= ZIP64_BYTES_THR + || header_start >= ZIP64_BYTES_THR + || large_file + { size += mem::size_of::() as u16; Some(compressed_size) } else { @@ -1221,6 +1230,8 @@ impl Zip64ExtraFieldBlock { } else { None }; + // TODO: (unsopported for now) + // Disk Start Number 4 bytes Number of the disk on which this file starts if size == 0 { return None; } From 16debc5f34c2358365d3dca9cb5dc691dd3285b7 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 5 Feb 2026 21:01:31 -0700 Subject: [PATCH 04/32] fmt --- examples/write_dir.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/write_dir.rs b/examples/write_dir.rs index 046b7bd4a..3ed688bd1 100644 --- a/examples/write_dir.rs +++ b/examples/write_dir.rs @@ -119,7 +119,11 @@ fn zip_dir( // Write file or directory explicitly // Some unzip tools unzip files with directory paths correctly, some do not! if path.is_file() { - println!("adding file '{}' as '{}' ...", path.display(), path_stripped.display()); + println!( + "adding file '{}' as '{}' ...", + path.display(), + path_stripped.display() + ); zip.start_file(path_as_string, options)?; let mut f = File::open(path)?; @@ -127,7 +131,11 @@ fn zip_dir( } else if !path_stripped.as_os_str().is_empty() { // Only if not root! Avoids path spec / warning // and mapname conversion failed error on unzip - println!("adding dir '{}' as '{}' ...", path.display(), path_stripped.display()); + println!( + "adding dir '{}' as '{}' ...", + path.display(), + path_stripped.display() + ); zip.add_directory(path_as_string, options)?; } } From 027138831ed414b18bb1fe4012d764eeacbef362 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 5 Feb 2026 22:59:09 -0700 Subject: [PATCH 05/32] re fix --- src/types.rs | 57 ++++++++++++++++++++++++---------------------------- src/write.rs | 4 ++-- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/types.rs b/src/types.rs index 063dd35a5..9a48370d3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -953,14 +953,14 @@ impl ZipFileData { }) } - pub(crate) fn is_zip_64(&self) -> bool { - if self.compressed_size > spec::ZIP64_BYTES_THR { - return true; - } - if self.uncompressed_size > spec::ZIP64_BYTES_THR { - return true; - } - if self.header_start > spec::ZIP64_BYTES_THR { + pub(crate) fn was_large_file(&self) -> bool { + // if self.compressed_size > spec::ZIP64_BYTES_THR { + // return true; + // } + // if self.uncompressed_size > spec::ZIP64_BYTES_THR { + // return true; + // } + if self.header_start >= spec::ZIP64_BYTES_THR { return true; } // TODO: Also disk number (unsupported for now) @@ -968,7 +968,7 @@ impl ZipFileData { } pub(crate) fn block(&self) -> ZipResult { - let is_zip_64 = self.is_zip_64(); + let is_zip_64 = self.was_large_file(); let compressed_size = if is_zip_64 { spec::ZIP64_BYTES_THR as u32 } else { @@ -1203,28 +1203,21 @@ impl Zip64ExtraFieldBlock { header_start: u64, ) -> Option { let mut size: u16 = 0; - // we need this field if others fields are (compressed_size, header_start) present - let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR - || compressed_size >= ZIP64_BYTES_THR - || header_start >= ZIP64_BYTES_THR - || large_file - { - size += mem::size_of::() as u16; - Some(uncompressed_size) - } else { - None - }; - // we need this field if other field (header_start) ais present - let compressed_size = if compressed_size >= ZIP64_BYTES_THR - || header_start >= ZIP64_BYTES_THR - || large_file - { - size += mem::size_of::() as u16; - Some(compressed_size) - } else { - None - }; - let header_start = if header_start >= ZIP64_BYTES_THR { + let uncompressed_size = + if (uncompressed_size != 0 && uncompressed_size >= ZIP64_BYTES_THR) || large_file { + size += mem::size_of::() as u16; + Some(uncompressed_size) + } else { + None + }; + let compressed_size = + if (compressed_size != 0 && compressed_size >= ZIP64_BYTES_THR) || large_file { + size += mem::size_of::() as u16; + Some(compressed_size) + } else { + None + }; + let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { size += mem::size_of::() as u16; Some(header_start) } else { @@ -1263,6 +1256,7 @@ impl Zip64ExtraFieldBlock { let full_size = self.full_size(); + println!("{self:?}"); let mut ret = Vec::with_capacity(full_size); ret.extend(magic.to_le_bytes()); ret.extend(u16::to_le_bytes(size)); @@ -1278,6 +1272,7 @@ impl Zip64ExtraFieldBlock { } debug_assert_eq!(ret.len(), full_size); + println!("{ret:?} = {}, {}", ret.len(), ret.len() - 4); ret.into_boxed_slice() } } diff --git a/src/write.rs b/src/write.rs index fec85bb50..487ab761f 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2208,8 +2208,8 @@ fn update_local_file_header( update_local_zip64_extra_field(writer, file)?; - file.compressed_size = spec::ZIP64_BYTES_THR; - file.uncompressed_size = spec::ZIP64_BYTES_THR; + // file.compressed_size = spec::ZIP64_BYTES_THR; + // file.uncompressed_size = spec::ZIP64_BYTES_THR; } else { // check compressed size as well as it can also be slightly larger than uncompressed size if file.compressed_size > spec::ZIP64_BYTES_THR { From 06b29c5743305e05d597dd742aab547ce4c53932 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:36:41 -0800 Subject: [PATCH 06/32] Update src/types.rs Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com> Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> --- src/types.rs | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/types.rs b/src/types.rs index 9a48370d3..08f92e089 100644 --- a/src/types.rs +++ b/src/types.rs @@ -969,30 +969,18 @@ impl ZipFileData { pub(crate) fn block(&self) -> ZipResult { let is_zip_64 = self.was_large_file(); - let compressed_size = if is_zip_64 { - spec::ZIP64_BYTES_THR as u32 - } else { - self.compressed_size - .min(spec::ZIP64_BYTES_THR) - .try_into() - .map_err(std::io::Error::other)? - }; - let uncompressed_size = if is_zip_64 { - spec::ZIP64_BYTES_THR as u32 - } else { - self.uncompressed_size - .min(spec::ZIP64_BYTES_THR) - .try_into() - .map_err(std::io::Error::other)? - }; - let offset = if is_zip_64 { - spec::ZIP64_BYTES_THR as u32 - } else { - self.header_start - .min(spec::ZIP64_BYTES_THR) - .try_into() - .map_err(std::io::Error::other)? - }; + let compressed_size = self.compressed_size + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)?; + let uncompressed_size = self.uncompressed_size + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)?; + let offset = self.header_start + .min(spec::ZIP64_BYTES_THR) + .try_into() + .map_err(std::io::Error::other)?; let extra_field_len: u16 = self .extra_field_len() .try_into() From 83f0dccbdc1b005f9619bf267b6ebddd14b2f5c3 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 7 Feb 2026 08:43:23 -0700 Subject: [PATCH 07/32] add some code --- src/types.rs | 92 ++++++++++++++++++++++++++++++++++------------------ src/write.rs | 17 +++++++--- 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/src/types.rs b/src/types.rs index 08f92e089..6d6b5086c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -968,16 +968,18 @@ impl ZipFileData { } pub(crate) fn block(&self) -> ZipResult { - let is_zip_64 = self.was_large_file(); - let compressed_size = self.compressed_size + let compressed_size = self + .compressed_size .min(spec::ZIP64_BYTES_THR) .try_into() .map_err(std::io::Error::other)?; - let uncompressed_size = self.uncompressed_size + let uncompressed_size = self + .uncompressed_size .min(spec::ZIP64_BYTES_THR) .try_into() .map_err(std::io::Error::other)?; - let offset = self.header_start + let offset = self + .header_start .min(spec::ZIP64_BYTES_THR) .try_into() .map_err(std::io::Error::other)?; @@ -1025,15 +1027,6 @@ impl ZipFileData { }) } - pub(crate) fn zip64_extra_field_block(&self) -> Option { - Zip64ExtraFieldBlock::maybe_new( - self.large_file, - self.uncompressed_size, - self.compressed_size, - self.header_start, - ) - } - pub(crate) fn write_data_descriptor( &self, writer: &mut W, @@ -1184,27 +1177,66 @@ pub(crate) struct Zip64ExtraFieldBlock { } impl Zip64ExtraFieldBlock { - pub(crate) fn maybe_new( - large_file: bool, + /// This entry in the Local header MUST include BOTH original and compressed file size fields + pub(crate) fn local_header( uncompressed_size: u64, compressed_size: u64, header_start: u64, ) -> Option { let mut size: u16 = 0; - let uncompressed_size = - if (uncompressed_size != 0 && uncompressed_size >= ZIP64_BYTES_THR) || large_file { - size += mem::size_of::() as u16; - Some(uncompressed_size) - } else { - None - }; - let compressed_size = - if (compressed_size != 0 && compressed_size >= ZIP64_BYTES_THR) || large_file { - size += mem::size_of::() as u16; - Some(compressed_size) - } else { - None - }; + let should_add_size = + uncompressed_size >= ZIP64_BYTES_THR || compressed_size >= ZIP64_BYTES_THR; + let uncompressed_size = if should_add_size { + size += mem::size_of::() as u16; + Some(uncompressed_size) + } else { + None + }; + let compressed_size = if should_add_size { + size += mem::size_of::() as u16; + Some(compressed_size) + } else { + None + }; + let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(header_start) + } else { + None + }; + // TODO: (unsopported for now) + // Disk Start Number 4 bytes Number of the disk on which this file starts + if size == 0 { + return None; + } + + Some(Zip64ExtraFieldBlock { + magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG, + size, + uncompressed_size, + compressed_size, + header_start, + }) + } + + pub(crate) fn central_header( + uncompressed_size: u64, + compressed_size: u64, + header_start: u64, + ) -> Option { + let mut size: u16 = 0; + let uncompressed_size = if uncompressed_size != 0 && uncompressed_size >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(uncompressed_size) + } else { + None + }; + let compressed_size = if compressed_size != 0 && compressed_size >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(compressed_size) + } else { + None + }; let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { size += mem::size_of::() as u16; Some(header_start) @@ -1225,9 +1257,7 @@ impl Zip64ExtraFieldBlock { header_start, }) } -} -impl Zip64ExtraFieldBlock { pub fn full_size(&self) -> usize { assert!(self.size > 0); self.size as usize + mem::size_of::() + mem::size_of::() diff --git a/src/write.rs b/src/write.rs index 487ab761f..c9b42529c 100644 --- a/src/write.rs +++ b/src/write.rs @@ -986,9 +986,7 @@ impl ZipWriter { None => vec![], }; let central_extra_data = options.extended_options.central_extra_data(); - if let Some(zip64_block) = - Zip64ExtraFieldBlock::maybe_new(options.large_file, 0, 0, header_start) - { + if let Some(zip64_block) = Zip64ExtraFieldBlock::local_header(0, 0, header_start) { let mut new_extra_data = zip64_block.serialize().into_vec(); new_extra_data.append(&mut extra_data); extra_data = new_extra_data; @@ -2232,7 +2230,11 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) Vec::new() }; let central_len = file.central_extra_field_len(); - let zip64_extra_field_block = file.zip64_extra_field_block(); + let zip64_extra_field_block = Zip64ExtraFieldBlock::central_header( + file.uncompressed_size, + file.compressed_size, + file.header_start, + ); let zip64_block_len = if let Some(zip64) = zip64_extra_field_block { zip64.full_size() } else { @@ -2287,7 +2289,12 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, ) -> ZipResult<()> { - let block = file.zip64_extra_field_block().ok_or(invalid!( + let block = Zip64ExtraFieldBlock::central_header( + file.uncompressed_size, + file.compressed_size, + file.header_start, + ) + .ok_or(invalid!( "Attempted to update a nonexistent ZIP64 extra field" ))?; From ea8e004db0072731da18fee96cb585512246f7ce Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 7 Feb 2026 08:54:51 -0700 Subject: [PATCH 08/32] typos --- src/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.rs b/src/types.rs index 6d6b5086c..1e77413ac 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1204,7 +1204,7 @@ impl Zip64ExtraFieldBlock { } else { None }; - // TODO: (unsopported for now) + // TODO: (unsupported for now) // Disk Start Number 4 bytes Number of the disk on which this file starts if size == 0 { return None; @@ -1243,7 +1243,7 @@ impl Zip64ExtraFieldBlock { } else { None }; - // TODO: (unsopported for now) + // TODO: (unsupported for now) // Disk Start Number 4 bytes Number of the disk on which this file starts if size == 0 { return None; From 760beb6a37380a01c8234033f7a16b7e08e07c54 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 7 Feb 2026 09:58:04 -0700 Subject: [PATCH 09/32] comment for now --- src/types.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/types.rs b/src/types.rs index 1e77413ac..23adcf981 100644 --- a/src/types.rs +++ b/src/types.rs @@ -953,19 +953,19 @@ impl ZipFileData { }) } - pub(crate) fn was_large_file(&self) -> bool { - // if self.compressed_size > spec::ZIP64_BYTES_THR { - // return true; - // } - // if self.uncompressed_size > spec::ZIP64_BYTES_THR { - // return true; - // } - if self.header_start >= spec::ZIP64_BYTES_THR { - return true; - } - // TODO: Also disk number (unsupported for now) - false - } + // pub(crate) fn was_large_file(&self) -> bool { + // // if self.compressed_size > spec::ZIP64_BYTES_THR { + // // return true; + // // } + // // if self.uncompressed_size > spec::ZIP64_BYTES_THR { + // // return true; + // // } + // if self.header_start >= spec::ZIP64_BYTES_THR { + // return true; + // } + // // TODO: Also disk number (unsupported for now) + // false + // } pub(crate) fn block(&self) -> ZipResult { let compressed_size = self From 4f1662c6b87e58c3f287be8bb8c369c28835fb8f Mon Sep 17 00:00:00 2001 From: "amazon-q-developer[bot]" <208079219+amazon-q-developer[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:09:00 +0000 Subject: [PATCH 10/32] [skip ci] fix: correct ZIP64 local header update and remove debug statements - Fix update_local_zip64_extra_field to use local_header instead of central_header - Remove debug println statements from Zip64ExtraFieldBlock::serialize - Remove debug println from update_local_zip64_extra_field This fixes test failures with "assertion failed: file_end >= self.stats.start" by ensuring the local header ZIP64 extra field is created correctly. The local header must include both compressed and uncompressed sizes when either exceeds the ZIP64 threshold, whereas the central header only includes fields that are actually needed. --- src/types.rs | 3 --- src/write.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/types.rs b/src/types.rs index 2213679ad..7275ea80a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1259,7 +1259,6 @@ impl Zip64ExtraFieldBlock { let full_size = self.full_size(); - println!("{self:?}"); let mut ret = Vec::with_capacity(full_size); ret.extend(magic.to_le_bytes()); ret.extend(u16::to_le_bytes(size)); @@ -1276,8 +1275,6 @@ impl Zip64ExtraFieldBlock { debug_assert_eq!(ret.len(), full_size); println!("{ret:?} = {}, {}", ret.len(), ret.len() - 4); - ret.into_boxed_slice() - } } #[derive(Copy, Clone, Debug)] diff --git a/src/write.rs b/src/write.rs index c9de0592c..15207b867 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2326,7 +2326,7 @@ fn strip_alignment_extra_field(extra_field: &[u8]) -> Vec { fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, -) -> ZipResult<()> { + let block = Zip64ExtraFieldBlock::local_header( let block = Zip64ExtraFieldBlock::central_header( file.uncompressed_size, file.compressed_size, From 7c9a4d8d8c758bf73c465f7b3ef375dbfdfc9cb0 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:10:54 -0800 Subject: [PATCH 11/32] Update write_dir.rs: use debug formatting when logging path Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> --- examples/write_dir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/write_dir.rs b/examples/write_dir.rs index 3ed688bd1..c84940317 100644 --- a/examples/write_dir.rs +++ b/examples/write_dir.rs @@ -120,7 +120,7 @@ fn zip_dir( // Some unzip tools unzip files with directory paths correctly, some do not! if path.is_file() { println!( - "adding file '{}' as '{}' ...", + "adding file {:?} as {:?} ...", path.display(), path_stripped.display() ); From 98eb52f48947b2a1e92d49eabbaed545457ffc60 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:11:15 -0800 Subject: [PATCH 12/32] Update write_dir.rs Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> --- examples/write_dir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/write_dir.rs b/examples/write_dir.rs index c84940317..76a5b3295 100644 --- a/examples/write_dir.rs +++ b/examples/write_dir.rs @@ -114,7 +114,7 @@ fn zip_dir( let path_as_string = path_stripped .to_str() .map(str::to_owned) - .ok_or_else(|| format!("'{}' is a Non UTF-8 Path", path_stripped.display()))?; + .ok_or_else(|| format!("{:?} is a Non UTF-8 Path", path_stripped.display()))?; // Write file or directory explicitly // Some unzip tools unzip files with directory paths correctly, some do not! From 5e4df5ab38175c001945e90777d38f43770e50a9 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:47:24 -0800 Subject: [PATCH 13/32] Fix: missing closing curly --- src/types.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types.rs b/src/types.rs index 7275ea80a..df8b1e4a3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1275,6 +1275,8 @@ impl Zip64ExtraFieldBlock { debug_assert_eq!(ret.len(), full_size); println!("{ret:?} = {}, {}", ret.len(), ret.len() - 4); + ret.into_boxed_slice() + } } #[derive(Copy, Clone, Debug)] From a17854d047bf37770c99ccc3efbd7e0a9ba16449 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:47:48 -0800 Subject: [PATCH 14/32] Remove debug logging --- src/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index df8b1e4a3..cc5d36733 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1274,7 +1274,6 @@ impl Zip64ExtraFieldBlock { } debug_assert_eq!(ret.len(), full_size); - println!("{ret:?} = {}, {}", ret.len(), ret.len() - 4); ret.into_boxed_slice() } } From d33488f4480492c00e13aca9256cc3879630acc6 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:02:01 -0800 Subject: [PATCH 15/32] Fix: unclosed delimiter due to duplicated line --- src/write.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/write.rs b/src/write.rs index a3591e7b5..ca4aeec64 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2339,7 +2339,6 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, let block = Zip64ExtraFieldBlock::local_header( - let block = Zip64ExtraFieldBlock::central_header( file.uncompressed_size, file.compressed_size, file.header_start, From d599cfe245631531cdb446eea048a26f43e3c5c4 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:02:48 -0800 Subject: [PATCH 16/32] Fix: accidentally deleted line at start of update_local_zip64_extra_field method --- src/write.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/write.rs b/src/write.rs index ca4aeec64..db90e42f9 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2338,6 +2338,7 @@ fn strip_alignment_extra_field(extra_field: &[u8]) -> Vec { fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, +) -> ZipResult<()> { let block = Zip64ExtraFieldBlock::local_header( file.uncompressed_size, file.compressed_size, From 5857e8950f481e36a53b696534d657bc0ffb3ee6 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 15 Feb 2026 16:32:45 -0700 Subject: [PATCH 17/32] add method --- src/extra_fields/mod.rs | 6 ++++++ src/types.rs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/extra_fields/mod.rs b/src/extra_fields/mod.rs index 1ece406fd..f7eea3389 100644 --- a/src/extra_fields/mod.rs +++ b/src/extra_fields/mod.rs @@ -56,6 +56,12 @@ pub(crate) enum UsedExtraField { DataStreamAlignment = 0xa11e, } +impl From for u16 { + fn from(value: UsedExtraField) -> Self { + value as u16 + } +} + macro_rules! extra_field_match { ($x:expr, $( $variant:path ),+ $(,)?) => { match $x { diff --git a/src/types.rs b/src/types.rs index c60f3a05c..ddc31f84e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1260,7 +1260,9 @@ impl Zip64ExtraFieldBlock { }; // TODO: (unsupported for now) // Disk Start Number 4 bytes Number of the disk on which this file starts + if size == 0 { + // no info added, return early return None; } @@ -1299,7 +1301,9 @@ impl Zip64ExtraFieldBlock { }; // TODO: (unsupported for now) // Disk Start Number 4 bytes Number of the disk on which this file starts + if size == 0 { + // no info added, return early return None; } From b923f643d29bb85d1998782e096e0cb2232361a9 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 15 Feb 2026 16:33:09 -0700 Subject: [PATCH 18/32] add test --- tests/zip64_large.rs | 122 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index 2f17bbc84..bfe906839 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -53,7 +53,13 @@ // 22c400260 00 00 50 4b 05 06 00 00 00 00 03 00 03 00 27 01 |..PK..........'.| // 22c400270 00 00 ff ff ff ff 00 00 |........| // 22c400278 -use std::io::{self, Read, Seek, SeekFrom}; +use std::{ + fs::File, + io::{self, Cursor, Read, Seek, SeekFrom}, + path::Path, +}; + +use zip::write::SimpleFileOptions; const BLOCK1_LENGTH: u64 = 0x60; const BLOCK1: [u8; BLOCK1_LENGTH as usize] = [ @@ -209,3 +215,117 @@ fn zip64_large() { }; } } + +#[test] +fn test_zip64_check_extra_field() { + let path = Path::new("bigfile.bin"); + let bigfile = File::create(path).expect("Failed to create a big file"); + + // 1024 MiB = 1024 * 1024 * 1024 bytes + bigfile + .set_len(1024 * 1024 * 1024) + .expect("Failed to set file length of the big file"); + + let mut archive_buffer = Vec::new(); + let res = zip64_check_extra_field(path, &mut archive_buffer); + std::fs::remove_file(path).expect("Failed to remove the big file"); + + assert_eq!(res.unwrap(), ()); + assert_eq!(archive_buffer.len(), 5368709856); + + let mut read_archive = + zip::ZipArchive::new(Cursor::new(&archive_buffer)).expect("Failed to read the archive"); + + assert_eq!(read_archive.len(), 4 + 1 + 1); // the archive should contain 4 files + 1 directory + 1 file in the directory + { + let dir = read_archive.by_name("dir/").unwrap(); + assert_eq!(dir.compressed_size(), 0); + assert_eq!(dir.size(), 0); + let header_start = 4294967452; + assert_eq!(dir.header_start(), header_start); + assert_eq!(dir.central_header_start(), 5368709599); + let start_file = dir.header_start() as usize; + let range = (start_file)..(start_file + 70); // take a bunch of bytes from the local file header of the directory entry, which should contain the zip64 extra field + let raw_access = archive_buffer.get(range).unwrap(); + eprintln!("RAW ACCESS: {:x?}", raw_access); + assert_eq!(raw_access[0..4], [0x50, 0x4b, 0x03, 0x04]); // local file header signature + assert_eq!(raw_access[4..6], [0x2d, 0x00]); // version needed to extract + assert_eq!(raw_access[6..8], [0x00, 0x00]); // general purpose bit flag + assert_eq!(raw_access[8..10], [0x00, 0x00]); // compression method + // assert_eq!(raw_access[10..12], [0x00, 0x00]); // last mod file time + // assert_eq!(raw_access[12..14], [0x00, 0x00]); // last mod file date + assert_eq!(raw_access[14..18], [0x00, 0x00, 0x00, 0x00]); // crc-32 + assert_eq!(raw_access[18..22], [0x00, 0x00, 0x00, 0x00]); // compressed size - IMPORTANT + assert_eq!(raw_access[22..26], [0x00, 0x00, 0x00, 0x00]); // uncompressed size - IMPORTANT + assert_eq!(raw_access[26..28], [0x04, 0x00]); // file name length + assert_eq!(raw_access[28..30], [0x0c, 0x00]); // extra field length + assert_eq!(raw_access[30..34], *b"dir/"); // file name + assert_eq!(raw_access[34..36], [0x01, 0x00]); // zip64 extra field header id + assert_eq!(raw_access[36..38], [0x08, 0x00]); // zip64 extra field data size (should be 0 for a directory entry, since + assert_eq!( + raw_access[38..46], + [0x9c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] + ); // zip64 extra field - IMPORTANT + } + { + let bigfile_archive = read_archive.by_name("dir/bigfile.bin").unwrap(); + assert_eq!(bigfile_archive.compressed_size(), 1024 * 1024 * 1024); + assert_eq!(bigfile_archive.size(), 1024 * 1024 * 1024); + let header_start = 4294967498; + assert_eq!(bigfile_archive.header_start(), header_start); + assert_eq!(bigfile_archive.central_header_start(), 5368709673); + let start_file = bigfile_archive.header_start() as usize; + let range = (start_file)..(start_file + 70); // take a bunch of bytes from the local file header of the directory entry, which should contain the zip64 extra field + let raw_access = archive_buffer.get(range).unwrap(); + eprintln!("RAW ACCESS: {:x?}", raw_access); + assert_eq!(raw_access[0..4], [0x50, 0x4b, 0x03, 0x04]); // local file header signature + assert_eq!(raw_access[4..6], [0x2d, 0x00]); // version needed to extract + assert_eq!(raw_access[6..8], [0x00, 0x00]); // general purpose bit flag + assert_eq!(raw_access[8..10], [0x00, 0x00]); // compression method + // assert_eq!(raw_access[10..12], [0x00, 0x00]); // last mod file time + // assert_eq!(raw_access[12..14], [0x00, 0x00]); // last mod file date + assert_eq!(raw_access[14..18], [176, 194, 100, 91]); // crc-32 + assert_eq!(raw_access[18..22], [0xff, 0xff, 0xff, 0xff]); // compressed size + assert_eq!(raw_access[22..26], [0xff, 0xff, 0xff, 0xff]); // uncompressed size + assert_eq!(raw_access[26..28], [0x0f, 0x00]); // file name length + assert_eq!(raw_access[28..30], [0x0c, 0x00]); // extra field length + assert_eq!(raw_access[30..45], *b"dir/bigfile.bin"); // file name + assert_eq!(raw_access[45..47], [0x01, 0x00]); // zip64 extra field header id + assert_eq!(raw_access[47..49], [0x08, 0x00]); // zip64 extra field data size + let offset_bytes = [0xca, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]; + assert_eq!(raw_access[49..57], offset_bytes); // zip64 extra field data Relative Header Offset + let offset = u64::from_le_bytes(offset_bytes); + assert_eq!(offset, header_start); // the offset in the zip64 extra field should match the header start of the file data + } + + panic!( + "This test is expected to fail because the zip64 extra field for the central directory header is not being written correctly, which causes the central directory to be misread and the archive to be considered malformed" + ); +} + +fn zip64_check_extra_field( + path: &Path, + archive_buffer: &mut Vec, +) -> Result<(), Box> { + let mut bigfile = File::open(path)?; + + let mut archive = zip::ZipWriter::new(Cursor::new(archive_buffer)); + let options = SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored) + .unix_permissions(0o755); + + // add 4GiB of file data to the archive, which should trigger the zip64 extra field + for i in 0..4 { + bigfile.seek(SeekFrom::Start(0))?; + let filename = format!("file{}.bin", i + 1); + archive.start_file(filename, options)?; + std::io::copy(&mut bigfile, &mut archive)?; + } + // now add a directory entry, which SHOULD trigger the zip64 extra field for the central directory header + archive.add_directory("dir/", options)?; + archive.start_file("dir/bigfile.bin", options)?; + bigfile.seek(SeekFrom::Start(0))?; + std::io::copy(&mut bigfile, &mut archive)?; + archive.finish()?; + Ok(()) +} From 0b749dc9f69953a790c4795d0cf3cbfba64b5e6c Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 21 Feb 2026 09:25:15 -0700 Subject: [PATCH 19/32] rewrite zip64 extended --- .../zip64_extended_information.rs | 150 ++++++++++++++++++ src/spec.rs | 38 ----- src/types.rs | 135 +--------------- src/write.rs | 11 +- 4 files changed, 157 insertions(+), 177 deletions(-) create mode 100644 src/extra_fields/zip64_extended_information.rs diff --git a/src/extra_fields/zip64_extended_information.rs b/src/extra_fields/zip64_extended_information.rs new file mode 100644 index 000000000..8c6af0e97 --- /dev/null +++ b/src/extra_fields/zip64_extended_information.rs @@ -0,0 +1,150 @@ +//! 4.5.3 -Zip64 Extended Information Extra Field (0x0001) +//! +//! | Value | Size | Description | +//! | ---------------------- | ------- | -------------------------------------------- | +//! | `0x0001` | 2 bytes | Tag for this "extra" block type | +//! | Size | 2 bytes | Size of this "extra" block | +//! | Original Size | 8 bytes | Original uncompressed file size | +//! | Compressed Size | 8 bytes | Size of compressed data | +//! | Relative Header Offset | 8 bytes | Offset of local header record | +//! | Disk Start Number | 4 bytes | Number of the disk on which this file starts | +//! + +use core::mem; + +use crate::{ZIP64_BYTES_THR, extra_fields::UsedExtraField}; + +/// Zip64 extended information extra field +#[derive(Copy, Clone, Debug)] +pub struct Zip64ExtendedInformation { + magic: UsedExtraField, + size: u16, + uncompressed_size: Option, + compressed_size: Option, + header_start: Option, + // Not used field + // disk_start: Option +} + +impl Zip64ExtendedInformation { + const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo; + /// This entry in the Local header MUST include BOTH original and compressed file size fields + pub(crate) fn local_header( + uncompressed_size: u64, + compressed_size: u64, + header_start: u64, + ) -> Option { + let mut size: u16 = 0; + let should_add_size = + uncompressed_size >= ZIP64_BYTES_THR || compressed_size >= ZIP64_BYTES_THR; + let uncompressed_size = if should_add_size { + size += mem::size_of::() as u16; + Some(uncompressed_size) + } else { + None + }; + let compressed_size = if should_add_size { + size += mem::size_of::() as u16; + Some(compressed_size) + } else { + None + }; + let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(header_start) + } else { + None + }; + // TODO: (unsupported for now) + // Disk Start Number 4 bytes Number of the disk on which this file starts + + if size == 0 { + // no info added, return early + return None; + } + + Some(Self { + magic: Self::MAGIC, + size, + uncompressed_size, + compressed_size, + header_start, + }) + } + + pub(crate) fn central_header( + uncompressed_size: u64, + compressed_size: u64, + header_start: u64, + ) -> Option { + let mut size: u16 = 0; + let uncompressed_size = if uncompressed_size != 0 && uncompressed_size >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(uncompressed_size) + } else { + None + }; + let compressed_size = if compressed_size != 0 && compressed_size >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(compressed_size) + } else { + None + }; + let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { + size += mem::size_of::() as u16; + Some(header_start) + } else { + None + }; + // TODO: (unsupported for now) + // Disk Start Number 4 bytes Number of the disk on which this file starts + + if size == 0 { + // no info added, return early + return None; + } + + Some(Self { + magic: Self::MAGIC, + size, + uncompressed_size, + compressed_size, + header_start, + }) + } + + /// Get the full size of the block + pub(crate) fn full_size(&self) -> usize { + self.size as usize + mem::size_of::() + mem::size_of::() + } + + /// Serialize the block + pub fn serialize(self) -> Box<[u8]> { + let Self { + magic, + size, + uncompressed_size, + compressed_size, + header_start, + } = self; + + let full_size = self.full_size(); + + let mut ret = Vec::with_capacity(full_size); + ret.extend(magic.to_le_bytes()); + ret.extend(u16::to_le_bytes(size)); + + if let Some(uncompressed_size) = uncompressed_size { + ret.extend(u64::to_le_bytes(uncompressed_size)); + } + if let Some(compressed_size) = compressed_size { + ret.extend(u64::to_le_bytes(compressed_size)); + } + if let Some(header_start) = header_start { + ret.extend(u64::to_le_bytes(header_start)); + } + debug_assert_eq!(ret.len(), full_size); + + ret.into_boxed_slice() + } +} diff --git a/src/spec.rs b/src/spec.rs index e53bba8d6..c50eda287 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,6 +1,5 @@ #![macro_use] -use crate::extra_fields::UsedExtraField; use crate::read::ArchiveOffset; use crate::read::magic_finder::{Backwards, Forward, MagicFinder, OptimisticMagicFinder}; use crate::result::{ZipError, ZipResult, invalid}; @@ -93,43 +92,6 @@ pub(crate) enum ZipFlags { Reserved = 0b1000_0000_0000_0000, } -/// Similar to [`Magic`], but used for extra field tags as per section 4.5.3 of APPNOTE.TXT. -#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub(crate) struct ExtraFieldMagic(u16); - -/* TODO: maybe try to use this for parsing extra fields as well as writing them? */ -#[allow(dead_code)] -impl ExtraFieldMagic { - pub const fn literal(x: u16) -> Self { - Self(x) - } - - #[inline(always)] - pub const fn from_le_bytes(bytes: [u8; 2]) -> Self { - Self(u16::from_le_bytes(bytes)) - } - - #[inline(always)] - pub const fn to_le_bytes(self) -> [u8; 2] { - self.0.to_le_bytes() - } - - #[allow(clippy::wrong_self_convention)] - #[inline(always)] - pub fn from_le(self) -> Self { - Self(u16::from_le(self.0)) - } - - #[allow(clippy::wrong_self_convention)] - #[inline(always)] - pub fn to_le(self) -> Self { - Self(u16::to_le(self.0)) - } - - pub const ZIP64_EXTRA_FIELD_TAG: Self = Self::literal(UsedExtraField::Zip64ExtendedInfo as u16); -} - /// The file size at which a ZIP64 record becomes necessary. /// /// If a file larger than this threshold attempts to be written, compressed or uncompressed, and diff --git a/src/types.rs b/src/types.rs index d47e23003..93684119b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,7 +6,6 @@ use crate::spec::{self, FixedSizeBlock, Magic, Pod, ZipFlags}; use crate::write::FileOptionExtension; use crate::zipcrypto::EncryptWith; use core::fmt::{self, Debug, Formatter}; -use core::mem; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; @@ -18,12 +17,12 @@ pub(crate) mod ffi { pub const S_IFLNK: u32 = 0o0120000; } +use crate::CompressionMethod; use crate::extra_fields::{ExtraField, UsedExtraField}; use crate::read::find_data_start; use crate::result::DateTimeRangeError; use crate::spec::is_dir; use crate::types::ffi::S_IFDIR; -use crate::{CompressionMethod, ZIP64_BYTES_THR}; use std::io::{Read, Seek}; pub(crate) struct ZipRawValues { @@ -1231,138 +1230,6 @@ impl FixedSizeBlock for ZipLocalEntryBlock { ]; } -#[derive(Copy, Clone, Debug)] -pub(crate) struct Zip64ExtraFieldBlock { - magic: spec::ExtraFieldMagic, - size: u16, - uncompressed_size: Option, - compressed_size: Option, - header_start: Option, - // Excluded fields: - // u32: disk start number -} - -impl Zip64ExtraFieldBlock { - /// This entry in the Local header MUST include BOTH original and compressed file size fields - pub(crate) fn local_header( - uncompressed_size: u64, - compressed_size: u64, - header_start: u64, - ) -> Option { - let mut size: u16 = 0; - let should_add_size = - uncompressed_size >= ZIP64_BYTES_THR || compressed_size >= ZIP64_BYTES_THR; - let uncompressed_size = if should_add_size { - size += mem::size_of::() as u16; - Some(uncompressed_size) - } else { - None - }; - let compressed_size = if should_add_size { - size += mem::size_of::() as u16; - Some(compressed_size) - } else { - None - }; - let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { - size += mem::size_of::() as u16; - Some(header_start) - } else { - None - }; - // TODO: (unsupported for now) - // Disk Start Number 4 bytes Number of the disk on which this file starts - - if size == 0 { - // no info added, return early - return None; - } - - Some(Zip64ExtraFieldBlock { - magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG, - size, - uncompressed_size, - compressed_size, - header_start, - }) - } - - pub(crate) fn central_header( - uncompressed_size: u64, - compressed_size: u64, - header_start: u64, - ) -> Option { - let mut size: u16 = 0; - let uncompressed_size = if uncompressed_size != 0 && uncompressed_size >= ZIP64_BYTES_THR { - size += mem::size_of::() as u16; - Some(uncompressed_size) - } else { - None - }; - let compressed_size = if compressed_size != 0 && compressed_size >= ZIP64_BYTES_THR { - size += mem::size_of::() as u16; - Some(compressed_size) - } else { - None - }; - let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { - size += mem::size_of::() as u16; - Some(header_start) - } else { - None - }; - // TODO: (unsupported for now) - // Disk Start Number 4 bytes Number of the disk on which this file starts - - if size == 0 { - // no info added, return early - return None; - } - - Some(Zip64ExtraFieldBlock { - magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG, - size, - uncompressed_size, - compressed_size, - header_start, - }) - } - - pub fn full_size(&self) -> usize { - assert!(self.size > 0); - self.size as usize + mem::size_of::() + mem::size_of::() - } - - pub fn serialize(self) -> Box<[u8]> { - let Self { - magic, - size, - uncompressed_size, - compressed_size, - header_start, - } = self; - - let full_size = self.full_size(); - - let mut ret = Vec::with_capacity(full_size); - ret.extend(magic.to_le_bytes()); - ret.extend(u16::to_le_bytes(size)); - - if let Some(uncompressed_size) = uncompressed_size { - ret.extend(u64::to_le_bytes(uncompressed_size)); - } - if let Some(compressed_size) = compressed_size { - ret.extend(u64::to_le_bytes(compressed_size)); - } - if let Some(header_start) = header_start { - ret.extend(u64::to_le_bytes(header_start)); - } - debug_assert_eq!(ret.len(), full_size); - - ret.into_boxed_slice() - } -} - #[derive(Copy, Clone, Debug)] #[repr(packed, C)] pub(crate) struct ZipDataDescriptorBlock { diff --git a/src/write.rs b/src/write.rs index 06efa660b..0ad136e0a 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2,13 +2,14 @@ use crate::compression::CompressionMethod; 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::types::ffi::S_IFLNK; use crate::types::{ - AesExtraField, AesVendorVersion, DateTime, MIN_VERSION, System, Zip64ExtraFieldBlock, - ZipFileData, ZipLocalEntryBlock, ZipRawValues, ffi, + AesExtraField, AesVendorVersion, DateTime, MIN_VERSION, System, ZipFileData, + ZipLocalEntryBlock, ZipRawValues, ffi, }; use core::default::Default; use core::fmt::{Debug, Formatter}; @@ -1125,7 +1126,7 @@ impl ZipWriter { None => vec![], }; let central_extra_data = options.extended_options.central_extra_data(); - if let Some(zip64_block) = Zip64ExtraFieldBlock::local_header(0, 0, header_start) { + if let Some(zip64_block) = Zip64ExtendedInformation::local_header(0, 0, header_start) { let mut new_extra_data = zip64_block.serialize().into_vec(); new_extra_data.append(&mut extra_data); extra_data = new_extra_data; @@ -2359,7 +2360,7 @@ fn write_central_directory_header(writer: &mut T, file: &ZipFileData) Vec::new() }; let central_len = file.central_extra_field_len(); - let zip64_extra_field_block = Zip64ExtraFieldBlock::central_header( + let zip64_extra_field_block = Zip64ExtendedInformation::central_header( file.uncompressed_size, file.compressed_size, file.header_start, @@ -2418,7 +2419,7 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, ) -> ZipResult<()> { - let block = Zip64ExtraFieldBlock::local_header( + let block = Zip64ExtendedInformation::local_header( file.uncompressed_size, file.compressed_size, file.header_start, From 4876dede8d01b023e0d2857a1ebea7a3bf21fcfa Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 21 Feb 2026 09:25:41 -0700 Subject: [PATCH 20/32] change mod.rs --- src/extra_fields/mod.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/extra_fields/mod.rs b/src/extra_fields/mod.rs index f7eea3389..fac0b5924 100644 --- a/src/extra_fields/mod.rs +++ b/src/extra_fields/mod.rs @@ -18,11 +18,13 @@ impl ExtraFieldVersion for CentralHeaderVersion {} mod extended_timestamp; mod ntfs; +mod zip64_extended_information; mod zipinfo_utf8; // re-export -pub use extended_timestamp::*; +pub use extended_timestamp::ExtendedTimestamp; pub use ntfs::Ntfs; +pub use zip64_extended_information::Zip64ExtendedInformation; pub use zipinfo_utf8::UnicodeExtraField; /// contains one extra field @@ -33,6 +35,9 @@ pub enum ExtraField { /// extended timestamp, as described in ExtendedTimestamp(ExtendedTimestamp), + + /// Zip64 extended information + Zip64ExtendedInfo(Zip64ExtendedInformation), } /// Extra field used in this crate @@ -56,6 +61,13 @@ pub(crate) enum UsedExtraField { DataStreamAlignment = 0xa11e, } +impl UsedExtraField { + pub const fn to_le_bytes(self) -> [u8; 2] { + let field_u16 = self as u16; + field_u16.to_le_bytes() + } +} + impl From for u16 { fn from(value: UsedExtraField) -> Self { value as u16 From 33bb5224650cfd3fcf05f11ff6ea5c7c1f698ea1 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 21 Feb 2026 09:33:04 -0700 Subject: [PATCH 21/32] remove from enum for now --- src/extra_fields/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/extra_fields/mod.rs b/src/extra_fields/mod.rs index fac0b5924..3299c7b3c 100644 --- a/src/extra_fields/mod.rs +++ b/src/extra_fields/mod.rs @@ -35,9 +35,6 @@ pub enum ExtraField { /// extended timestamp, as described in ExtendedTimestamp(ExtendedTimestamp), - - /// Zip64 extended information - Zip64ExtendedInfo(Zip64ExtendedInformation), } /// Extra field used in this crate From 06eb6fbd0b88ea050c8e8c80041ad7cb7644b7a6 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 16:17:21 -0700 Subject: [PATCH 22/32] fix zip64 --- .../zip64_extended_information.rs | 82 +++++---- src/write.rs | 11 +- tests/zip64_large.rs | 158 ++++++++++++------ 3 files changed, 161 insertions(+), 90 deletions(-) diff --git a/src/extra_fields/zip64_extended_information.rs b/src/extra_fields/zip64_extended_information.rs index 8c6af0e97..fa96e187e 100644 --- a/src/extra_fields/zip64_extended_information.rs +++ b/src/extra_fields/zip64_extended_information.rs @@ -17,6 +17,8 @@ use crate::{ZIP64_BYTES_THR, extra_fields::UsedExtraField}; /// Zip64 extended information extra field #[derive(Copy, Clone, Debug)] pub struct Zip64ExtendedInformation { + /// The local header does not contains any `header_start` + _is_local_header: bool, magic: UsedExtraField, size: u16, uncompressed_size: Option, @@ -28,32 +30,30 @@ pub struct Zip64ExtendedInformation { impl Zip64ExtendedInformation { const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo; + + pub(crate) fn from_new_entry(is_large_file: bool) -> Option { + Self::local_header(is_large_file, 0, 0) + } + /// This entry in the Local header MUST include BOTH original and compressed file size fields pub(crate) fn local_header( - uncompressed_size: u64, - compressed_size: u64, - header_start: u64, + is_large_file: bool, + mut uncompressed_size: u64, + mut compressed_size: u64, ) -> Option { + if is_large_file { + uncompressed_size = u64::MAX; + compressed_size = u64::MAX; + } let mut size: u16 = 0; let should_add_size = uncompressed_size >= ZIP64_BYTES_THR || compressed_size >= ZIP64_BYTES_THR; - let uncompressed_size = if should_add_size { - size += mem::size_of::() as u16; - Some(uncompressed_size) - } else { - None - }; - let compressed_size = if should_add_size { + let (uncompressed_size, compressed_size) = if should_add_size { size += mem::size_of::() as u16; - Some(compressed_size) - } else { - None - }; - let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR { size += mem::size_of::() as u16; - Some(header_start) + (Some(uncompressed_size), Some(compressed_size)) } else { - None + (None, None) }; // TODO: (unsupported for now) // Disk Start Number 4 bytes Number of the disk on which this file starts @@ -64,11 +64,12 @@ impl Zip64ExtendedInformation { } Some(Self { + _is_local_header: true, magic: Self::MAGIC, size, uncompressed_size, compressed_size, - header_start, + header_start: None, }) } @@ -105,6 +106,7 @@ impl Zip64ExtendedInformation { } Some(Self { + _is_local_header: false, magic: Self::MAGIC, size, uncompressed_size, @@ -121,6 +123,7 @@ impl Zip64ExtendedInformation { /// Serialize the block pub fn serialize(self) -> Box<[u8]> { let Self { + _is_local_header, magic, size, uncompressed_size, @@ -129,22 +132,37 @@ impl Zip64ExtendedInformation { } = self; let full_size = self.full_size(); + if _is_local_header { + // the local header does not contains the header start + if let (Some(uncompressed_size), Some(compressed_size)) = + (self.compressed_size, self.compressed_size) + { + let mut ret = Vec::with_capacity(full_size); + ret.extend(magic.to_le_bytes()); + ret.extend(size.to_le_bytes()); + ret.extend(u64::to_le_bytes(uncompressed_size)); + ret.extend(u64::to_le_bytes(compressed_size)); + return ret.into_boxed_slice(); + } + // this should be unreachable + Box::new([]) + } else { + let mut ret = Vec::with_capacity(full_size); + ret.extend(magic.to_le_bytes()); + ret.extend(u16::to_le_bytes(size)); - let mut ret = Vec::with_capacity(full_size); - ret.extend(magic.to_le_bytes()); - ret.extend(u16::to_le_bytes(size)); + if let Some(uncompressed_size) = uncompressed_size { + ret.extend(u64::to_le_bytes(uncompressed_size)); + } + if let Some(compressed_size) = compressed_size { + ret.extend(u64::to_le_bytes(compressed_size)); + } + if let Some(header_start) = header_start { + ret.extend(u64::to_le_bytes(header_start)); + } + debug_assert_eq!(ret.len(), full_size); - if let Some(uncompressed_size) = uncompressed_size { - ret.extend(u64::to_le_bytes(uncompressed_size)); + ret.into_boxed_slice() } - if let Some(compressed_size) = compressed_size { - ret.extend(u64::to_le_bytes(compressed_size)); - } - if let Some(header_start) = header_start { - ret.extend(u64::to_le_bytes(header_start)); - } - debug_assert_eq!(ret.len(), full_size); - - ret.into_boxed_slice() } } diff --git a/src/write.rs b/src/write.rs index 0ad136e0a..c6a1200b2 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1114,19 +1114,12 @@ impl ZipWriter { uncompressed_size: 0, }); - // Check if we're close to the 4GB boundary and force ZIP64 if needed - // This ensures we properly handle appending to files close to 4GB - if header_start > spec::ZIP64_BYTES_THR { - // Files that start on or past the 4GiB boundary are always ZIP64 - options.large_file = true; - } - let mut extra_data = match options.extended_options.extra_data() { Some(data) => data.to_vec(), None => vec![], }; let central_extra_data = options.extended_options.central_extra_data(); - if let Some(zip64_block) = Zip64ExtendedInformation::local_header(0, 0, header_start) { + if let Some(zip64_block) = Zip64ExtendedInformation::from_new_entry(options.large_file) { let mut new_extra_data = zip64_block.serialize().into_vec(); new_extra_data.append(&mut extra_data); extra_data = new_extra_data; @@ -2420,9 +2413,9 @@ fn update_local_zip64_extra_field( file: &mut ZipFileData, ) -> ZipResult<()> { let block = Zip64ExtendedInformation::local_header( + file.large_file, file.uncompressed_size, file.compressed_size, - file.header_start, ) .ok_or(invalid!( "Attempted to update a nonexistent ZIP64 extra field" diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index bfe906839..14a4bb9f9 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -221,9 +221,10 @@ fn test_zip64_check_extra_field() { let path = Path::new("bigfile.bin"); let bigfile = File::create(path).expect("Failed to create a big file"); + let bigfile_size: u32 = 1024 * 1024 * 1024; // 1024 MiB = 1024 * 1024 * 1024 bytes bigfile - .set_len(1024 * 1024 * 1024) + .set_len(bigfile_size as u64) .expect("Failed to set file length of the big file"); let mut archive_buffer = Vec::new(); @@ -231,7 +232,7 @@ fn test_zip64_check_extra_field() { std::fs::remove_file(path).expect("Failed to remove the big file"); assert_eq!(res.unwrap(), ()); - assert_eq!(archive_buffer.len(), 5368709856); + assert_eq!(archive_buffer.len(), 5368709808); let mut read_archive = zip::ZipArchive::new(Cursor::new(&archive_buffer)).expect("Failed to read the archive"); @@ -243,64 +244,123 @@ fn test_zip64_check_extra_field() { assert_eq!(dir.size(), 0); let header_start = 4294967452; assert_eq!(dir.header_start(), header_start); - assert_eq!(dir.central_header_start(), 5368709599); - let start_file = dir.header_start() as usize; - let range = (start_file)..(start_file + 70); // take a bunch of bytes from the local file header of the directory entry, which should contain the zip64 extra field - let raw_access = archive_buffer.get(range).unwrap(); - eprintln!("RAW ACCESS: {:x?}", raw_access); - assert_eq!(raw_access[0..4], [0x50, 0x4b, 0x03, 0x04]); // local file header signature - assert_eq!(raw_access[4..6], [0x2d, 0x00]); // version needed to extract - assert_eq!(raw_access[6..8], [0x00, 0x00]); // general purpose bit flag - assert_eq!(raw_access[8..10], [0x00, 0x00]); // compression method - // assert_eq!(raw_access[10..12], [0x00, 0x00]); // last mod file time - // assert_eq!(raw_access[12..14], [0x00, 0x00]); // last mod file date - assert_eq!(raw_access[14..18], [0x00, 0x00, 0x00, 0x00]); // crc-32 - assert_eq!(raw_access[18..22], [0x00, 0x00, 0x00, 0x00]); // compressed size - IMPORTANT - assert_eq!(raw_access[22..26], [0x00, 0x00, 0x00, 0x00]); // uncompressed size - IMPORTANT - assert_eq!(raw_access[26..28], [0x04, 0x00]); // file name length - assert_eq!(raw_access[28..30], [0x0c, 0x00]); // extra field length - assert_eq!(raw_access[30..34], *b"dir/"); // file name - assert_eq!(raw_access[34..36], [0x01, 0x00]); // zip64 extra field header id - assert_eq!(raw_access[36..38], [0x08, 0x00]); // zip64 extra field data size (should be 0 for a directory entry, since + assert_eq!(dir.central_header_start(), 5368709575); + let central_header_start = dir.central_header_start() as usize; + let central_header_end = central_header_start + 62; + // take a bunch of bytes from the central file header of the directory entry, which should contain the zip64 extra field + let range = central_header_start..central_header_end; + let central_header = archive_buffer.get(range).unwrap(); + assert_eq!(central_header[0..4], [0x50, 0x4b, 0x01, 0x02]); // central file header signature + // assert_eq!(central_header[4..6], [0x14, 0x03]); // version made by + assert_eq!(central_header[6..8], [0x14, 0x00]); // version needed to extract + assert_eq!(central_header[8..10], [0x00, 0x00]); // general purpose bit flag + assert_eq!(central_header[10..12], [0x00, 0x00]); // compression method + // assert_eq!(raw_access[12..14], [0x00, 0x00]); // last mod file time + // assert_eq!(raw_access[14..16], [0x00, 0x00]); // last mod file date + assert_eq!(central_header[16..20], [0x00, 0x00, 0x00, 0x00]); // crc-32 + assert_eq!(central_header[20..24], [0x00, 0x00, 0x00, 0x00]); // compressed size - IMPORTANT + assert_eq!(central_header[24..28], [0x00, 0x00, 0x00, 0x00]); // uncompressed size - IMPORTANT + assert_eq!(central_header[28..30], [0x04, 0x00]); // file name length + // IMPORTANT + assert_eq!(central_header[30..32], [0x0c, 0x00]); // extra field length + assert_eq!(central_header[32..34], [0x00, 0x00]); // file comment length + assert_eq!(central_header[34..36], [0x00, 0x00]); // disk number start + assert_eq!(central_header[36..38], [0x00, 0x00]); // internal file attributes + // assert_eq!(raw_access[38..42], [0x00, 0x00, 0x00, 0x00]); // external file attributes + // IMPORTANT + assert_eq!(central_header[42..46], [0xFF, 0xFF, 0xFF, 0xFF]); // relative offset of local header + assert_eq!(central_header[46..50], *b"dir/"); // file name + assert_eq!(central_header[50..52], [0x01, 0x00]); // zip64 extra field header id + assert_eq!(central_header[52..54], [0x08, 0x00]); // zip64 extra field data size (should be 0 for a directory entry, since + // IMPORTANT assert_eq!( - raw_access[38..46], + central_header[54..], [0x9c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] - ); // zip64 extra field - IMPORTANT + ); // zip64 extra field + assert_eq!(central_header[54..], dir.header_start().to_le_bytes()); + + // now we check the local header + let local_block_start = dir.header_start() as usize; + let local_block_end = (dir.header_start() + 33) as usize; + let range_local_block = local_block_start..=local_block_end; + let local_block = archive_buffer.get(range_local_block).unwrap(); + eprintln!("local_block = {:x?}", local_block); + assert_eq!(local_block[0..4], [0x50, 0x4b, 0x03, 0x04]); // local header signature + assert_eq!(local_block[4..6], [0x14, 0x00]); // version + assert_eq!(local_block[6..8], [0x00, 0x00]); // flags + assert_eq!(local_block[8..10], [0x00, 0x00]); // compression + // assert_eq!(local_block[10..12], [0x00, 0x00]); // time + // assert_eq!(local_block[12..14], [0x00, 0x00]); // date + assert_eq!(local_block[14..18], [0x00, 0x00, 0x00, 0x00]); // crc 32 + assert_eq!(local_block[18..22], [0x00, 0x00, 0x00, 0x00]); // compressed size + assert_eq!(local_block[22..26], [0x00, 0x00, 0x00, 0x00]); // uncompressed size + assert_eq!(local_block[26..28], [0x04, 0x00]); // file name length + assert_eq!(local_block[28..30], [0x00, 0x00]); // extra field length + assert_eq!(local_block[30..], *b"dir/"); // file name + // there is not zip64 extra field in the local header } { let bigfile_archive = read_archive.by_name("dir/bigfile.bin").unwrap(); assert_eq!(bigfile_archive.compressed_size(), 1024 * 1024 * 1024); assert_eq!(bigfile_archive.size(), 1024 * 1024 * 1024); - let header_start = 4294967498; + let header_start = 4294967486; assert_eq!(bigfile_archive.header_start(), header_start); - assert_eq!(bigfile_archive.central_header_start(), 5368709673); - let start_file = bigfile_archive.header_start() as usize; - let range = (start_file)..(start_file + 70); // take a bunch of bytes from the local file header of the directory entry, which should contain the zip64 extra field - let raw_access = archive_buffer.get(range).unwrap(); - eprintln!("RAW ACCESS: {:x?}", raw_access); - assert_eq!(raw_access[0..4], [0x50, 0x4b, 0x03, 0x04]); // local file header signature - assert_eq!(raw_access[4..6], [0x2d, 0x00]); // version needed to extract - assert_eq!(raw_access[6..8], [0x00, 0x00]); // general purpose bit flag - assert_eq!(raw_access[8..10], [0x00, 0x00]); // compression method + assert_eq!(bigfile_archive.central_header_start(), 5368709637); + + let central_header_start = bigfile_archive.central_header_start() as usize; + // take a bunch of bytes from the central file header of the directory entry, which should contain the zip64 extra field + let central_header_end = central_header_start + 73; + let range = central_header_start..central_header_end; + let central_header = archive_buffer.get(range).unwrap(); + assert_eq!(central_header[0..4], [0x50, 0x4b, 0x01, 0x02]); // central file header signature + assert_eq!(central_header[4..6], [0x0A, 0x03]); // version made by + assert_eq!(central_header[6..8], [0x0A, 0x00]); // version needed to extract + assert_eq!(central_header[8..10], [0x00, 0x00]); // general purpose bit flag + assert_eq!(central_header[10..12], [0x00, 0x00]); // compression method + // assert_eq!(raw_access[12..14], [0x00, 0x00]); // last mod file time + // assert_eq!(raw_access[14..16], [0x00, 0x00]); // last mod file date + assert_eq!(central_header[16..20], [0xB0, 0xC2, 0x64, 0x5B]); // crc-32 + assert_eq!(central_header[20..24], bigfile_size.to_le_bytes()); // compressed size - IMPORTANT + assert_eq!(central_header[24..28], bigfile_size.to_le_bytes()); // uncompressed size - IMPORTANT + assert_eq!(central_header[28..30], [0x0f, 0x00]); // file name length + // IMPORTANT + assert_eq!(central_header[30..32], [0x0c, 0x00]); // extra field length + assert_eq!(central_header[32..34], [0x00, 0x00]); // file comment length + assert_eq!(central_header[34..36], [0x00, 0x00]); // disk number start + assert_eq!(central_header[36..38], [0x00, 0x00]); // internal file attributes + // assert_eq!(raw_access[38..42], [0x00, 0x00, 0x00, 0x00]); // external file attributes + // IMPORTANT + assert_eq!(central_header[42..46], [0xFF, 0xFF, 0xFF, 0xFF]); // relative offset of local header + assert_eq!(central_header[46..61], *b"dir/bigfile.bin"); // file name + assert_eq!(central_header[61..63], [0x01, 0x00]); // zip64 extra field header id + assert_eq!(central_header[63..65], [0x08, 0x00]); // zip64 extra field data size + assert_eq!( + central_header[65..], + [0xbe, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] + ); // zip64 extra field data Relative Header Offset + assert_eq!(central_header[65..], header_start.to_le_bytes()); // the offset in the zip64 extra field should match the header start of the file data + + // now we check the local header + let local_header_start = bigfile_archive.header_start() as usize; + // take a bunch of bytes from the local file header of the directory entry, which should contain the zip64 extra field + let local_header_end = local_header_start + 45; + let range = local_header_start..local_header_end; + let local_header = archive_buffer.get(range).unwrap(); + eprintln!("RAW ACCESS: {:x?}", local_header); + assert_eq!(local_header[0..4], [0x50, 0x4b, 0x03, 0x04]); // local file header signature + assert_eq!(local_header[4..6], [0x0A, 0x00]); // version needed to extract + assert_eq!(local_header[6..8], [0x00, 0x00]); // general purpose bit flag + assert_eq!(local_header[8..10], [0x00, 0x00]); // compression method // assert_eq!(raw_access[10..12], [0x00, 0x00]); // last mod file time // assert_eq!(raw_access[12..14], [0x00, 0x00]); // last mod file date - assert_eq!(raw_access[14..18], [176, 194, 100, 91]); // crc-32 - assert_eq!(raw_access[18..22], [0xff, 0xff, 0xff, 0xff]); // compressed size - assert_eq!(raw_access[22..26], [0xff, 0xff, 0xff, 0xff]); // uncompressed size - assert_eq!(raw_access[26..28], [0x0f, 0x00]); // file name length - assert_eq!(raw_access[28..30], [0x0c, 0x00]); // extra field length - assert_eq!(raw_access[30..45], *b"dir/bigfile.bin"); // file name - assert_eq!(raw_access[45..47], [0x01, 0x00]); // zip64 extra field header id - assert_eq!(raw_access[47..49], [0x08, 0x00]); // zip64 extra field data size - let offset_bytes = [0xca, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]; - assert_eq!(raw_access[49..57], offset_bytes); // zip64 extra field data Relative Header Offset - let offset = u64::from_le_bytes(offset_bytes); - assert_eq!(offset, header_start); // the offset in the zip64 extra field should match the header start of the file data + assert_eq!(local_header[14..18], [176, 194, 100, 91]); // crc-32 + assert_eq!(local_header[18..22], bigfile_size.to_le_bytes()); // compressed size + assert_eq!(local_header[22..26], bigfile_size.to_le_bytes()); // uncompressed size + assert_eq!(local_header[26..28], [0x0f, 0x00]); // file name length + // IMPORTANT + assert_eq!(local_header[28..30], [0x00, 0x00]); // extra field length + assert_eq!(local_header[30..], *b"dir/bigfile.bin"); // file name } - - panic!( - "This test is expected to fail because the zip64 extra field for the central directory header is not being written correctly, which causes the central directory to be misread and the archive to be considered malformed" - ); } fn zip64_check_extra_field( From a9d1f16fa010b946db48725c0b75a2d43e9ad642 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 16:23:56 -0700 Subject: [PATCH 23/32] re update import --- src/extra_fields/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/extra_fields/mod.rs b/src/extra_fields/mod.rs index a4f7510ca..e9470ccdf 100644 --- a/src/extra_fields/mod.rs +++ b/src/extra_fields/mod.rs @@ -4,8 +4,11 @@ use std::fmt::Display; mod extended_timestamp; mod ntfs; +mod zip64_extended_information; mod zipinfo_utf8; +pub(crate) use zip64_extended_information::Zip64ExtendedInformation; + // re-export pub use extended_timestamp::*; pub use ntfs::Ntfs; From 3dbf252d7e4877b0d80af2356f2585e97d3e8487 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 16:40:12 -0700 Subject: [PATCH 24/32] readd --- .../zip64_extended_information.rs | 36 +++++++------------ src/write.rs | 15 ++++---- tests/zip64_large.rs | 2 +- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/src/extra_fields/zip64_extended_information.rs b/src/extra_fields/zip64_extended_information.rs index fa96e187e..3963b8be0 100644 --- a/src/extra_fields/zip64_extended_information.rs +++ b/src/extra_fields/zip64_extended_information.rs @@ -32,36 +32,26 @@ impl Zip64ExtendedInformation { const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo; pub(crate) fn from_new_entry(is_large_file: bool) -> Option { - Self::local_header(is_large_file, 0, 0) + if is_large_file { + Self::local_header(u64::MAX, u64::MAX) + } else { + None + } } /// This entry in the Local header MUST include BOTH original and compressed file size fields - pub(crate) fn local_header( - is_large_file: bool, - mut uncompressed_size: u64, - mut compressed_size: u64, - ) -> Option { - if is_large_file { - uncompressed_size = u64::MAX; - compressed_size = u64::MAX; - } - let mut size: u16 = 0; + pub(crate) fn local_header(uncompressed_size: u64, compressed_size: u64) -> Option { let should_add_size = uncompressed_size >= ZIP64_BYTES_THR || compressed_size >= ZIP64_BYTES_THR; - let (uncompressed_size, compressed_size) = if should_add_size { - size += mem::size_of::() as u16; - size += mem::size_of::() as u16; - (Some(uncompressed_size), Some(compressed_size)) - } else { - (None, None) - }; - // TODO: (unsupported for now) - // Disk Start Number 4 bytes Number of the disk on which this file starts - - if size == 0 { - // no info added, return early + if !should_add_size { return None; } + let size = (mem::size_of::() + mem::size_of::()) as u16; + let uncompressed_size = Some(uncompressed_size); + let compressed_size = Some(compressed_size); + + // TODO: (unsupported for now) + // Disk Start Number 4 bytes Number of the disk on which this file starts Some(Self { _is_local_header: true, diff --git a/src/write.rs b/src/write.rs index ca16660c7..7edaa85a4 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2344,7 +2344,7 @@ fn update_local_file_header( // check compressed size as well as it can also be slightly larger than uncompressed size if file.compressed_size > spec::ZIP64_BYTES_THR { return Err(ZipError::Io(io::Error::other( - "Large file option has not been set", + "large_file(true) option has not been set", ))); } writer.write_u32_le(file.compressed_size as u32)?; @@ -2421,14 +2421,11 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, ) -> ZipResult<()> { - let block = Zip64ExtendedInformation::local_header( - file.large_file, - file.uncompressed_size, - file.compressed_size, - ) - .ok_or(invalid!( - "Attempted to update a nonexistent ZIP64 extra field" - ))?; + let block = + Zip64ExtendedInformation::local_header(file.uncompressed_size, file.compressed_size) + .ok_or(invalid!( + "Attempted to update a nonexistent ZIP64 extra field" + ))?; let zip64_extra_field_start = file.header_start + size_of::() as u64 diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index 14a4bb9f9..45b4db62c 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -313,7 +313,7 @@ fn test_zip64_check_extra_field() { let range = central_header_start..central_header_end; let central_header = archive_buffer.get(range).unwrap(); assert_eq!(central_header[0..4], [0x50, 0x4b, 0x01, 0x02]); // central file header signature - assert_eq!(central_header[4..6], [0x0A, 0x03]); // version made by + // assert_eq!(central_header[4..6], [0x0A, 0x03]); // version made by assert_eq!(central_header[6..8], [0x0A, 0x00]); // version needed to extract assert_eq!(central_header[8..10], [0x00, 0x00]); // general purpose bit flag assert_eq!(central_header[10..12], [0x00, 0x00]); // compression method From f3f3cb356549e0e7fdb2225d9a00c42a0f810d4a Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 17:37:56 -0700 Subject: [PATCH 25/32] handle user forcing the large_file() --- src/extra_fields/zip64_extended_information.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/extra_fields/zip64_extended_information.rs b/src/extra_fields/zip64_extended_information.rs index 3963b8be0..8048f4352 100644 --- a/src/extra_fields/zip64_extended_information.rs +++ b/src/extra_fields/zip64_extended_information.rs @@ -33,16 +33,23 @@ impl Zip64ExtendedInformation { pub(crate) fn from_new_entry(is_large_file: bool) -> Option { if is_large_file { - Self::local_header(u64::MAX, u64::MAX) + Self::local_header(true, u64::MAX, u64::MAX) } else { None } } /// This entry in the Local header MUST include BOTH original and compressed file size fields - pub(crate) fn local_header(uncompressed_size: u64, compressed_size: u64) -> Option { - let should_add_size = - uncompressed_size >= ZIP64_BYTES_THR || compressed_size >= ZIP64_BYTES_THR; + /// If the user is using `is_large_file` when the file is not large we force the zip64 extra field + pub(crate) fn local_header( + is_large_file: bool, + uncompressed_size: u64, + compressed_size: u64, + ) -> Option { + // here - we force if `is_large_file` is `true` + let should_add_size = is_large_file + || uncompressed_size >= ZIP64_BYTES_THR + || compressed_size >= ZIP64_BYTES_THR; if !should_add_size { return None; } From 9a870973030e4f10b76b0357993bfbf98d461190 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 17:43:28 -0700 Subject: [PATCH 26/32] fix and add comment to test --- src/write.rs | 13 ++++++++----- tests/zip64_large.rs | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/write.rs b/src/write.rs index 7edaa85a4..58580b894 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2421,11 +2421,14 @@ fn update_local_zip64_extra_field( writer: &mut T, file: &mut ZipFileData, ) -> ZipResult<()> { - let block = - Zip64ExtendedInformation::local_header(file.uncompressed_size, file.compressed_size) - .ok_or(invalid!( - "Attempted to update a nonexistent ZIP64 extra field" - ))?; + let block = Zip64ExtendedInformation::local_header( + file.large_file, + file.uncompressed_size, + file.compressed_size, + ) + .ok_or(invalid!( + "Attempted to update a nonexistent ZIP64 extra field" + ))?; let zip64_extra_field_start = file.header_start + size_of::() as u64 diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index 45b4db62c..2190741e3 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -234,6 +234,11 @@ fn test_zip64_check_extra_field() { assert_eq!(res.unwrap(), ()); assert_eq!(archive_buffer.len(), 5368709808); + // uncomment for debug + // use std::io::Write; + // let mut file = File::create("tests/data/test_zip64_check_extra_field.zip").unwrap(); + // file.write_all(&archive_buffer).unwrap(); + let mut read_archive = zip::ZipArchive::new(Cursor::new(&archive_buffer)).expect("Failed to read the archive"); From 7778c1c03e5908a18853b1c728c4c70b4ca733c4 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 18:01:32 -0700 Subject: [PATCH 27/32] not on wasm --- tests/zip64_large.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index 2190741e3..dbe93c142 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -216,6 +216,9 @@ fn zip64_large() { } } +/// We cannot run this test because on wasm32 +/// the literal `5368709808` does not fit into the type `usize` whose range is `0..=4294967295` +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_zip64_check_extra_field() { let path = Path::new("bigfile.bin"); @@ -313,7 +316,7 @@ fn test_zip64_check_extra_field() { assert_eq!(bigfile_archive.central_header_start(), 5368709637); let central_header_start = bigfile_archive.central_header_start() as usize; - // take a bunch of bytes from the central file header of the directory entry, which should contain the zip64 extra field + // take a bunch of bytes from the central file header of the file entry, which should contain the zip64 extra field let central_header_end = central_header_start + 73; let range = central_header_start..central_header_end; let central_header = archive_buffer.get(range).unwrap(); @@ -347,7 +350,6 @@ fn test_zip64_check_extra_field() { // now we check the local header let local_header_start = bigfile_archive.header_start() as usize; - // take a bunch of bytes from the local file header of the directory entry, which should contain the zip64 extra field let local_header_end = local_header_start + 45; let range = local_header_start..local_header_end; let local_header = archive_buffer.get(range).unwrap(); From cc381e3f8525deeaaf7fbd02efd0ebabc4ccef09 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 18:27:37 -0700 Subject: [PATCH 28/32] fix unused --- tests/zip64_large.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index dbe93c142..ea0e71dbf 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -370,6 +370,9 @@ fn test_zip64_check_extra_field() { } } +/// We cannot run this test because on wasm32 +/// See `test_zip64_check_extra_field` +#[cfg(not(target_arch = "wasm32"))] fn zip64_check_extra_field( path: &Path, archive_buffer: &mut Vec, From 8fc51fefe45c05552d5a464b740747683897e9dd Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sun, 22 Feb 2026 20:21:57 -0700 Subject: [PATCH 29/32] fix imports again and again --- tests/zip64_large.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index ea0e71dbf..92f4bd489 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -53,13 +53,7 @@ // 22c400260 00 00 50 4b 05 06 00 00 00 00 03 00 03 00 27 01 |..PK..........'.| // 22c400270 00 00 ff ff ff ff 00 00 |........| // 22c400278 -use std::{ - fs::File, - io::{self, Cursor, Read, Seek, SeekFrom}, - path::Path, -}; - -use zip::write::SimpleFileOptions; +use std::io::{self, Read, Seek, SeekFrom}; const BLOCK1_LENGTH: u64 = 0x60; const BLOCK1: [u8; BLOCK1_LENGTH as usize] = [ @@ -221,6 +215,7 @@ fn zip64_large() { #[cfg(not(target_arch = "wasm32"))] #[test] fn test_zip64_check_extra_field() { + use std::{fs::File, io::Cursor, path::Path}; let path = Path::new("bigfile.bin"); let bigfile = File::create(path).expect("Failed to create a big file"); @@ -374,9 +369,11 @@ fn test_zip64_check_extra_field() { /// See `test_zip64_check_extra_field` #[cfg(not(target_arch = "wasm32"))] fn zip64_check_extra_field( - path: &Path, + path: &std::path::Path, archive_buffer: &mut Vec, ) -> Result<(), Box> { + use std::{fs::File, io::Cursor}; + use zip::write::SimpleFileOptions; let mut bigfile = File::open(path)?; let mut archive = zip::ZipWriter::new(Cursor::new(archive_buffer)); From 7a27a73b614ed627a0e376768712ecb48d11ec00 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Fri, 27 Feb 2026 08:52:42 -0700 Subject: [PATCH 30/32] cleanup --- src/extra_fields/zip64_extended_information.rs | 4 ++-- src/types.rs | 14 -------------- src/write.rs | 2 +- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/extra_fields/zip64_extended_information.rs b/src/extra_fields/zip64_extended_information.rs index 8048f4352..8f11dc074 100644 --- a/src/extra_fields/zip64_extended_information.rs +++ b/src/extra_fields/zip64_extended_information.rs @@ -16,7 +16,7 @@ use crate::{ZIP64_BYTES_THR, extra_fields::UsedExtraField}; /// Zip64 extended information extra field #[derive(Copy, Clone, Debug)] -pub struct Zip64ExtendedInformation { +pub(crate) struct Zip64ExtendedInformation { /// The local header does not contains any `header_start` _is_local_header: bool, magic: UsedExtraField, @@ -31,7 +31,7 @@ pub struct Zip64ExtendedInformation { impl Zip64ExtendedInformation { const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo; - pub(crate) fn from_new_entry(is_large_file: bool) -> Option { + pub(crate) fn new_local(is_large_file: bool) -> Option { if is_large_file { Self::local_header(true, u64::MAX, u64::MAX) } else { diff --git a/src/types.rs b/src/types.rs index 93684119b..757303059 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1016,20 +1016,6 @@ impl ZipFileData { }) } - // pub(crate) fn was_large_file(&self) -> bool { - // // if self.compressed_size > spec::ZIP64_BYTES_THR { - // // return true; - // // } - // // if self.uncompressed_size > spec::ZIP64_BYTES_THR { - // // return true; - // // } - // if self.header_start >= spec::ZIP64_BYTES_THR { - // return true; - // } - // // TODO: Also disk number (unsupported for now) - // false - // } - pub(crate) fn block(&self) -> ZipResult { let compressed_size = self .compressed_size diff --git a/src/write.rs b/src/write.rs index 58580b894..4bf445ef8 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1128,7 +1128,7 @@ impl ZipWriter { None => vec![], }; let central_extra_data = options.extended_options.central_extra_data(); - if let Some(zip64_block) = Zip64ExtendedInformation::from_new_entry(options.large_file) { + if let Some(zip64_block) = Zip64ExtendedInformation::new_local(options.large_file) { let mut new_extra_data = zip64_block.serialize().into_vec(); new_extra_data.append(&mut extra_data); extra_data = new_extra_data; From f159eef174500df5a3ac9f5d86f294248d3c1f61 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Fri, 27 Feb 2026 09:05:53 -0700 Subject: [PATCH 31/32] clippy --- src/write.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/write.rs b/src/write.rs index 4bf445ef8..7fc0043f6 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2497,11 +2497,10 @@ impl Seek for StreamWriter { fn seek(&mut self, pos: SeekFrom) -> io::Result { match pos { SeekFrom::Current(0) | SeekFrom::End(0) => return Ok(self.bytes_written), - SeekFrom::Start(x) => { - if x == self.bytes_written { + SeekFrom::Start(x) + if x == self.bytes_written => { return Ok(self.bytes_written); } - } _ => {} } Err(io::Error::new( From d030fd2d410105fe80b29f4bde79071331b27db5 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Fri, 27 Feb 2026 09:13:01 -0700 Subject: [PATCH 32/32] fmt --- src/write.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/write.rs b/src/write.rs index 7fc0043f6..77296a8f0 100644 --- a/src/write.rs +++ b/src/write.rs @@ -2497,10 +2497,9 @@ impl Seek for StreamWriter { fn seek(&mut self, pos: SeekFrom) -> io::Result { match pos { SeekFrom::Current(0) | SeekFrom::End(0) => return Ok(self.bytes_written), - SeekFrom::Start(x) - if x == self.bytes_written => { - return Ok(self.bytes_written); - } + SeekFrom::Start(x) if x == self.bytes_written => { + return Ok(self.bytes_written); + } _ => {} } Err(io::Error::new(