From 35de20b108d7d075518c80d3eb53d89f87082c0f Mon Sep 17 00:00:00 2001 From: "Prin.Ing" <267065148+Prin-Ing@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:31:15 +0000 Subject: [PATCH] Limit the ZIP writer to 65534 files so Archive::open can read it Found while testing crates/engine/src/cmd/fileio/zip.rs:26 (store, deflate) vs :178 (Archive::open) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/fileio/zip.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/fileio/zip.rs b/crates/engine/src/cmd/fileio/zip.rs index bed66a18c..8279b368f 100644 --- a/crates/engine/src/cmd/fileio/zip.rs +++ b/crates/engine/src/cmd/fileio/zip.rs @@ -21,10 +21,10 @@ const VERSION: u16 = 20; pub const DEFLATE_CHUNK: usize = 1 << 20; /// The archive of `files` (`(name, bytes)`; `/` separates folders in a name), stored without -/// compression. Fails past the limits of a ZIP without its 64-bit extension (65535 files, 4 GB). +/// compression. Fails past the limits of a ZIP without its 64-bit extension (65534 files, 4 GB; 65535 is the ZIP64 marker). pub fn store, B: AsRef<[u8]>>(files: &[(N, B)]) -> Result, String> { - let too_big = || "too much to zip: at most 65535 files and 4 GB".to_string(); - let count = u16::try_from(files.len()).map_err(|_| too_big())?; + let too_big = || "too much to zip: at most 65534 files and 4 GB".to_string(); + let count = u16::try_from(files.len()).ok().filter(|&c| c != u16::MAX).ok_or_else(too_big)?; let mut out = Vec::with_capacity(files.iter().map(|(n, b)| 76 + 2 * n.as_ref().len() + b.as_ref().len()).sum::() + 22); let mut central = Vec::new(); for (name, bytes) in files { @@ -79,7 +79,7 @@ pub fn store, B: AsRef<[u8]>>(files: &[(N, B)]) -> Result, /// Fails past the same limits as [`store`]. `stop` is asked before each [`DEFLATE_CHUNK`] bytes are /// deflated; when it answers true, the archive is dropped and an error returned. pub fn deflate, B: AsRef<[u8]>>(files: &[(N, B)], (time, date): (u16, u16), stop: impl Fn() -> bool) -> Result, String> { - let too_big = || "too much to zip: at most 65535 files and 4 GB".to_string(); + let too_big = || "too much to zip: at most 65534 files and 4 GB".to_string(); let (mut out, mut central) = (Vec::new(), Vec::new()); for (name, bytes) in files { let (name, bytes) = (name.as_ref(), bytes.as_ref()); @@ -118,7 +118,7 @@ pub fn deflate, B: AsRef<[u8]>>(files: &[(N, B)], (time, date): (u central.extend_from_slice(&offset.to_le_bytes()); central.extend_from_slice(name.as_bytes()); } - let count = u16::try_from(files.len()).map_err(|_| too_big())?; + let count = u16::try_from(files.len()).ok().filter(|&c| c != u16::MAX).ok_or_else(too_big)?; let (start, len) = (u32::try_from(out.len()).map_err(|_| too_big())?, u32::try_from(central.len()).map_err(|_| too_big())?); out.extend_from_slice(¢ral); out.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); @@ -315,3 +315,19 @@ pub fn entries(zip: &[u8]) -> Result)>, String> { } Ok(out) } + +#[cfg(test)] +mod limit_tests { + use super::*; + + #[test] + fn max_files_round_trip_through_the_reader() { + let at_limit: Vec<(String, &[u8])> = (0..65534).map(|i| (format!("{i}"), &[][..])).collect(); + assert!(Archive::open(&store(&at_limit).unwrap(), usize::MAX).is_ok()); + let deflated = deflate(&at_limit, FIXED_TIME, || false).unwrap(); + assert!(Archive::open(&deflated, usize::MAX).is_ok()); + let over: Vec<(String, &[u8])> = (0..65535).map(|i| (format!("{i}"), &[][..])).collect(); + assert!(store(&over).is_err()); + assert!(deflate(&over, FIXED_TIME, || false).is_err()); + } +}