Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1732fef
fix: zip64 central header
Its-Just-Nans Feb 6, 2026
93ef7f9
rm typo
Its-Just-Nans Feb 6, 2026
74e8085
revert and re-fix
Its-Just-Nans Feb 6, 2026
16debc5
fmt
Its-Just-Nans Feb 6, 2026
0271388
re fix
Its-Just-Nans Feb 6, 2026
06b29c5
Update src/types.rs
Pr0methean Feb 6, 2026
579a25d
Merge branch 'master' into fix-zip64-central-header
Pr0methean Feb 6, 2026
83f0dcc
add some code
Its-Just-Nans Feb 7, 2026
b6ae8b0
Merge branch 'master' into fix-zip64-central-header
Its-Just-Nans Feb 7, 2026
ea8e004
typos
Its-Just-Nans Feb 7, 2026
760beb6
comment for now
Its-Just-Nans Feb 7, 2026
adedb12
Merge branch 'master' into fix-zip64-central-header
Pr0methean Feb 15, 2026
4f1662c
[skip ci] fix: correct ZIP64 local header update and remove debug sta…
amazon-q-developer[bot] Feb 15, 2026
7c9a4d8
Update write_dir.rs: use debug formatting when logging path
Pr0methean Feb 15, 2026
98eb52f
Update write_dir.rs
Pr0methean Feb 15, 2026
a5228c9
Merge branch 'master' into fix-zip64-central-header
Pr0methean Feb 15, 2026
5e4df5a
Fix: missing closing curly
Pr0methean Feb 15, 2026
a17854d
Remove debug logging
Pr0methean Feb 15, 2026
df11cad
Merge branch 'master' into fix-zip64-central-header
Pr0methean Feb 15, 2026
d33488f
Fix: unclosed delimiter due to duplicated line
Pr0methean Feb 15, 2026
d599cfe
Fix: accidentally deleted line at start of update_local_zip64_extra_f…
Pr0methean Feb 15, 2026
cb3a796
Merge branch 'master' into fix-zip64-central-header
Its-Just-Nans Feb 15, 2026
5857e89
add method
Its-Just-Nans Feb 15, 2026
b923f64
add test
Its-Just-Nans Feb 15, 2026
fe34f0b
Merge branch 'master' into fix-zip64-central-header
Pr0methean Feb 16, 2026
7852894
Merge branch 'master' into fix-zip64-central-header
Its-Just-Nans Feb 21, 2026
0b749dc
rewrite zip64 extended
Its-Just-Nans Feb 21, 2026
4876ded
change mod.rs
Its-Just-Nans Feb 21, 2026
33bb522
remove from enum for now
Its-Just-Nans Feb 21, 2026
06eb6fb
fix zip64
Its-Just-Nans Feb 22, 2026
529545e
Merge branch 'master' into fix-zip64-central-header
Its-Just-Nans Feb 22, 2026
a9d1f16
re update import
Its-Just-Nans Feb 22, 2026
3dbf252
readd
Its-Just-Nans Feb 22, 2026
f3f3cb3
handle user forcing the large_file()
Its-Just-Nans Feb 23, 2026
9a87097
fix and add comment to test
Its-Just-Nans Feb 23, 2026
7778c1c
not on wasm
Its-Just-Nans Feb 23, 2026
cc381e3
fix unused
Its-Just-Nans Feb 23, 2026
8fc51fe
fix imports again and again
Its-Just-Nans Feb 23, 2026
7a27a73
cleanup
Its-Just-Nans Feb 27, 2026
f159eef
clippy
Its-Just-Nans Feb 27, 2026
d030fd2
fmt
Its-Just-Nans Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions examples/write_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/extra_fields/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<UsedExtraField> 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)
Expand Down
165 changes: 165 additions & 0 deletions src/extra_fields/zip64_extended_information.rs
Original file line number Diff line number Diff line change
@@ -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<u64>,
compressed_size: Option<u64>,
header_start: Option<u64>,
// Not used field
// disk_start: Option<u32>
}

impl Zip64ExtendedInformation {
const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo;

pub(crate) fn new_local(is_large_file: bool) -> Option<Self> {
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<Self> {
// 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::<u64>() + mem::size_of::<u64>()) as u16;
let uncompressed_size = Some(uncompressed_size);
let compressed_size = Some(compressed_size);

// TODO: (unsupported for now)

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
// 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<Self> {
let mut size: u16 = 0;
let uncompressed_size = if uncompressed_size != 0 && uncompressed_size >= ZIP64_BYTES_THR {
size += mem::size_of::<u64>() as u16;
Some(uncompressed_size)
} else {
None
};
let compressed_size = if compressed_size != 0 && compressed_size >= ZIP64_BYTES_THR {
size += mem::size_of::<u64>() as u16;
Some(compressed_size)
} else {
None
};
let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR {
size += mem::size_of::<u64>() 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::<UsedExtraField>() + mem::size_of::<u16>()
}

/// 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()
}
}
}
38 changes: 0 additions & 38 deletions src/spec.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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
Expand Down
Loading