From 7c108201239d2504bb61e941828dc9f388187a94 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 04:26:31 +0000 Subject: [PATCH] Preserve ZIP directory semantics in bundle reader --- crates/scraft-format/src/bundle.rs | 5 +++-- crates/zip/src/archive.rs | 9 +++++++++ crates/zip/src/lib.rs | 7 +++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/scraft-format/src/bundle.rs b/crates/scraft-format/src/bundle.rs index 4421472a2..6b4885003 100644 --- a/crates/scraft-format/src/bundle.rs +++ b/crates/scraft-format/src/bundle.rs @@ -95,10 +95,11 @@ pub fn read_bundle(bytes: &[u8], limits: BundleLimits) -> Result<(String, HashMa let mut session = None; let mut files = HashMap::new(); for index in 0..len { - let name = archive.entry_name(index).map_err(zip_error)?.ok_or_else(|| BundleError::Invalid("missing entry".into()))?; - if name.ends_with('/') { + let is_dir = archive.entry_is_dir(index).map_err(zip_error)?.ok_or_else(|| BundleError::Invalid("missing entry".into()))?; + if is_dir { continue; } + let name = archive.entry_name(index).map_err(zip_error)?.ok_or_else(|| BundleError::Invalid("missing entry".into()))?; let is_session = !name.contains('/') && Path::new(&name).extension().and_then(OsStr::to_str) == Some(SESSION_EXTENSION); let size = archive.declared_size(&name).ok_or_else(|| BundleError::Invalid(format!("{name}: missing entry")))?; if is_session && session.is_none() { diff --git a/crates/zip/src/archive.rs b/crates/zip/src/archive.rs index 1783b3e06..83d7b603d 100644 --- a/crates/zip/src/archive.rs +++ b/crates/zip/src/archive.rs @@ -78,6 +78,15 @@ impl<'a> Archive<'a> { Ok(Some(name.into_owned())) } + /// Whether the entry at `index` is a directory according to ZIP filename conventions. + pub fn entry_is_dir(&mut self, index: usize) -> Result, Error> { + if index >= self.zip.len() { + return Ok(None); + } + let file = self.zip.by_index_raw(index).map_err(|error| Error::NotZip(error.to_string()))?; + Ok(Some(file.is_dir())) + } + /// Do two entries share a name? `zip` then keeps one of them, and which one a writer meant /// is not known: a format that must not guess refuses the archive. pub fn has_duplicate_names(&self) -> bool { diff --git a/crates/zip/src/lib.rs b/crates/zip/src/lib.rs index ef3264294..452351ee3 100644 --- a/crates/zip/src/lib.rs +++ b/crates/zip/src/lib.rs @@ -231,11 +231,14 @@ mod tests { zip.write_all(b"1").unwrap(); zip.start_file("second", opts).unwrap(); zip.write_all(b"2").unwrap(); + zip.start_file("folder\\", opts).unwrap(); let bytes = zip.finish().unwrap().into_inner(); - let mut archive = Archive::open(&bytes, Limits { max_entries: 2, max_total_bytes: 10 }).unwrap(); + let mut archive = Archive::open(&bytes, Limits { max_entries: 3, max_total_bytes: 10 }).unwrap(); assert_eq!(archive.entry_name(0).unwrap().as_deref(), Some("first")); assert_eq!(archive.entry_name(1).unwrap().as_deref(), Some("second")); - assert_eq!(archive.entry_name(2).unwrap(), None); + assert_eq!(archive.entry_is_dir(2).unwrap(), Some(true)); + assert_eq!(archive.entry_name(2).unwrap().as_deref(), Some("folder\\")); + assert_eq!(archive.entry_name(3).unwrap(), None); } }