From 1c6ac2e086ae06289da205a4206dafa36f661ac0 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 04:02:41 +0000 Subject: [PATCH] fix(xfl): honor the ZIP64 entry-count sentinel --- crates/xfl/src/lib.rs | 6 +++--- crates/xfl/src/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/xfl/src/lib.rs b/crates/xfl/src/lib.rs index 26c726c39..488e69ceb 100644 --- a/crates/xfl/src/lib.rs +++ b/crates/xfl/src/lib.rs @@ -262,9 +262,10 @@ fn find_end(data: &[u8]) -> Result<(u64, u64, u64, usize), ZipError> { // consistent candidate is found. This preserves useful damaged-ZIP diagnostics. fallback = Some(candidate); + let count = u16_at(data, candidate + 10).unwrap_or(u16::MAX); let size = u32_at(data, candidate + 12).unwrap_or(u32::MAX); let offset = u32_at(data, candidate + 16).unwrap_or(u32::MAX); - if size == u32::MAX || offset == u32::MAX { + if count == u16::MAX || size == u32::MAX || offset == u32::MAX { let locator = candidate.checked_sub(20).filter(|&at| u32_at(data, at) == Some(LOCATOR64)); let valid = locator.and_then(|locator| find_zip64_end(data, locator)).and_then(|at| { let count = u64_at(data, at + 32)?; @@ -280,8 +281,7 @@ fn find_end(data: &[u8]) -> Result<(u64, u64, u64, usize), ZipError> { continue; } } else { - let count = u16_at(data, candidate + 10).map(u64::from).unwrap_or(u64::MAX); - let Some((_, shift)) = locate_directory(data, count, size.into(), offset.into(), candidate) else { + let Some((_, shift)) = locate_directory(data, count.into(), size.into(), offset.into(), candidate) else { continue; }; if count == 0 && shift > 0 { diff --git a/crates/xfl/src/tests.rs b/crates/xfl/src/tests.rs index 5d35118fb..76de11450 100644 --- a/crates/xfl/src/tests.rs +++ b/crates/xfl/src/tests.rs @@ -319,6 +319,26 @@ fn zip64_archives_open_and_their_damage_is_named() { assert_eq!(Archive::open(&t).err(), Some(ZipError::DamagedEntry("a.xml".into(), "has no ZIP64 sizes"))); } +#[test] +fn zip64_count_sentinel_keeps_a_comment_eocd_from_masking_the_archive() { + let mut bytes = write(&[Raw::new("a.xml", b"x", false)], true); + let end = bytes.len() - 22; + let locator = end - 20; + let zip64_end = locator - 56; + let directory_size = u64_at(&bytes, zip64_end + 40).unwrap() as u32; + let directory_offset = u64_at(&bytes, zip64_end + 48).unwrap() as u32; + // Count requires ZIP64, while directory size and offset still fit in the classic fields. + bytes[end + 12..end + 16].copy_from_slice(&directory_size.to_le_bytes()); + bytes[end + 16..end + 20].copy_from_slice(&directory_offset.to_le_bytes()); + bytes[end + 20..end + 22].copy_from_slice(&22u16.to_le_bytes()); + // This valid empty EOCD-shaped signature is inside the real end record's comment. + bytes.extend_from_slice(&END.to_le_bytes()); + bytes.extend_from_slice(&[0; 18]); + + let archive = Archive::open(&bytes).unwrap(); + assert_eq!(archive.read("a.xml").unwrap().unwrap(), b"x"); +} + #[test] fn self_extracting_archives_support_relative_and_adjusted_offsets() { let entries = [Raw::new("DOMDocument.xml", b"", true), Raw::new("LIBRARY/a.xml", b"", false)];