diff --git a/examples/write_dir.rs b/examples/write_dir.rs index 030dfea26..76a5b3295 100644 --- a/examples/write_dir.rs +++ b/examples/write_dir.rs @@ -110,24 +110,32 @@ 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/extra_fields/mod.rs b/src/extra_fields/mod.rs index 22ff1acfc..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; @@ -58,6 +61,19 @@ 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 + } +} + impl Display for UsedExtraField { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "0x{:04X}", *self as u16) diff --git a/src/extra_fields/zip64_extended_information.rs b/src/extra_fields/zip64_extended_information.rs new file mode 100644 index 000000000..8f11dc074 --- /dev/null +++ b/src/extra_fields/zip64_extended_information.rs @@ -0,0 +1,165 @@ +//! 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(crate) struct Zip64ExtendedInformation { + /// The local header does not contains any `header_start` + _is_local_header: bool, + 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; + + pub(crate) fn new_local(is_large_file: bool) -> Option { + if is_large_file { + Self::local_header(true, u64::MAX, u64::MAX) + } else { + None + } + } + + /// This entry in the Local header MUST include BOTH original and compressed file size fields + /// If the user is using `is_large_file` when the file is not large we force the zip64 extra field + pub(crate) fn local_header( + 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; + } + 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, + magic: Self::MAGIC, + size, + uncompressed_size, + compressed_size, + header_start: None, + }) + } + + 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 { + _is_local_header: false, + 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 { + _is_local_header, + magic, + size, + uncompressed_size, + compressed_size, + header_start, + } = 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)); + + 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 d8d76797b..757303059 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 { @@ -1018,6 +1017,21 @@ impl ZipFileData { } pub(crate) fn block(&self) -> ZipResult { + 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() @@ -1040,16 +1054,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() @@ -1066,23 +1072,10 @@ 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, }) } - 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, @@ -1223,93 +1216,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 { - pub(crate) fn maybe_new( - large_file: bool, - uncompressed_size: u64, - compressed_size: u64, - header_start: u64, - ) -> Option { - let mut size: u16 = 0; - let uncompressed_size = if uncompressed_size >= ZIP64_BYTES_THR || large_file { - size += mem::size_of::() as u16; - Some(uncompressed_size) - } else { - None - }; - let compressed_size = if compressed_size >= ZIP64_BYTES_THR || large_file { - size += mem::size_of::() as u16; - Some(compressed_size) - } else { - None - }; - let header_start = if header_start >= ZIP64_BYTES_THR { - size += mem::size_of::() as u16; - Some(header_start) - } else { - None - }; - if size == 0 { - return None; - } - - Some(Zip64ExtraFieldBlock { - magic: spec::ExtraFieldMagic::ZIP64_EXTRA_FIELD_TAG, - size, - uncompressed_size, - compressed_size, - header_start, - }) - } -} - -impl Zip64ExtraFieldBlock { - pub fn full_size(&self) -> usize { - assert!(self.size > 0); - 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 856cc4a6f..77296a8f0 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}; @@ -1122,21 +1123,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) = - Zip64ExtraFieldBlock::maybe_new(options.large_file, 0, 0, header_start) - { + 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; @@ -2346,13 +2338,13 @@ 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 { 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)?; @@ -2370,12 +2362,17 @@ 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 = Zip64ExtendedInformation::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 { 0 }; - let total_extra_len = zip64_len + stripped_extra.len() + central_len; + let total_extra_len = zip64_block_len + stripped_extra.len() + central_len; block.extra_field_length = u16::try_from(total_extra_len) .map_err(|_| invalid!("Extra field length in central directory exceeds 64KiB"))?; @@ -2383,7 +2380,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() { @@ -2424,7 +2421,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 = Zip64ExtendedInformation::local_header( + file.large_file, + file.uncompressed_size, + file.compressed_size, + ) + .ok_or(invalid!( "Attempted to update a nonexistent ZIP64 extra field" ))?; @@ -2495,10 +2497,8 @@ 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); } _ => {} } diff --git a/tests/zip64_large.rs b/tests/zip64_large.rs index 2f17bbc84..92f4bd489 100644 --- a/tests/zip64_large.rs +++ b/tests/zip64_large.rs @@ -209,3 +209,190 @@ 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() { + 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"); + + let bigfile_size: u32 = 1024 * 1024 * 1024; + // 1024 MiB = 1024 * 1024 * 1024 bytes + bigfile + .set_len(bigfile_size as u64) + .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(), 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"); + + 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(), 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!( + central_header[54..], + [0x9c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00] + ); // 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 = 4294967486; + assert_eq!(bigfile_archive.header_start(), header_start); + 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 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(); + 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; + 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!(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 + } +} + +/// 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: &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)); + 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(()) +}