From 79f0d3610a2287888665f7f4637fe0c302ae8195 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 11 Apr 2026 13:19:54 -0600 Subject: [PATCH 01/23] use bytes direclty --- src/read.rs | 3 ++- src/write.rs | 31 +++++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/read.rs b/src/read.rs index c38fb0d73..b6674cd63 100644 --- a/src/read.rs +++ b/src/read.rs @@ -217,7 +217,7 @@ impl ZipArchive { pub(crate) fn merge_contents( &mut self, mut w: W, - ) -> ZipResult, ZipFileData>> { + ) -> ZipResult, ZipFileData>> { if self.shared.files.is_empty() { return Ok(IndexMap::new()); } @@ -277,6 +277,7 @@ impl ZipArchive { /* Copy over file data from source archive directly. */ io::copy(&mut limited_raw, &mut w)?; + let new_files = new_files.into_iter().map(|f| (f.0.into_boxed_bytes(), f.1)).collect(); /* Return the files we've just written to the data stream. */ Ok(new_files) } diff --git a/src/write.rs b/src/write.rs index fa8d87fd0..f2fbf393f 100644 --- a/src/write.rs +++ b/src/write.rs @@ -172,7 +172,7 @@ pub(crate) mod zip_writer { /// ``` pub struct ZipWriter { pub(super) inner: GenericZipWriter, - pub(super) files: IndexMap, ZipFileData>, + pub(super) files: IndexMap, ZipFileData>, pub(super) stats: ZipWriterStats, pub(super) writing_to_file: bool, pub(super) writing_raw: bool, @@ -835,10 +835,10 @@ impl ZipWriter { pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult> { readwriter.seek(SeekFrom::Start(0))?; let shared = ZipArchive::get_metadata(config, &mut readwriter)?; - + let files = shared.files.into_iter().map(|f| (f.0.into_boxed_bytes(), f.1)).collect(); Ok(ZipWriter { inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), - files: shared.files, + files: files, stats: ZipWriterStats::default(), writing_to_file: false, comment: shared.comment, @@ -873,11 +873,11 @@ impl ZipWriter { /// widely-compatible archive compared to [`Self::shallow_copy_file`]. Does not copy alignment. pub fn deep_copy_file(&mut self, src_name: &str, dest_name: &str) -> ZipResult<()> { self.finish_file()?; - if src_name == dest_name || self.files.contains_key(dest_name) { + if src_name == dest_name || self.files.contains_key(dest_name.as_bytes()) { return Err(invalid!("That file already exists")); } let write_position = self.inner.try_inner_mut()?.stream_position()?; - let src_index = self.index_by_name(src_name)?; + let src_index = self.index_by_name(src_name.as_bytes())?; let src_data = &mut self.files[src_index]; let src_data_start = src_data.data_start(self.inner.try_inner_mut()?)?; debug_assert!(src_data_start <= write_position); @@ -894,9 +894,8 @@ impl ZipWriter { .try_inner_mut()? .seek(SeekFrom::Start(write_position))?; let mut new_data = src_data.clone(); - let dest_name_raw = dest_name.as_bytes(); + new_data.file_name_raw = dest_name.as_bytes().into(); new_data.file_name = dest_name.into(); - new_data.file_name_raw = dest_name_raw.into(); new_data.header_start = write_position; let extra_data_start = write_position + (size_of::() + size_of::()) as u64 @@ -989,7 +988,15 @@ impl ZipWriter { let comment = mem::take(&mut self.comment); let zip64_extensible_data_sector = mem::take(&mut self.zip64_extensible_data_sector); let files = mem::take(&mut self.files); - + let files: IndexMap, ZipFileData> = files + .into_iter() + .map(|f| { + let s = String::from_utf8(f.0.into_vec()) + .expect("invalid UTF-8") + .into_boxed_str(); + (s, f.1) + }) + .collect(); Ok(ZipArchive::from_finalized_writer( files, comment, @@ -1381,10 +1388,10 @@ impl ZipWriter { } fn insert_file_data(&mut self, file: ZipFileData) -> ZipResult { - if self.files.contains_key(&file.file_name) { + if self.files.contains_key(file.file_name.as_bytes()) { return Err(invalid!("Duplicate filename: {}", file.file_name)); } - let (index, _) = self.files.insert_full(file.file_name.clone(), file); + let (index, _) = self.files.insert_full(file.file_name.as_bytes().to_vec().into_boxed_slice(), file); Ok(index) } @@ -1962,7 +1969,7 @@ impl ZipWriter { Ok(central_start) } - fn index_by_name(&self, name: &str) -> ZipResult { + fn index_by_name(&self, name: &[u8]) -> ZipResult { self.files.get_index_of(name).ok_or(ZipError::FileNotFound) } @@ -1976,7 +1983,7 @@ impl ZipWriter { if src_name == dest_name { return Err(invalid!("Trying to copy a file to itself")); } - let src_index = self.index_by_name(src_name)?; + let src_index = self.index_by_name(src_name.as_bytes())?; let mut dest_data = self.files[src_index].clone(); dest_data.file_name = dest_name.into(); dest_data.file_name_raw = dest_name.as_bytes().into(); From f1eb6c4ee466ba52123ac7ae074a6e921aeaf7d5 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 11 Apr 2026 13:23:53 -0600 Subject: [PATCH 02/23] cargo fmt --- src/read.rs | 5 ++++- src/write.rs | 26 ++++++++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/read.rs b/src/read.rs index b6674cd63..77dd8f47a 100644 --- a/src/read.rs +++ b/src/read.rs @@ -277,7 +277,10 @@ impl ZipArchive { /* Copy over file data from source archive directly. */ io::copy(&mut limited_raw, &mut w)?; - let new_files = new_files.into_iter().map(|f| (f.0.into_boxed_bytes(), f.1)).collect(); + let new_files = new_files + .into_iter() + .map(|f| (f.0.into_boxed_bytes(), f.1)) + .collect(); /* Return the files we've just written to the data stream. */ Ok(new_files) } diff --git a/src/write.rs b/src/write.rs index f2fbf393f..e05cd9980 100644 --- a/src/write.rs +++ b/src/write.rs @@ -835,7 +835,11 @@ impl ZipWriter { pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult> { readwriter.seek(SeekFrom::Start(0))?; let shared = ZipArchive::get_metadata(config, &mut readwriter)?; - let files = shared.files.into_iter().map(|f| (f.0.into_boxed_bytes(), f.1)).collect(); + let files = shared + .files + .into_iter() + .map(|f| (f.0.into_boxed_bytes(), f.1)) + .collect(); Ok(ZipWriter { inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), files: files, @@ -989,14 +993,14 @@ impl ZipWriter { let zip64_extensible_data_sector = mem::take(&mut self.zip64_extensible_data_sector); let files = mem::take(&mut self.files); let files: IndexMap, ZipFileData> = files - .into_iter() - .map(|f| { - let s = String::from_utf8(f.0.into_vec()) - .expect("invalid UTF-8") - .into_boxed_str(); - (s, f.1) - }) - .collect(); + .into_iter() + .map(|f| { + let s = String::from_utf8(f.0.into_vec()) + .expect("invalid UTF-8") + .into_boxed_str(); + (s, f.1) + }) + .collect(); Ok(ZipArchive::from_finalized_writer( files, comment, @@ -1391,7 +1395,9 @@ impl ZipWriter { if self.files.contains_key(file.file_name.as_bytes()) { return Err(invalid!("Duplicate filename: {}", file.file_name)); } - let (index, _) = self.files.insert_full(file.file_name.as_bytes().to_vec().into_boxed_slice(), file); + let (index, _) = self + .files + .insert_full(file.file_name.as_bytes().to_vec().into_boxed_slice(), file); Ok(index) } From 7f4e34093498ba165e880e7434e9a71dbc30b485 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 11 Apr 2026 16:35:12 -0600 Subject: [PATCH 03/23] fix --- src/write.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/write.rs b/src/write.rs index e05cd9980..c4c086ce4 100644 --- a/src/write.rs +++ b/src/write.rs @@ -842,7 +842,7 @@ impl ZipWriter { .collect(); Ok(ZipWriter { inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), - files: files, + files, stats: ZipWriterStats::default(), writing_to_file: false, comment: shared.comment, @@ -992,15 +992,13 @@ impl ZipWriter { let comment = mem::take(&mut self.comment); let zip64_extensible_data_sector = mem::take(&mut self.zip64_extensible_data_sector); let files = mem::take(&mut self.files); - let files: IndexMap, ZipFileData> = files + let files = files .into_iter() .map(|f| { - let s = String::from_utf8(f.0.into_vec()) - .expect("invalid UTF-8") - .into_boxed_str(); - (s, f.1) + let s = String::from_utf8(f.0.into_vec())?.into_boxed_str(); + Ok((s, f.1)) }) - .collect(); + .collect::>()?; Ok(ZipArchive::from_finalized_writer( files, comment, From f6ae0a32bb352c416721b7f18fbcf22552962a7a Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 11 Apr 2026 18:05:28 -0600 Subject: [PATCH 04/23] also on read --- src/read.rs | 12 ++++-------- src/read/stream.rs | 4 ++-- src/read/zip_archive.rs | 14 +++++++------- src/write.rs | 20 +++----------------- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/read.rs b/src/read.rs index 77dd8f47a..cb2c422bf 100644 --- a/src/read.rs +++ b/src/read.rs @@ -78,7 +78,7 @@ pub(crate) fn make_writable_dir_all>(outpath: T) -> Result<(), Zi pub(crate) fn make_symlink_impl( outpath: &Path, target_str: &str, - _existing_files: &IndexMap, T>, + _existing_files: &IndexMap, T>, ) -> ZipResult<()> { std::os::unix::fs::symlink(Path::new(&target_str), outpath)?; Ok(()) @@ -88,7 +88,7 @@ pub(crate) fn make_symlink_impl( pub(crate) fn make_symlink_impl( outpath: &Path, target_str: &str, - existing_files: &IndexMap, T>, + existing_files: &IndexMap, T>, ) -> ZipResult<()> { let target = Path::new(OsStr::new(&target_str)); let target_is_dir_from_archive = existing_files.contains_key(target_str) && is_dir(target_str); @@ -111,7 +111,7 @@ pub(crate) fn make_symlink_impl( pub(crate) fn make_symlink( outpath: &Path, target: &[u8], - #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, + #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, ) -> ZipResult<()> { let Ok(target_str) = std::str::from_utf8(target) else { return Err(invalid!("Invalid UTF-8 as symlink target")); @@ -123,7 +123,7 @@ pub(crate) fn make_symlink( pub(crate) fn make_symlink( outpath: &Path, target: &[u8], - #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, + #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, ) -> ZipResult<()> { let Ok(_) = std::str::from_utf8(target) else { return Err(invalid!("Invalid UTF-8 as symlink target")); @@ -277,10 +277,6 @@ impl ZipArchive { /* Copy over file data from source archive directly. */ io::copy(&mut limited_raw, &mut w)?; - let new_files = new_files - .into_iter() - .map(|f| (f.0.into_boxed_bytes(), f.1)) - .collect(); /* Return the files we've just written to the data stream. */ Ok(new_files) } diff --git a/src/read/stream.rs b/src/read/stream.rs index 66299e252..1a039455d 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -63,10 +63,10 @@ impl ZipStreamReader { /// Extraction is not atomic; If an error is encountered, some of the files /// may be left on disk. pub fn extract>(self, directory: P) -> ZipResult<()> { - struct Extractor(PathBuf, IndexMap, ()>); + struct Extractor(PathBuf, IndexMap, ()>); impl ZipStreamVisitor for Extractor { fn visit_file(&mut self, file: &mut ZipFile<'_, R>) -> ZipResult<()> { - self.1.insert(file.name().into(), ()); + self.1.insert(file.name_raw().into(), ()); let mut outpath = self.0.clone(); file.safe_prepare_path(&self.0, &mut outpath, None::<&(_, fn(&Path) -> bool)>)?; diff --git a/src/read/zip_archive.rs b/src/read/zip_archive.rs index 507c23ffd..101a256ef 100644 --- a/src/read/zip_archive.rs +++ b/src/read/zip_archive.rs @@ -20,7 +20,7 @@ use std::sync::Arc; /// Immutable metadata about a `ZipArchive`. #[derive(Debug)] pub struct ZipArchiveMetadata { - pub(crate) files: IndexMap, ZipFileData>, + pub(crate) files: IndexMap, ZipFileData>, pub(crate) offset: u64, pub(crate) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. @@ -48,7 +48,7 @@ impl SharedBuilder { ) -> ZipArchiveMetadata { let mut index_map = IndexMap::with_capacity(self.files.len()); self.files.into_iter().for_each(|file| { - index_map.insert(file.file_name.clone(), file); + index_map.insert(file.file_name_raw.clone(), file); }); ZipArchiveMetadata { files: index_map, @@ -90,7 +90,7 @@ pub struct ZipArchive { impl ZipArchive { pub(crate) fn from_finalized_writer( - files: IndexMap, ZipFileData>, + files: IndexMap, ZipFileData>, comment: Box<[u8]>, zip64_extensible_data_sector: Option>, reader: R, @@ -364,7 +364,7 @@ impl ZipArchive { /// Returns an iterator over all the file and directory names in this archive. pub fn file_names(&self) -> impl Iterator { - self.shared.files.keys().map(std::convert::AsRef::as_ref) + self.shared.files.values().map(|f| f.file_name.as_ref()) } /// Returns Ok(true) if any compressed data in this archive belongs to more than one file. This @@ -414,7 +414,7 @@ impl ZipArchive { /// Get the index of a file entry by name, if it's present. #[inline] pub fn index_for_name(&self, name: &str) -> Option { - self.shared.files.get_index_of(name) + self.shared.files.get_index_of(name.as_bytes()) } /// Search for a file entry by path, decrypt with given password @@ -462,7 +462,7 @@ impl ZipArchive { self.shared .files .get_index(index) - .map(|(name, _)| name.as_ref()) + .map(|(_, file)| file.file_name.as_ref()) } /// Search for a file entry by name and return a seekable object. @@ -500,7 +500,7 @@ impl ZipArchive { name: &str, password: Option<&[u8]>, ) -> ZipResult> { - let Some(index) = self.shared.files.get_index_of(name) else { + let Some(index) = self.shared.files.get_index_of(name.as_bytes()) else { return Err(ZipError::FileNotFound); }; self.by_index_with_options(index, ZipReadOptions::new().password(password)) diff --git a/src/write.rs b/src/write.rs index c4c086ce4..c8132baac 100644 --- a/src/write.rs +++ b/src/write.rs @@ -835,14 +835,9 @@ impl ZipWriter { pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult> { readwriter.seek(SeekFrom::Start(0))?; let shared = ZipArchive::get_metadata(config, &mut readwriter)?; - let files = shared - .files - .into_iter() - .map(|f| (f.0.into_boxed_bytes(), f.1)) - .collect(); Ok(ZipWriter { inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), - files, + files: shared.files, stats: ZipWriterStats::default(), writing_to_file: false, comment: shared.comment, @@ -992,13 +987,6 @@ impl ZipWriter { let comment = mem::take(&mut self.comment); let zip64_extensible_data_sector = mem::take(&mut self.zip64_extensible_data_sector); let files = mem::take(&mut self.files); - let files = files - .into_iter() - .map(|f| { - let s = String::from_utf8(f.0.into_vec())?.into_boxed_str(); - Ok((s, f.1)) - }) - .collect::>()?; Ok(ZipArchive::from_finalized_writer( files, comment, @@ -1390,12 +1378,10 @@ impl ZipWriter { } fn insert_file_data(&mut self, file: ZipFileData) -> ZipResult { - if self.files.contains_key(file.file_name.as_bytes()) { + if self.files.contains_key(&file.file_name_raw) { return Err(invalid!("Duplicate filename: {}", file.file_name)); } - let (index, _) = self - .files - .insert_full(file.file_name.as_bytes().to_vec().into_boxed_slice(), file); + let (index, _) = self.files.insert_full(file.file_name_raw.clone(), file); Ok(index) } From 3edd51f360e11ef6f1e62e36a00cfadeea8afe87 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 11 Apr 2026 19:26:47 -0600 Subject: [PATCH 05/23] add as bytes --- src/read.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/read.rs b/src/read.rs index cb2c422bf..d8c43b6e9 100644 --- a/src/read.rs +++ b/src/read.rs @@ -91,7 +91,7 @@ pub(crate) fn make_symlink_impl( existing_files: &IndexMap, T>, ) -> ZipResult<()> { let target = Path::new(OsStr::new(&target_str)); - let target_is_dir_from_archive = existing_files.contains_key(target_str) && is_dir(target_str); + let target_is_dir_from_archive = existing_files.contains_key(target_str.as_bytes()) && is_dir(target_str); let target_is_dir = if target_is_dir_from_archive { true } else if let Ok(meta) = std::fs::metadata(target) { From f717f24565d6ee0c089d6d45a2636a85435c2a88 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 11 Apr 2026 20:57:43 -0600 Subject: [PATCH 06/23] fmt --- src/read.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/read.rs b/src/read.rs index d8c43b6e9..885605b58 100644 --- a/src/read.rs +++ b/src/read.rs @@ -91,7 +91,8 @@ pub(crate) fn make_symlink_impl( existing_files: &IndexMap, T>, ) -> ZipResult<()> { let target = Path::new(OsStr::new(&target_str)); - let target_is_dir_from_archive = existing_files.contains_key(target_str.as_bytes()) && is_dir(target_str); + let target_is_dir_from_archive = + existing_files.contains_key(target_str.as_bytes()) && is_dir(target_str); let target_is_dir = if target_is_dir_from_archive { true } else if let Ok(meta) = std::fs::metadata(target) { From 93a93e3251fd3d46cfb88af24aca5d3b03e37b5b Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 15 Apr 2026 12:57:43 -0600 Subject: [PATCH 07/23] one alloc filename --- src/read.rs | 4 +- src/read/zip_archive.rs | 29 +++++---- src/types.rs | 129 ++++++++++++++++++++++++++++++++++++++-- src/write.rs | 9 ++- 4 files changed, 151 insertions(+), 20 deletions(-) diff --git a/src/read.rs b/src/read.rs index 885605b58..f738766c2 100644 --- a/src/read.rs +++ b/src/read.rs @@ -14,7 +14,7 @@ use crate::spec::{ CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, ZIP64_BYTES_THR, ZipCentralEntryBlock, ZipFlags, }; -use crate::types::{SimpleFileOptions, System, ZipFileData, ffi}; +use crate::types::{SimpleFileOptions, System, ZipFileData, ZipFileDataInner, ffi}; use crate::unstable::LittleEndianReadExt; use core::mem::replace; use indexmap::IndexMap; @@ -218,7 +218,7 @@ impl ZipArchive { pub(crate) fn merge_contents( &mut self, mut w: W, - ) -> ZipResult, ZipFileData>> { + ) -> ZipResult, ZipFileDataInner>> { if self.shared.files.is_empty() { return Ok(IndexMap::new()); } diff --git a/src/read/zip_archive.rs b/src/read/zip_archive.rs index 101a256ef..a58df2978 100644 --- a/src/read/zip_archive.rs +++ b/src/read/zip_archive.rs @@ -9,6 +9,7 @@ use crate::read::{ }; use crate::result::{ZipError, ZipResult}; use crate::spec; +use crate::types::ZipFileDataInner; use crate::types::ZipFileData; use crate::unstable::path_to_string; use core::ops::Range; @@ -20,7 +21,7 @@ use std::sync::Arc; /// Immutable metadata about a `ZipArchive`. #[derive(Debug)] pub struct ZipArchiveMetadata { - pub(crate) files: IndexMap, ZipFileData>, + pub(crate) files: IndexMap, ZipFileDataInner>, pub(crate) offset: u64, pub(crate) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. @@ -32,7 +33,7 @@ pub struct ZipArchiveMetadata { #[derive(Debug)] pub(crate) struct SharedBuilder { - pub(crate) files: Vec, + pub(crate) files: Vec, pub(super) offset: u64, pub(super) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. @@ -48,7 +49,7 @@ impl SharedBuilder { ) -> ZipArchiveMetadata { let mut index_map = IndexMap::with_capacity(self.files.len()); self.files.into_iter().for_each(|file| { - index_map.insert(file.file_name_raw.clone(), file); + index_map.insert(file.file_name_raw.clone(), file.into_inner()); }); ZipArchiveMetadata { files: index_map, @@ -100,6 +101,7 @@ impl ZipArchive { Some((_, file)) => file.header_start, None => central_start, }; + let files = files.into_iter().map(|(k,v)| (k, v.into_inner())).collect(); let shared = Arc::new(ZipArchiveMetadata { files, offset: initial_offset, @@ -225,12 +227,12 @@ impl ZipArchive { &mut self, file_number: usize, ) -> ZipResult> { - let (_, data) = self + let (file_name_raw, data) = self .shared .files .get_index(file_number) .ok_or(ZipError::FileNotFound)?; - + let data = data.into_zip_file_data(file_name_raw); let limit_reader = data.find_content(&mut self.reader)?; match data.aes_mode { None => Ok(None), @@ -372,7 +374,8 @@ impl ZipArchive { /// copies would take up space independently in the destination. pub fn has_overlapping_files(&mut self) -> ZipResult { let mut ranges = Vec::>::with_capacity(self.shared.files.len()); - for file in self.shared.files.values() { + for (file_name_raw, file) in &self.shared.files { + let file = file.into_zip_file_data(file_name_raw); if file.compressed_size == 0 { continue; } @@ -477,7 +480,8 @@ impl ZipArchive { .files .get_index(index) .ok_or(ZipError::FileNotFound) - .and_then(move |(_, data)| { + .and_then(move |(ff, data)| { + let data = data.into_zip_file_data(ff); let seek_reader = match data.compression_method { CompressionMethod::Stored => { ZipFileSeekReader::Raw(data.find_content_seek(reader)?) @@ -535,11 +539,12 @@ impl ZipArchive { /// Get a contained file by index without decompressing it pub fn by_index_raw(&mut self, file_number: usize) -> ZipResult> { let reader = &mut self.reader; - let (_, data) = self + let (file_name_raw, data) = self .shared .files .get_index(file_number) .ok_or(ZipError::FileNotFound)?; + let data = data.into_zip_file_data(file_name_raw); Ok(ZipFile { reader: ZipFileReader::Raw(data.find_content(reader)?), data: Cow::Borrowed(data), @@ -552,12 +557,12 @@ impl ZipArchive { file_number: usize, mut options: ZipReadOptions<'_>, ) -> ZipResult> { - let (_, data) = self + let (file_name_raw, data) = self .shared .files .get_index(file_number) .ok_or(ZipError::FileNotFound)?; - + let data = data.into_zip_file_data(file_name_raw); if options.ignore_encryption_flag { // Always use no password when we're ignoring the encryption flag. options.password = None; @@ -610,12 +615,14 @@ impl ZipArchive { let mut root_dir: Option = None; for i in 0..self.len() { - let (_, file) = self + let (filename, file) = self .shared .files .get_index(i) .ok_or(ZipError::FileNotFound)?; + let file = file.into_zip_file_data(filename); + let path = match file.enclosed_name() { Some(path) => path, None => return Ok(None), diff --git a/src/types.rs b/src/types.rs index 21f8a6001..788a54325 100644 --- a/src/types.rs +++ b/src/types.rs @@ -165,7 +165,97 @@ pub const DEFAULT_VERSION: u8 = 45; /// Structure representing a ZIP file. #[derive(Debug, Clone, Default)] -pub struct ZipFileData { +pub struct ZipFileDataInner { + /// Compatibility of the file attribute information + pub system: System, + /// Specification version + pub version_made_by: u8, + /// ZIP flags + pub flags: u16, + /// True if the file is encrypted. + pub encrypted: bool, + /// True if `file_name` and `file_comment` are UTF8 + pub is_utf8: bool, + /// True if the file uses a data-descriptor section + pub using_data_descriptor: bool, + /// Compression method used to store the file + pub compression_method: crate::compression::CompressionMethod, + /// Compression level to store the file + pub compression_level: Option, + /// Last modified time. This will only have a 2 second precision. + pub last_modified_time: Option, + /// CRC32 checksum + pub crc32: u32, + /// Size of the file in the ZIP + pub compressed_size: u64, + /// Size of the file when extracted + pub uncompressed_size: u64, + /// Name of the file + pub file_name: Box, + /// Extra field usually used for storage expansion + pub extra_field: Option>, + /// Extra field only written to central directory + pub central_extra_field: Option>, + /// File comment + pub file_comment: Box, + /// Specifies where the local header of the file starts + pub header_start: u64, + /// Specifies where the extra data of the file starts + pub extra_data_start: Option, + /// Specifies where the central header of the file starts + /// + /// Note that when this is not known, it is set to 0 + pub central_header_start: u64, + /// Specifies where the compressed data of the file starts + pub data_start: OnceLock, + /// External file attributes + pub external_attributes: u32, + /// Reserve local ZIP64 extra field + pub large_file: bool, + /// AES mode if applicable + pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>, + /// Specifies where in the extra data the AES metadata starts + pub aes_extra_data_start: u64, + /// extra fields, see + pub extra_fields: Vec, +} + +impl ZipFileDataInner { + pub fn into_zip_file_data<'a>(&'a self, file_name_raw: &'a Box<[u8]>) -> ZipFileData<'a> { + ZipFileData { + system: self.system, + version_made_by: self.version_made_by, + flags: self.flags, + encrypted: self.encrypted, + is_utf8: self.is_utf8, + using_data_descriptor: self.using_data_descriptor, + compression_method: self.compression_method, + compression_level: self.compression_level, + last_modified_time: self.last_modified_time, + crc32: self.crc32, + compressed_size: self.compressed_size, + uncompressed_size: self.uncompressed_size, + file_name: self.file_name, + file_name_raw, + extra_field: self.extra_field, + central_extra_field: self.central_extra_field, + file_comment: self.file_comment, + header_start: self.header_start, + extra_data_start: self.extra_data_start, + central_header_start: self.central_header_start, + data_start: self.data_start, + external_attributes: self.external_attributes, + large_file: self.large_file, + aes_mode: self.aes_mode, + aes_extra_data_start: self.aes_extra_data_start, + extra_fields: self.extra_fields, + } + } +} + +/// Structure representing a ZIP file. +#[derive(Debug, Clone, Default)] +pub struct ZipFileData<'a> { /// Compatibility of the file attribute information pub system: System, /// Specification version @@ -193,7 +283,7 @@ pub struct ZipFileData { /// Name of the file pub file_name: Box, /// Raw file name. To be used when `file_name` was incorrectly decoded. - pub file_name_raw: Box<[u8]>, + pub file_name_raw: &'a Box<[u8]>, /// Extra field usually used for storage expansion pub extra_field: Option>, /// Extra field only written to central directory @@ -224,6 +314,36 @@ pub struct ZipFileData { } impl ZipFileData { + pub fn into_inner(self) -> ZipFileDataInner { + ZipFileDataInner { + system: self.system, + version_made_by: self.version_made_by, + flags: self.flags, + encrypted: self.encrypted, + is_utf8: self.is_utf8, + using_data_descriptor: self.using_data_descriptor, + compression_method: self.compression_method, + compression_level: self.compression_level, + last_modified_time: self.last_modified_time, + crc32: self.crc32, + compressed_size: self.compressed_size, + uncompressed_size: self.uncompressed_size, + file_name: self.file_name, + file_comment: self.file_comment, + extra_field: self.extra_field, + central_extra_field: self.central_extra_field, + header_start: self.header_start, + extra_data_start: self.extra_data_start, + central_header_start: self.central_header_start, + data_start: self.data_start, + external_attributes: self.external_attributes, + large_file: self.large_file, + aes_mode: self.aes_mode, + aes_extra_data_start: self.aes_extra_data_start, + extra_fields: self.extra_fields, + } + } + /// Get the starting offset of the data of the compressed file pub fn data_start(&self, reader: &mut (impl Read + Seek + ?Sized)) -> ZipResult { match self.data_start.get() { @@ -404,7 +524,8 @@ impl ZipFileData { #[allow(clippy::too_many_arguments)] pub(crate) fn initialize_local_block( - name: &S, + file_name: Box, + file_name_raw: &Box<[u8]>, options: &FileOptions<'_, T>, raw_values: &ZipRawValues, header_start: u64, @@ -420,8 +541,6 @@ impl ZipFileData { let permissions = options .permissions .unwrap_or(FileOptions::DEFAULT_FILE_PERMISSION); - let file_name: Box = name.to_string().into_boxed_str(); - let file_name_raw: Box<[u8]> = file_name.as_bytes().into(); let mut external_attributes = permissions << 16; let system = if (permissions & ffi::S_IFLNK) == ffi::S_IFLNK { System::Unix diff --git a/src/write.rs b/src/write.rs index c8132baac..f70f95798 100644 --- a/src/write.rs +++ b/src/write.rs @@ -835,9 +835,10 @@ impl ZipWriter { pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult> { readwriter.seek(SeekFrom::Start(0))?; let shared = ZipArchive::get_metadata(config, &mut readwriter)?; + let files = shared.files.into_iter().map(|(k,v)| (k.clone(), v.into_zip_file_data(k))).collect(); Ok(ZipWriter { inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), - files: shared.files, + files, stats: ZipWriterStats::default(), writing_to_file: false, comment: shared.comment, @@ -1275,8 +1276,11 @@ impl ZipWriter { } #[cfg(feature = "aes-crypto")] let aes_mode = aes_mode.map(super::aes::AesModeOptions::to_tuple); + let file_name: Box = name.to_string().into_boxed_str(); + let file_name_raw: Box<[u8]> = file_name.as_bytes().into(); let mut file = ZipFileData::initialize_local_block( - name, + file_name, + &file_name_raw, &options, &raw_values, header_start, @@ -1575,6 +1579,7 @@ impl ZipWriter { /* Get the file entries from the source archive. */ let new_files = source.merge_contents(writer)?; + let new_files: IndexMap, ZipFileData> = new_files.into_iter().map(|(k,v)| (k, v.into_zip_file_data(&k))).collect(); /* These file entries are now ours! */ self.files.extend(new_files); From 9c5e5291a2879c269bbd71e07ba3e4a5eadc498d Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 20 Apr 2026 19:12:34 -0600 Subject: [PATCH 08/23] add stuff --- src/read.rs | 33 ++++----- src/read/stream.rs | 16 +++-- src/read/zip_archive.rs | 26 +++---- src/result.rs | 4 +- src/types.rs | 146 +++------------------------------------- src/write.rs | 49 +++++++------- 6 files changed, 71 insertions(+), 203 deletions(-) diff --git a/src/read.rs b/src/read.rs index f738766c2..f1be6a06a 100644 --- a/src/read.rs +++ b/src/read.rs @@ -14,7 +14,7 @@ use crate::spec::{ CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, ZIP64_BYTES_THR, ZipCentralEntryBlock, ZipFlags, }; -use crate::types::{SimpleFileOptions, System, ZipFileData, ZipFileDataInner, ffi}; +use crate::types::{SimpleFileOptions, System, ZipFileData, ffi}; use crate::unstable::LittleEndianReadExt; use core::mem::replace; use indexmap::IndexMap; @@ -47,6 +47,7 @@ pub use crate::aes::AesInfo; /// If your logic depends on the buffer being completely populated, use [`Self::read_exact()`] instead. It will continue reading until the entire buffer is filled or an error occurs. #[derive(Debug)] pub struct ZipFile<'a, R: Read + ?Sized> { + pub(crate) file_name_raw: Cow<'a, [u8]>, pub(crate) data: Cow<'a, ZipFileData>, pub(crate) reader: ZipFileReader<'a, R>, } @@ -218,7 +219,7 @@ impl ZipArchive { pub(crate) fn merge_contents( &mut self, mut w: W, - ) -> ZipResult, ZipFileDataInner>> { + ) -> ZipResult, ZipFileData>> { if self.shared.files.is_empty() { return Ok(IndexMap::new()); } @@ -455,13 +456,13 @@ impl ZipArchive { pub(crate) fn central_header_to_zip_file( reader: &mut R, central_directory: &CentralDirectoryInfo, -) -> ZipResult { +) -> ZipResult<(ZipFileData, Box<[u8]>)> { let central_header_start = reader.stream_position()?; // Parse central header let block = ZipCentralEntryBlock::parse(reader)?; - let file = central_header_to_zip_file_inner( + let (file, file_name_raw) = central_header_to_zip_file_inner( reader, central_directory.archive_offset, central_header_start, @@ -471,7 +472,7 @@ pub(crate) fn central_header_to_zip_file( let central_header_end = reader.stream_position()?; reader.seek(SeekFrom::Start(central_header_end))?; - Ok(file) + Ok((file, file_name_raw)) } #[inline] @@ -494,7 +495,7 @@ fn central_header_to_zip_file_inner( archive_offset: u64, central_header_start: u64, block: ZipCentralEntryBlock, -) -> ZipResult { +) -> ZipResult<(ZipFileData, Box<[u8]>)> { let ZipCentralEntryBlock { // magic, version_made_by, @@ -520,7 +521,7 @@ fn central_header_to_zip_file_inner( let is_utf8 = ZipFlags::matching(flags, ZipFlags::LanguageEncoding); let using_data_descriptor = ZipFlags::matching(flags, ZipFlags::UsingDataDescriptor); - let file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?; + let mut file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?; let extra_field = read_variable_length_byte_field(reader, extra_field_length as usize)?; let file_comment_raw = read_variable_length_byte_field(reader, file_comment_length as usize)?; let file_name: Box = if is_utf8 { @@ -550,7 +551,6 @@ fn central_header_to_zip_file_inner( uncompressed_size: uncompressed_size.into(), flags, file_name, - file_name_raw, extra_field: Some(Arc::from(extra_field)), central_extra_field: None, file_comment, @@ -564,7 +564,7 @@ fn central_header_to_zip_file_inner( aes_extra_data_start: 0, extra_fields: Vec::new(), }; - parse_extra_field(&mut result)?; + parse_extra_field(&mut result, &mut file_name_raw)?; let aes_enabled = result.compression_method == CompressionMethod::AES; if aes_enabled && result.aes_mode.is_none() { @@ -577,10 +577,10 @@ fn central_header_to_zip_file_inner( .checked_add(archive_offset) .ok_or(invalid!("Archive header is too large"))?; - Ok(result) + Ok((result, file_name_raw)) } -pub(crate) fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> { +pub(crate) fn parse_extra_field(file: &mut ZipFileData, file_name_raw: &mut [u8]) -> ZipResult<()> { let mut extra_field = file.extra_field.clone(); let mut central_extra_field = file.central_extra_field.clone(); for field_group in [&mut extra_field, &mut central_extra_field] { @@ -595,7 +595,7 @@ pub(crate) fn parse_extra_field(file: &mut ZipFileData) -> ZipResult<()> { let mut position = reader.position(); while position < len as u64 { let old_position = position; - let remove = parse_single_extra_field(file, &mut reader, position, false)?; + let remove = parse_single_extra_field(file, &mut reader, position, false, file_name_raw)?; position = reader.position(); if remove { modified = true; @@ -628,6 +628,7 @@ pub(crate) fn parse_single_extra_field( reader: &mut R, bytes_already_read: u64, disallow_zip64: bool, + file_name_raw: &mut [u8], ) -> ZipResult { let kind = match reader.read_u16_le() { Ok(kind) => kind, @@ -705,10 +706,10 @@ pub(crate) fn parse_single_extra_field( Ok(UsedExtraField::UnicodePath) => { // Info-ZIP Unicode Path Extra Field // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt - file.file_name_raw = UnicodeExtraField::try_from_reader(reader, len)? - .unwrap_valid(&file.file_name_raw)?; + file_name_raw = &mut UnicodeExtraField::try_from_reader(reader, len)? + .unwrap_valid(&file_name_raw)?; file.file_name = - String::from_utf8(file.file_name_raw.clone().into_vec())?.into_boxed_str(); + std::str::from_utf8(file_name_raw)?.into_boxed_str(); file.is_utf8 = true; } _ => { @@ -806,7 +807,7 @@ impl<'a, R: Read + ?Sized> ZipFile<'a, R> { /// /// The encoding of this data is currently undefined. pub fn name_raw(&self) -> &[u8] { - &self.get_metadata().file_name_raw + &self.file_name_raw } /// Get the name of the file in a sanitized form. It truncates the name to the first NULL byte, diff --git a/src/read/stream.rs b/src/read/stream.rs index 1a039455d..07445db5c 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -34,13 +34,13 @@ impl ZipStreamReader { // Parse central header let block = ZipCentralEntryBlock::parse(&mut self.0)?; - let file = central_header_to_zip_file_inner( + let (file, file_name_raw) = central_header_to_zip_file_inner( &mut self.0, archive_offset, central_header_start, block, )?; - Ok(ZipStreamFileMetadata(file)) + Ok(ZipStreamFileMetadata(file, file_name_raw)) } /// Iterate over the stream and extract all file and their @@ -135,7 +135,7 @@ pub trait ZipStreamVisitor { /// Additional metadata for the file. #[derive(Debug)] -pub struct ZipStreamFileMetadata(ZipFileData); +pub struct ZipStreamFileMetadata(ZipFileData, Box<[u8]>); impl ZipStreamFileMetadata { /// Get the name of the file @@ -158,7 +158,7 @@ impl ZipStreamFileMetadata { /// /// The encoding of this data is currently undefined. pub fn name_raw(&self) -> &[u8] { - &self.0.file_name_raw + &self.1 } /// Rewrite the path, ignoring any path components with special meaning. @@ -249,9 +249,9 @@ pub fn read_zipfile_from_stream(reader: &mut R) -> ZipResult {} Err(e) => return Err(e), } @@ -268,6 +268,7 @@ pub fn read_zipfile_from_stream(reader: &mut R) -> ZipResult( let block = block.from_le(); - let mut result = ZipFileData::from_local_block(block, reader)?; + let (mut result, file_name_raw) = ZipFileData::from_local_block(block, reader)?; result.compressed_size = compressed_size; if result.encrypted { @@ -321,6 +322,7 @@ pub fn read_zipfile_from_stream_with_compressed_size( } = result; Ok(Some(ZipFile { + file_name_raw: Cow::Owned(file_name_raw), data: Cow::Owned(result), reader: make_reader( compression_method, diff --git a/src/read/zip_archive.rs b/src/read/zip_archive.rs index a58df2978..9280c32ad 100644 --- a/src/read/zip_archive.rs +++ b/src/read/zip_archive.rs @@ -9,7 +9,6 @@ use crate::read::{ }; use crate::result::{ZipError, ZipResult}; use crate::spec; -use crate::types::ZipFileDataInner; use crate::types::ZipFileData; use crate::unstable::path_to_string; use core::ops::Range; @@ -21,7 +20,7 @@ use std::sync::Arc; /// Immutable metadata about a `ZipArchive`. #[derive(Debug)] pub struct ZipArchiveMetadata { - pub(crate) files: IndexMap, ZipFileDataInner>, + pub(crate) files: IndexMap, ZipFileData>, pub(crate) offset: u64, pub(crate) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. @@ -33,7 +32,7 @@ pub struct ZipArchiveMetadata { #[derive(Debug)] pub(crate) struct SharedBuilder { - pub(crate) files: Vec, + pub(crate) files: Vec<(Box<[u8]>, ZipFileData)>, pub(super) offset: u64, pub(super) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. @@ -48,8 +47,8 @@ impl SharedBuilder { zip64_extensible_data_sector: Option>, ) -> ZipArchiveMetadata { let mut index_map = IndexMap::with_capacity(self.files.len()); - self.files.into_iter().for_each(|file| { - index_map.insert(file.file_name_raw.clone(), file.into_inner()); + self.files.into_iter().for_each(|(file_name_raw, file)| { + index_map.insert(file_name_raw, file); }); ZipArchiveMetadata { files: index_map, @@ -101,7 +100,7 @@ impl ZipArchive { Some((_, file)) => file.header_start, None => central_start, }; - let files = files.into_iter().map(|(k,v)| (k, v.into_inner())).collect(); + // let files = files.into_iter().map(|(k,v)| (k, v.into_inner())).collect(); let shared = Arc::new(ZipArchiveMetadata { files, offset: initial_offset, @@ -203,8 +202,8 @@ impl ZipArchive { let mut files = Vec::with_capacity(file_capacity); reader.seek(SeekFrom::Start(dir_info.directory_start))?; for _ in 0..dir_info.number_of_files { - let file = central_header_to_zip_file(reader, dir_info)?; - files.push(file); + let (file, file_name_raw) = central_header_to_zip_file(reader, dir_info)?; + files.push((file_name_raw, file)); } Ok(SharedBuilder { @@ -232,7 +231,6 @@ impl ZipArchive { .files .get_index(file_number) .ok_or(ZipError::FileNotFound)?; - let data = data.into_zip_file_data(file_name_raw); let limit_reader = data.find_content(&mut self.reader)?; match data.aes_mode { None => Ok(None), @@ -375,7 +373,6 @@ impl ZipArchive { pub fn has_overlapping_files(&mut self) -> ZipResult { let mut ranges = Vec::>::with_capacity(self.shared.files.len()); for (file_name_raw, file) in &self.shared.files { - let file = file.into_zip_file_data(file_name_raw); if file.compressed_size == 0 { continue; } @@ -480,8 +477,7 @@ impl ZipArchive { .files .get_index(index) .ok_or(ZipError::FileNotFound) - .and_then(move |(ff, data)| { - let data = data.into_zip_file_data(ff); + .and_then(move |(file_name_raw, data)| { let seek_reader = match data.compression_method { CompressionMethod::Stored => { ZipFileSeekReader::Raw(data.find_content_seek(reader)?) @@ -544,8 +540,8 @@ impl ZipArchive { .files .get_index(file_number) .ok_or(ZipError::FileNotFound)?; - let data = data.into_zip_file_data(file_name_raw); Ok(ZipFile { + file_name_raw: Cow::Borrowed(file_name_raw), reader: ZipFileReader::Raw(data.find_content(reader)?), data: Cow::Borrowed(data), }) @@ -562,7 +558,6 @@ impl ZipArchive { .files .get_index(file_number) .ok_or(ZipError::FileNotFound)?; - let data = data.into_zip_file_data(file_name_raw); if options.ignore_encryption_flag { // Always use no password when we're ignoring the encryption flag. options.password = None; @@ -589,6 +584,7 @@ impl ZipArchive { }; Ok(ZipFile { + file_name_raw: Cow::Borrowed(file_name_raw), data: Cow::Borrowed(data), reader: make_reader( data.compression_method, @@ -621,8 +617,6 @@ impl ZipArchive { .get_index(i) .ok_or(ZipError::FileNotFound)?; - let file = file.into_zip_file_data(filename); - let path = match file.enclosed_name() { Some(path) => path, None => return Ok(None), diff --git a/src/result.rs b/src/result.rs index c5356517c..e602dbd9b 100644 --- a/src/result.rs +++ b/src/result.rs @@ -94,8 +94,8 @@ impl From for ZipError { } } -impl From for ZipError { - fn from(_: std::string::FromUtf8Error) -> Self { +impl From for ZipError { + fn from(_: std::str::Utf8Error) -> Self { invalid!("Invalid UTF-8") } } diff --git a/src/types.rs b/src/types.rs index 788a54325..ddbaea752 100644 --- a/src/types.rs +++ b/src/types.rs @@ -165,7 +165,7 @@ pub const DEFAULT_VERSION: u8 = 45; /// Structure representing a ZIP file. #[derive(Debug, Clone, Default)] -pub struct ZipFileDataInner { +pub struct ZipFileData { /// Compatibility of the file attribute information pub system: System, /// Specification version @@ -216,134 +216,12 @@ pub struct ZipFileDataInner { pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>, /// Specifies where in the extra data the AES metadata starts pub aes_extra_data_start: u64, - /// extra fields, see - pub extra_fields: Vec, -} - -impl ZipFileDataInner { - pub fn into_zip_file_data<'a>(&'a self, file_name_raw: &'a Box<[u8]>) -> ZipFileData<'a> { - ZipFileData { - system: self.system, - version_made_by: self.version_made_by, - flags: self.flags, - encrypted: self.encrypted, - is_utf8: self.is_utf8, - using_data_descriptor: self.using_data_descriptor, - compression_method: self.compression_method, - compression_level: self.compression_level, - last_modified_time: self.last_modified_time, - crc32: self.crc32, - compressed_size: self.compressed_size, - uncompressed_size: self.uncompressed_size, - file_name: self.file_name, - file_name_raw, - extra_field: self.extra_field, - central_extra_field: self.central_extra_field, - file_comment: self.file_comment, - header_start: self.header_start, - extra_data_start: self.extra_data_start, - central_header_start: self.central_header_start, - data_start: self.data_start, - external_attributes: self.external_attributes, - large_file: self.large_file, - aes_mode: self.aes_mode, - aes_extra_data_start: self.aes_extra_data_start, - extra_fields: self.extra_fields, - } - } -} - -/// Structure representing a ZIP file. -#[derive(Debug, Clone, Default)] -pub struct ZipFileData<'a> { - /// Compatibility of the file attribute information - pub system: System, - /// Specification version - pub version_made_by: u8, - /// ZIP flags - pub flags: u16, - /// True if the file is encrypted. - pub encrypted: bool, - /// True if `file_name` and `file_comment` are UTF8 - pub is_utf8: bool, - /// True if the file uses a data-descriptor section - pub using_data_descriptor: bool, - /// Compression method used to store the file - pub compression_method: crate::compression::CompressionMethod, - /// Compression level to store the file - pub compression_level: Option, - /// Last modified time. This will only have a 2 second precision. - pub last_modified_time: Option, - /// CRC32 checksum - pub crc32: u32, - /// Size of the file in the ZIP - pub compressed_size: u64, - /// Size of the file when extracted - pub uncompressed_size: u64, - /// Name of the file - pub file_name: Box, - /// Raw file name. To be used when `file_name` was incorrectly decoded. - pub file_name_raw: &'a Box<[u8]>, - /// Extra field usually used for storage expansion - pub extra_field: Option>, - /// Extra field only written to central directory - pub central_extra_field: Option>, - /// File comment - pub file_comment: Box, - /// Specifies where the local header of the file starts - pub header_start: u64, - /// Specifies where the extra data of the file starts - pub extra_data_start: Option, - /// Specifies where the central header of the file starts - /// - /// Note that when this is not known, it is set to 0 - pub central_header_start: u64, - /// Specifies where the compressed data of the file starts - pub data_start: OnceLock, - /// External file attributes - pub external_attributes: u32, - /// Reserve local ZIP64 extra field - pub large_file: bool, - /// AES mode if applicable - pub aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>, - /// Specifies where in the extra data the AES metadata starts - pub aes_extra_data_start: u64, /// extra fields, see pub extra_fields: Vec, } impl ZipFileData { - pub fn into_inner(self) -> ZipFileDataInner { - ZipFileDataInner { - system: self.system, - version_made_by: self.version_made_by, - flags: self.flags, - encrypted: self.encrypted, - is_utf8: self.is_utf8, - using_data_descriptor: self.using_data_descriptor, - compression_method: self.compression_method, - compression_level: self.compression_level, - last_modified_time: self.last_modified_time, - crc32: self.crc32, - compressed_size: self.compressed_size, - uncompressed_size: self.uncompressed_size, - file_name: self.file_name, - file_comment: self.file_comment, - extra_field: self.extra_field, - central_extra_field: self.central_extra_field, - header_start: self.header_start, - extra_data_start: self.extra_data_start, - central_header_start: self.central_header_start, - data_start: self.data_start, - external_attributes: self.external_attributes, - large_file: self.large_file, - aes_mode: self.aes_mode, - aes_extra_data_start: self.aes_extra_data_start, - extra_fields: self.extra_fields, - } - } - /// Get the starting offset of the data of the compressed file pub fn data_start(&self, reader: &mut (impl Read + Seek + ?Sized)) -> ZipResult { match self.data_start.get() { @@ -525,7 +403,6 @@ impl ZipFileData { #[allow(clippy::too_many_arguments)] pub(crate) fn initialize_local_block( file_name: Box, - file_name_raw: &Box<[u8]>, options: &FileOptions<'_, T>, raw_values: &ZipRawValues, header_start: u64, @@ -582,7 +459,6 @@ impl ZipFileData { compressed_size: raw_values.compressed_size, uncompressed_size: raw_values.uncompressed_size, file_name, // Never used for saving, but used as map key in insert_file_data() - file_name_raw, extra_field: Some(Arc::from(extra_field)), central_extra_field: options .extended_options @@ -606,7 +482,7 @@ impl ZipFileData { pub(crate) fn from_local_block( block: ZipLocalEntryBlock, reader: &mut R, - ) -> ZipResult { + ) -> ZipResult<(Self, Vec)> { let ZipLocalEntryBlock { version_made_by, flags, @@ -666,7 +542,7 @@ impl ZipFileData { }; let (version_made_by, system) = System::extract_bytes(version_made_by); - Ok(ZipFileData { + let data = ZipFileData { system, version_made_by, flags, @@ -680,7 +556,6 @@ impl ZipFileData { compressed_size: compressed_size.into(), uncompressed_size: uncompressed_size.into(), file_name, - file_name_raw: file_name_raw.into(), extra_field: Some(Arc::from(extra_field.into_boxed_slice())), central_extra_field: None, file_comment: String::with_capacity(0).into_boxed_str(), // file comment is only available in the central directory @@ -698,7 +573,8 @@ impl ZipFileData { extra_fields: Vec::new(), extra_data_start: None, aes_extra_data_start: 0, - }) + }; + Ok((data, file_name_raw)) } fn is_utf8(&self) -> bool { @@ -706,7 +582,7 @@ impl ZipFileData { } fn is_ascii(&self) -> bool { - self.file_name_raw.is_ascii() && self.file_comment.is_ascii() + self.file_name.is_ascii() && self.file_comment.is_ascii() } fn flags(&self) -> u16 { @@ -738,7 +614,7 @@ impl ZipFileData { } } - pub(crate) fn local_block(&self) -> ZipResult { + pub(crate) fn local_block(&self, file_name_raw: &[u8]) -> ZipResult { let (compressed_size, uncompressed_size) = if self.using_data_descriptor { (0, 0) } else { @@ -764,8 +640,7 @@ impl ZipFileData { crc32: self.crc32, compressed_size, uncompressed_size, - file_name_length: self - .file_name_raw + file_name_length: file_name_raw .len() .try_into() .map_err(std::io::Error::other)?, @@ -773,7 +648,7 @@ impl ZipFileData { }) } - pub(crate) fn block(&self) -> ZipResult { + pub(crate) fn block(&self, file_name_raw: &[u8]) -> ZipResult { let compressed_size = if self.large_file { spec::ZIP64_BYTES_THR as u32 } else { @@ -818,8 +693,7 @@ impl ZipFileData { crc32: self.crc32, compressed_size, uncompressed_size, - file_name_length: self - .file_name_raw + file_name_length: file_name_raw .len() .try_into() .map_err(std::io::Error::other)?, diff --git a/src/write.rs b/src/write.rs index f70f95798..7fa09ef4d 100644 --- a/src/write.rs +++ b/src/write.rs @@ -399,7 +399,7 @@ impl ExtendedFileOptions { Ok(()) } - fn validate_extra_data(data: &[u8], disallow_zip64: bool) -> ZipResult<()> { + fn validate_extra_data(data: &[u8], disallow_zip64: bool, file_name_raw: &mut [u8]) -> ZipResult<()> { let len = data.len() as u64; if len == 0 { return Ok(()); @@ -435,7 +435,7 @@ impl ExtendedFileOptions { } data.seek(SeekFrom::Current(-2))?; } - parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, disallow_zip64)?; + parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, disallow_zip64, file_name_raw)?; pos = data.position(); } Ok(()) @@ -835,10 +835,9 @@ impl ZipWriter { pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult> { readwriter.seek(SeekFrom::Start(0))?; let shared = ZipArchive::get_metadata(config, &mut readwriter)?; - let files = shared.files.into_iter().map(|(k,v)| (k.clone(), v.into_zip_file_data(k))).collect(); Ok(ZipWriter { inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), - files, + files: shared.files, stats: ZipWriterStats::default(), writing_to_file: false, comment: shared.comment, @@ -894,12 +893,12 @@ impl ZipWriter { .try_inner_mut()? .seek(SeekFrom::Start(write_position))?; let mut new_data = src_data.clone(); - new_data.file_name_raw = dest_name.as_bytes().into(); + let file_name_raw = dest_name.as_bytes().into(); new_data.file_name = dest_name.into(); new_data.header_start = write_position; let extra_data_start = write_position + (size_of::() + size_of::()) as u64 - + new_data.file_name_raw.len() as u64; + + file_name_raw.len() as u64; new_data.extra_data_start = Some(extra_data_start); if let Some(extra) = &src_data.extra_field { let stripped = strip_alignment_extra_field(extra, false); @@ -917,13 +916,13 @@ impl ZipWriter { new_data.data_start.take(); new_data.data_start.get_or_init(|| data_start); new_data.central_header_start = 0; - let block = new_data.local_block()?; - let index = self.insert_file_data(new_data)?; + let block = new_data.local_block(file_name_raw)?; + let index = self.insert_file_data(new_data, file_name_raw)?; let new_data = &self.files[index]; let result: io::Result<()> = { let plain_writer = self.inner.try_inner_mut()?; block.write(plain_writer)?; - plain_writer.write_all(&new_data.file_name_raw)?; + plain_writer.write_all(&file_name_raw)?; if let Some(data) = &new_data.extra_field { plain_writer.write_all(data)?; } @@ -1280,7 +1279,6 @@ impl ZipWriter { let file_name_raw: Box<[u8]> = file_name.as_bytes().into(); let mut file = ZipFileData::initialize_local_block( file_name, - &file_name_raw, &options, &raw_values, header_start, @@ -1303,16 +1301,16 @@ impl ZipWriter { !self.seek_possible || matches!(options.encrypt_with, Some(EncryptWith::ZipCrypto(..))); file.version_made_by = file.version_made_by.max(file.version_needed() as u8); file.extra_data_start = Some(header_end); - let index = self.insert_file_data(file)?; + let index = self.insert_file_data(file, &file_name_raw)?; self.writing_to_file = true; let result: ZipResult<()> = { - ExtendedFileOptions::validate_extra_data(&extra_data, false)?; + ExtendedFileOptions::validate_extra_data(&extra_data, false, &mut file_name_raw)?; let file = &mut self.files[index]; - let block = file.local_block()?; + let block = file.local_block(&file_name_raw)?; let writer = self.inner.try_inner_mut()?; block.write(writer)?; // file name - writer.write_all(&file.file_name_raw)?; + writer.write_all(&file_name_raw)?; if extra_data_len > 0 { writer.write_all(&extra_data)?; file.extra_field = Some(Arc::from(extra_data.into_boxed_slice())); @@ -1381,11 +1379,11 @@ impl ZipWriter { Ok(()) } - fn insert_file_data(&mut self, file: ZipFileData) -> ZipResult { - if self.files.contains_key(&file.file_name_raw) { + fn insert_file_data(&mut self, file: ZipFileData, file_name_raw: &[u8]) -> ZipResult { + if self.files.contains_key(file_name_raw) { return Err(invalid!("Duplicate filename: {}", file.file_name)); } - let (index, _) = self.files.insert_full(file.file_name_raw.clone(), file); + let (index, _) = self.files.insert_full(file_name_raw.into(), file); Ok(index) } @@ -1579,7 +1577,6 @@ impl ZipWriter { /* Get the file entries from the source archive. */ let new_files = source.merge_contents(writer)?; - let new_files: IndexMap, ZipFileData> = new_files.into_iter().map(|(k,v)| (k, v.into_zip_file_data(&k))).collect(); /* These file entries are now ours! */ self.files.extend(new_files); @@ -1903,8 +1900,8 @@ impl ZipWriter { let mut version_needed = u16::from(MIN_VERSION); let central_start = writer.stream_position()?; - for file in self.files.values() { - file.write_central_directory_header(writer)?; + for (filename_raw, file) in &self.files { + file.write_central_directory_header(writer, &filename_raw)?; version_needed = version_needed.max(file.version_needed()); } let central_size = writer.stream_position()? - central_start; @@ -1981,9 +1978,9 @@ impl ZipWriter { let src_index = self.index_by_name(src_name.as_bytes())?; let mut dest_data = self.files[src_index].clone(); dest_data.file_name = dest_name.into(); - dest_data.file_name_raw = dest_name.as_bytes().into(); + let file_name_raw = dest_name.as_bytes().into(); dest_data.central_header_start = 0; - self.insert_file_data(dest_data)?; + self.insert_file_data(dest_data, file_name_raw)?; Ok(()) } @@ -2483,7 +2480,7 @@ impl ZipFileData { let zip64_extra_field_start = self.header_start + (size_of::() + size_of::()) as u64 - + self.file_name_raw.len() as u64; + + self.file_name.as_bytes().len() as u64; writer.seek(SeekFrom::Start(zip64_extra_field_start))?; let zip64_block = zip64_block.serialize(); @@ -2491,8 +2488,8 @@ impl ZipFileData { Ok(()) } - pub(crate) fn write_central_directory_header(&self, writer: &mut T) -> ZipResult<()> { - let mut block = self.block()?; + pub(crate) fn write_central_directory_header(&self, writer: &mut T, file_name_raw: &[u8]) -> ZipResult<()> { + let mut block = self.block(file_name_raw)?; let stripped_extra = if let Some(extra) = &self.extra_field { strip_alignment_extra_field(extra, true) } else { @@ -2516,7 +2513,7 @@ impl ZipFileData { block.write(writer)?; // file name - writer.write_all(&self.file_name_raw)?; + writer.write_all(file_name_raw)?; // extra field if let Some(zip64_extra_field) = zip64_extra_field_block { writer.write_all(&zip64_extra_field.serialize())?; From 9dc94f808f212b31dd57d5b2804dc5d34b0f0f6c Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 20 Apr 2026 19:53:46 -0600 Subject: [PATCH 09/23] remove file_name_raw --- src/read.rs | 22 ++++++++++++---------- src/read/zip_archive.rs | 8 ++++---- src/result.rs | 12 ++++++------ src/types.rs | 18 ++++++------------ src/write.rs | 25 +++++++++++++++++++------ 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/read.rs b/src/read.rs index f1be6a06a..23aa8d477 100644 --- a/src/read.rs +++ b/src/read.rs @@ -476,8 +476,8 @@ pub(crate) fn central_header_to_zip_file( } #[inline] -fn read_variable_length_byte_field(reader: &mut R, len: usize) -> ZipResult> { - let mut data = vec![0; len].into_boxed_slice(); +fn read_variable_length_byte_field(reader: &mut R, len: usize) -> ZipResult> { + let mut data = vec![0; len]; if let Err(e) = reader.read_exact(&mut data) { if e.kind() == io::ErrorKind::UnexpectedEof { return Err(invalid!( @@ -577,10 +577,10 @@ fn central_header_to_zip_file_inner( .checked_add(archive_offset) .ok_or(invalid!("Archive header is too large"))?; - Ok((result, file_name_raw)) + Ok((result, file_name_raw.into())) } -pub(crate) fn parse_extra_field(file: &mut ZipFileData, file_name_raw: &mut [u8]) -> ZipResult<()> { +pub(crate) fn parse_extra_field(file: &mut ZipFileData, file_name_raw: &mut Vec) -> ZipResult<()> { let mut extra_field = file.extra_field.clone(); let mut central_extra_field = file.central_extra_field.clone(); for field_group in [&mut extra_field, &mut central_extra_field] { @@ -595,7 +595,8 @@ pub(crate) fn parse_extra_field(file: &mut ZipFileData, file_name_raw: &mut [u8] let mut position = reader.position(); while position < len as u64 { let old_position = position; - let remove = parse_single_extra_field(file, &mut reader, position, false, file_name_raw)?; + let remove = + parse_single_extra_field(file, &mut reader, position, false, file_name_raw)?; position = reader.position(); if remove { modified = true; @@ -628,7 +629,7 @@ pub(crate) fn parse_single_extra_field( reader: &mut R, bytes_already_read: u64, disallow_zip64: bool, - file_name_raw: &mut [u8], + file_name_raw: &mut Vec, ) -> ZipResult { let kind = match reader.read_u16_le() { Ok(kind) => kind, @@ -706,10 +707,11 @@ pub(crate) fn parse_single_extra_field( Ok(UsedExtraField::UnicodePath) => { // Info-ZIP Unicode Path Extra Field // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt - file_name_raw = &mut UnicodeExtraField::try_from_reader(reader, len)? - .unwrap_valid(&file_name_raw)?; - file.file_name = - std::str::from_utf8(file_name_raw)?.into_boxed_str(); + let unicode = UnicodeExtraField::try_from_reader(reader, len)?; + let file_name = unicode.unwrap_valid(&file_name_raw)?; + file_name_raw.clear(); + file_name_raw.extend_from_slice(&file_name); + file.file_name = String::from_utf8(file_name_raw.to_vec())?.into_boxed_str(); file.is_utf8 = true; } _ => { diff --git a/src/read/zip_archive.rs b/src/read/zip_archive.rs index 9280c32ad..4e59f49cb 100644 --- a/src/read/zip_archive.rs +++ b/src/read/zip_archive.rs @@ -226,7 +226,7 @@ impl ZipArchive { &mut self, file_number: usize, ) -> ZipResult> { - let (file_name_raw, data) = self + let (_file_name_raw, data) = self .shared .files .get_index(file_number) @@ -372,7 +372,7 @@ impl ZipArchive { /// copies would take up space independently in the destination. pub fn has_overlapping_files(&mut self) -> ZipResult { let mut ranges = Vec::>::with_capacity(self.shared.files.len()); - for (file_name_raw, file) in &self.shared.files { + for (_file_name_raw, file) in &self.shared.files { if file.compressed_size == 0 { continue; } @@ -477,7 +477,7 @@ impl ZipArchive { .files .get_index(index) .ok_or(ZipError::FileNotFound) - .and_then(move |(file_name_raw, data)| { + .and_then(move |(_file_name_raw, data)| { let seek_reader = match data.compression_method { CompressionMethod::Stored => { ZipFileSeekReader::Raw(data.find_content_seek(reader)?) @@ -611,7 +611,7 @@ impl ZipArchive { let mut root_dir: Option = None; for i in 0..self.len() { - let (filename, file) = self + let (_filename, file) = self .shared .files .get_index(i) diff --git a/src/result.rs b/src/result.rs index e602dbd9b..47adecb18 100644 --- a/src/result.rs +++ b/src/result.rs @@ -94,12 +94,6 @@ impl From for ZipError { } } -impl From for ZipError { - fn from(_: std::str::Utf8Error) -> Self { - invalid!("Invalid UTF-8") - } -} - /// Error type for time parsing #[derive(Debug)] pub struct DateTimeRangeError; @@ -111,6 +105,12 @@ impl From for DateTimeRangeError { } } +impl From for ZipError { + fn from(_: std::string::FromUtf8Error) -> Self { + invalid!("Invalid UTF-8") + } +} + impl fmt::Display for DateTimeRangeError { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!( diff --git a/src/types.rs b/src/types.rs index ddbaea752..8ecb9e5d3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -401,7 +401,7 @@ impl ZipFileData { } #[allow(clippy::too_many_arguments)] - pub(crate) fn initialize_local_block( + pub(crate) fn initialize_local_block( file_name: Box, options: &FileOptions<'_, T>, raw_values: &ZipRawValues, @@ -412,8 +412,6 @@ impl ZipFileData { aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>, extra_field: &[u8], ) -> Self - where - S: ToString, { let permissions = options .permissions @@ -577,16 +575,13 @@ impl ZipFileData { Ok((data, file_name_raw)) } - fn is_utf8(&self) -> bool { - std::str::from_utf8(&self.file_name_raw).is_ok() - } - fn is_ascii(&self) -> bool { self.file_name.is_ascii() && self.file_comment.is_ascii() } - fn flags(&self) -> u16 { - let utf8_bit: u16 = if self.is_utf8() && !self.is_ascii() { + fn flags(&self, file_name_raw: &[u8]) -> u16 { + let is_utf8 = std::str::from_utf8(file_name_raw).is_ok(); + let utf8_bit: u16 = if is_utf8 && !self.is_ascii() { ZipFlags::LanguageEncoding.as_u16() } else { 0 @@ -633,7 +628,7 @@ impl ZipFileData { .unwrap_or_else(DateTime::default_for_write); Ok(ZipLocalEntryBlock { version_made_by: self.version_needed(), - flags: self.flags(), + flags: self.flags(file_name_raw), compression_method: self.compression_method.serialize_to_u16(), last_mod_time: last_modified_time.timepart(), last_mod_date: last_modified_time.datepart(), @@ -686,7 +681,7 @@ impl ZipFileData { Ok(ZipCentralEntryBlock { version_made_by: ((self.system as u16) << 8) | version_made_by, version_to_extract, - flags: self.flags(), + flags: self.flags(file_name_raw), compression_method: self.compression_method.serialize_to_u16(), last_mod_time: last_modified_time.timepart(), last_mod_date: last_modified_time.datepart(), @@ -906,7 +901,6 @@ mod tests { compressed_size: 0, uncompressed_size: 0, file_name: file_name.clone().into_boxed_str(), - file_name_raw: file_name.into_bytes().into_boxed_slice(), extra_field: None, central_extra_field: None, file_comment: String::with_capacity(0).into_boxed_str(), diff --git a/src/write.rs b/src/write.rs index 7fa09ef4d..010122df7 100644 --- a/src/write.rs +++ b/src/write.rs @@ -399,7 +399,10 @@ impl ExtendedFileOptions { Ok(()) } - fn validate_extra_data(data: &[u8], disallow_zip64: bool, file_name_raw: &mut [u8]) -> ZipResult<()> { + fn validate_extra_data( + data: &[u8], + disallow_zip64: bool, + ) -> ZipResult<()> { let len = data.len() as u64; if len == 0 { return Ok(()); @@ -435,7 +438,13 @@ impl ExtendedFileOptions { } data.seek(SeekFrom::Current(-2))?; } - parse_single_extra_field(&mut ZipFileData::default(), &mut data, pos, disallow_zip64, file_name_raw)?; + parse_single_extra_field( + &mut ZipFileData::default(), + &mut data, + pos, + disallow_zip64, + &mut Vec::new(), + )?; pos = data.position(); } Ok(()) @@ -893,7 +902,7 @@ impl ZipWriter { .try_inner_mut()? .seek(SeekFrom::Start(write_position))?; let mut new_data = src_data.clone(); - let file_name_raw = dest_name.as_bytes().into(); + let file_name_raw = dest_name.as_bytes(); new_data.file_name = dest_name.into(); new_data.header_start = write_position; let extra_data_start = write_position @@ -1276,7 +1285,7 @@ impl ZipWriter { #[cfg(feature = "aes-crypto")] let aes_mode = aes_mode.map(super::aes::AesModeOptions::to_tuple); let file_name: Box = name.to_string().into_boxed_str(); - let file_name_raw: Box<[u8]> = file_name.as_bytes().into(); + let file_name_raw = file_name.as_bytes().to_vec(); let mut file = ZipFileData::initialize_local_block( file_name, &options, @@ -1304,7 +1313,7 @@ impl ZipWriter { let index = self.insert_file_data(file, &file_name_raw)?; self.writing_to_file = true; let result: ZipResult<()> = { - ExtendedFileOptions::validate_extra_data(&extra_data, false, &mut file_name_raw)?; + ExtendedFileOptions::validate_extra_data(&extra_data, false)?; let file = &mut self.files[index]; let block = file.local_block(&file_name_raw)?; let writer = self.inner.try_inner_mut()?; @@ -2488,7 +2497,11 @@ impl ZipFileData { Ok(()) } - pub(crate) fn write_central_directory_header(&self, writer: &mut T, file_name_raw: &[u8]) -> ZipResult<()> { + pub(crate) fn write_central_directory_header( + &self, + writer: &mut T, + file_name_raw: &[u8], + ) -> ZipResult<()> { let mut block = self.block(file_name_raw)?; let stripped_extra = if let Some(extra) = &self.extra_field { strip_alignment_extra_field(extra, true) From 29885aa37ad11ab7085e4de6b4582b8a694867ad Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 20 Apr 2026 20:03:38 -0600 Subject: [PATCH 10/23] revert --- src/result.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/result.rs b/src/result.rs index 47adecb18..c5356517c 100644 --- a/src/result.rs +++ b/src/result.rs @@ -94,6 +94,12 @@ impl From for ZipError { } } +impl From for ZipError { + fn from(_: std::string::FromUtf8Error) -> Self { + invalid!("Invalid UTF-8") + } +} + /// Error type for time parsing #[derive(Debug)] pub struct DateTimeRangeError; @@ -105,12 +111,6 @@ impl From for DateTimeRangeError { } } -impl From for ZipError { - fn from(_: std::string::FromUtf8Error) -> Self { - invalid!("Invalid UTF-8") - } -} - impl fmt::Display for DateTimeRangeError { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!( From 6224eb82b6d6d2d29464c76b83b4ef1aebd25669 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 20 Apr 2026 20:07:41 -0600 Subject: [PATCH 11/23] update --- src/write.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/write.rs b/src/write.rs index 010122df7..0ebe47961 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1412,9 +1412,9 @@ impl ZipWriter { let writer = self.inner.try_inner_mut()?; if !self.writing_raw { - let file = match self.files.last_mut() { + let (file_name_raw, file) = match self.files.last_mut() { None => return Ok(()), - Some((_, f)) => f, + Some(s) => s, }; file.uncompressed_size = self.stats.bytes_written; @@ -1440,7 +1440,7 @@ impl ZipWriter { if file.using_data_descriptor { file.write_data_descriptor(writer, self.auto_large_file)?; } else { - file.update_local_file_header(writer)?; + file.update_local_file_header(writer, file_name_raw)?; writer.seek(SeekFrom::Start(file_end))?; } } @@ -2450,6 +2450,7 @@ impl ZipFileData { pub(crate) fn update_local_file_header( &mut self, writer: &mut T, + file_name_raw: &[u8], ) -> ZipResult<()> { writer.seek(SeekFrom::Start( self.header_start + (size_of::() + offset_of!(ZipLocalEntryBlock, crc32)) as u64, @@ -2459,7 +2460,7 @@ impl ZipFileData { writer.write_u32_le(spec::ZIP64_BYTES_THR as u32)?; writer.write_u32_le(spec::ZIP64_BYTES_THR as u32)?; - self.update_local_zip64_extra_field(writer)?; + self.update_local_zip64_extra_field(writer, file_name_raw)?; // self.compressed_size = spec::ZIP64_BYTES_THR; // self.uncompressed_size = spec::ZIP64_BYTES_THR; @@ -2477,7 +2478,7 @@ impl ZipFileData { Ok(()) } - fn update_local_zip64_extra_field(&mut self, writer: &mut T) -> ZipResult<()> { + fn update_local_zip64_extra_field(&mut self, writer: &mut T, file_name_raw: &[u8]) -> ZipResult<()> { let zip64_block = Zip64ExtendedInformation::local_header( self.large_file, self.uncompressed_size, @@ -2489,7 +2490,7 @@ impl ZipFileData { let zip64_extra_field_start = self.header_start + (size_of::() + size_of::()) as u64 - + self.file_name.as_bytes().len() as u64; + + file_name_raw.len() as u64; writer.seek(SeekFrom::Start(zip64_extra_field_start))?; let zip64_block = zip64_block.serialize(); From e516464e137f661a00a42c8c0a57dedcfaf664fe Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 20 Apr 2026 20:18:41 -0600 Subject: [PATCH 12/23] cleanup --- src/read.rs | 5 ++++- src/read/zip_archive.rs | 10 +++++----- src/types.rs | 10 +++------- src/write.rs | 29 ++++++++++++++++------------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/read.rs b/src/read.rs index 23aa8d477..b2efc6ad8 100644 --- a/src/read.rs +++ b/src/read.rs @@ -580,7 +580,10 @@ fn central_header_to_zip_file_inner( Ok((result, file_name_raw.into())) } -pub(crate) fn parse_extra_field(file: &mut ZipFileData, file_name_raw: &mut Vec) -> ZipResult<()> { +pub(crate) fn parse_extra_field( + file: &mut ZipFileData, + file_name_raw: &mut Vec, +) -> ZipResult<()> { let mut extra_field = file.extra_field.clone(); let mut central_extra_field = file.central_extra_field.clone(); for field_group in [&mut extra_field, &mut central_extra_field] { diff --git a/src/read/zip_archive.rs b/src/read/zip_archive.rs index 4e59f49cb..d918f5e83 100644 --- a/src/read/zip_archive.rs +++ b/src/read/zip_archive.rs @@ -100,7 +100,6 @@ impl ZipArchive { Some((_, file)) => file.header_start, None => central_start, }; - // let files = files.into_iter().map(|(k,v)| (k, v.into_inner())).collect(); let shared = Arc::new(ZipArchiveMetadata { files, offset: initial_offset, @@ -226,11 +225,12 @@ impl ZipArchive { &mut self, file_number: usize, ) -> ZipResult> { - let (_file_name_raw, data) = self + let (_, data) = self .shared .files .get_index(file_number) .ok_or(ZipError::FileNotFound)?; + let limit_reader = data.find_content(&mut self.reader)?; match data.aes_mode { None => Ok(None), @@ -372,7 +372,7 @@ impl ZipArchive { /// copies would take up space independently in the destination. pub fn has_overlapping_files(&mut self) -> ZipResult { let mut ranges = Vec::>::with_capacity(self.shared.files.len()); - for (_file_name_raw, file) in &self.shared.files { + for file in self.shared.files.values() { if file.compressed_size == 0 { continue; } @@ -477,7 +477,7 @@ impl ZipArchive { .files .get_index(index) .ok_or(ZipError::FileNotFound) - .and_then(move |(_file_name_raw, data)| { + .and_then(move |(_, data)| { let seek_reader = match data.compression_method { CompressionMethod::Stored => { ZipFileSeekReader::Raw(data.find_content_seek(reader)?) @@ -611,7 +611,7 @@ impl ZipArchive { let mut root_dir: Option = None; for i in 0..self.len() { - let (_filename, file) = self + let (_, file) = self .shared .files .get_index(i) diff --git a/src/types.rs b/src/types.rs index 8ecb9e5d3..d78dd9891 100644 --- a/src/types.rs +++ b/src/types.rs @@ -411,8 +411,7 @@ impl ZipFileData { compression_method: crate::compression::CompressionMethod, aes_mode: Option<(AesMode, AesVendorVersion, CompressionMethod)>, extra_field: &[u8], - ) -> Self - { + ) -> Self { let permissions = options .permissions .unwrap_or(FileOptions::DEFAULT_FILE_PERMISSION); @@ -575,13 +574,10 @@ impl ZipFileData { Ok((data, file_name_raw)) } - fn is_ascii(&self) -> bool { - self.file_name.is_ascii() && self.file_comment.is_ascii() - } - fn flags(&self, file_name_raw: &[u8]) -> u16 { let is_utf8 = std::str::from_utf8(file_name_raw).is_ok(); - let utf8_bit: u16 = if is_utf8 && !self.is_ascii() { + let is_ascii = file_name_raw.is_ascii() && self.file_comment.is_ascii(); + let utf8_bit: u16 = if is_utf8 && !is_ascii { ZipFlags::LanguageEncoding.as_u16() } else { 0 diff --git a/src/write.rs b/src/write.rs index 0ebe47961..bfd62b474 100644 --- a/src/write.rs +++ b/src/write.rs @@ -399,10 +399,7 @@ impl ExtendedFileOptions { Ok(()) } - fn validate_extra_data( - data: &[u8], - disallow_zip64: bool, - ) -> ZipResult<()> { + fn validate_extra_data(data: &[u8], disallow_zip64: bool) -> ZipResult<()> { let len = data.len() as u64; if len == 0 { return Ok(()); @@ -844,6 +841,7 @@ impl ZipWriter { pub fn new_append_with_config(config: Config, mut readwriter: A) -> ZipResult> { readwriter.seek(SeekFrom::Start(0))?; let shared = ZipArchive::get_metadata(config, &mut readwriter)?; + Ok(ZipWriter { inner: GenericZipWriter::Storer(MaybeEncrypted::Unencrypted(readwriter)), files: shared.files, @@ -902,12 +900,12 @@ impl ZipWriter { .try_inner_mut()? .seek(SeekFrom::Start(write_position))?; let mut new_data = src_data.clone(); - let file_name_raw = dest_name.as_bytes(); + let dest_name_raw = dest_name.as_bytes(); new_data.file_name = dest_name.into(); new_data.header_start = write_position; let extra_data_start = write_position + (size_of::() + size_of::()) as u64 - + file_name_raw.len() as u64; + + dest_name_raw.len() as u64; new_data.extra_data_start = Some(extra_data_start); if let Some(extra) = &src_data.extra_field { let stripped = strip_alignment_extra_field(extra, false); @@ -925,13 +923,13 @@ impl ZipWriter { new_data.data_start.take(); new_data.data_start.get_or_init(|| data_start); new_data.central_header_start = 0; - let block = new_data.local_block(file_name_raw)?; - let index = self.insert_file_data(new_data, file_name_raw)?; + let block = new_data.local_block(dest_name_raw)?; + let index = self.insert_file_data(dest_name_raw, new_data)?; let new_data = &self.files[index]; let result: io::Result<()> = { let plain_writer = self.inner.try_inner_mut()?; block.write(plain_writer)?; - plain_writer.write_all(&file_name_raw)?; + plain_writer.write_all(&dest_name_raw)?; if let Some(data) = &new_data.extra_field { plain_writer.write_all(data)?; } @@ -996,6 +994,7 @@ impl ZipWriter { let comment = mem::take(&mut self.comment); let zip64_extensible_data_sector = mem::take(&mut self.zip64_extensible_data_sector); let files = mem::take(&mut self.files); + Ok(ZipArchive::from_finalized_writer( files, comment, @@ -1310,7 +1309,7 @@ impl ZipWriter { !self.seek_possible || matches!(options.encrypt_with, Some(EncryptWith::ZipCrypto(..))); file.version_made_by = file.version_made_by.max(file.version_needed() as u8); file.extra_data_start = Some(header_end); - let index = self.insert_file_data(file, &file_name_raw)?; + let index = self.insert_file_data(&file_name_raw, file)?; self.writing_to_file = true; let result: ZipResult<()> = { ExtendedFileOptions::validate_extra_data(&extra_data, false)?; @@ -1388,7 +1387,7 @@ impl ZipWriter { Ok(()) } - fn insert_file_data(&mut self, file: ZipFileData, file_name_raw: &[u8]) -> ZipResult { + fn insert_file_data(&mut self, file_name_raw: &[u8], file: ZipFileData) -> ZipResult { if self.files.contains_key(file_name_raw) { return Err(invalid!("Duplicate filename: {}", file.file_name)); } @@ -1989,7 +1988,7 @@ impl ZipWriter { dest_data.file_name = dest_name.into(); let file_name_raw = dest_name.as_bytes().into(); dest_data.central_header_start = 0; - self.insert_file_data(dest_data, file_name_raw)?; + self.insert_file_data(file_name_raw, dest_data)?; Ok(()) } @@ -2478,7 +2477,11 @@ impl ZipFileData { Ok(()) } - fn update_local_zip64_extra_field(&mut self, writer: &mut T, file_name_raw: &[u8]) -> ZipResult<()> { + fn update_local_zip64_extra_field( + &mut self, + writer: &mut T, + file_name_raw: &[u8], + ) -> ZipResult<()> { let zip64_block = Zip64ExtendedInformation::local_header( self.large_file, self.uncompressed_size, From 14d1494d16947202f3cb6680c4bd6f48bc479018 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 23 Apr 2026 15:39:02 -0600 Subject: [PATCH 13/23] clippy --- src/read.rs | 2 +- src/write.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/read.rs b/src/read.rs index b2efc6ad8..b7d0d07c2 100644 --- a/src/read.rs +++ b/src/read.rs @@ -711,7 +711,7 @@ pub(crate) fn parse_single_extra_field( // Info-ZIP Unicode Path Extra Field // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt let unicode = UnicodeExtraField::try_from_reader(reader, len)?; - let file_name = unicode.unwrap_valid(&file_name_raw)?; + let file_name = unicode.unwrap_valid(file_name_raw)?; file_name_raw.clear(); file_name_raw.extend_from_slice(&file_name); file.file_name = String::from_utf8(file_name_raw.to_vec())?.into_boxed_str(); diff --git a/src/write.rs b/src/write.rs index dd086b3a3..80414869b 100644 --- a/src/write.rs +++ b/src/write.rs @@ -929,7 +929,7 @@ impl ZipWriter { let result: io::Result<()> = { let plain_writer = self.inner.try_inner_mut()?; block.write(plain_writer)?; - plain_writer.write_all(&dest_name_raw)?; + plain_writer.write_all(dest_name_raw)?; if let Some(data) = &new_data.extra_field { plain_writer.write_all(data)?; } @@ -1909,7 +1909,7 @@ impl ZipWriter { let mut version_needed = u16::from(MIN_VERSION); let central_start = writer.stream_position()?; for (filename_raw, file) in &self.files { - file.write_central_directory_header(writer, &filename_raw)?; + file.write_central_directory_header(writer, filename_raw)?; version_needed = version_needed.max(file.version_needed()); } let central_size = writer.stream_position()? - central_start; @@ -1986,7 +1986,7 @@ impl ZipWriter { let src_index = self.index_by_name(src_name.as_bytes())?; let mut dest_data = self.files[src_index].clone(); dest_data.file_name = dest_name.into(); - let file_name_raw = dest_name.as_bytes().into(); + let file_name_raw = dest_name.as_bytes(); dest_data.central_header_start = 0; self.insert_file_data(file_name_raw, dest_data)?; From 116d876e08ffde1a518d5a8618220e95e538f6aa Mon Sep 17 00:00:00 2001 From: n4n5 Date: Thu, 23 Apr 2026 15:52:12 -0600 Subject: [PATCH 14/23] Update src/read.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: n4n5 --- src/read.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/read.rs b/src/read.rs index b7d0d07c2..64f4ef96c 100644 --- a/src/read.rs +++ b/src/read.rs @@ -711,10 +711,9 @@ pub(crate) fn parse_single_extra_field( // Info-ZIP Unicode Path Extra Field // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt let unicode = UnicodeExtraField::try_from_reader(reader, len)?; - let file_name = unicode.unwrap_valid(file_name_raw)?; - file_name_raw.clear(); - file_name_raw.extend_from_slice(&file_name); - file.file_name = String::from_utf8(file_name_raw.to_vec())?.into_boxed_str(); + let file_name = unicode.unwrap_valid(&file_name_raw)?; + file.file_name = String::from_utf8(file_name.to_vec())?.into_boxed_str(); + *file_name_raw = file_name.into_vec(); file.is_utf8 = true; } _ => { From b2b8190c8a1a20f16c44b2f8ae20f0d8393df9ad Mon Sep 17 00:00:00 2001 From: n4n5 Date: Fri, 24 Apr 2026 12:30:44 -0600 Subject: [PATCH 15/23] add --- src/read.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/read.rs b/src/read.rs index 64f4ef96c..df1551079 100644 --- a/src/read.rs +++ b/src/read.rs @@ -711,7 +711,7 @@ pub(crate) fn parse_single_extra_field( // Info-ZIP Unicode Path Extra Field // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt let unicode = UnicodeExtraField::try_from_reader(reader, len)?; - let file_name = unicode.unwrap_valid(&file_name_raw)?; + let file_name = unicode.unwrap_valid(file_name_raw)?; file.file_name = String::from_utf8(file_name.to_vec())?.into_boxed_str(); *file_name_raw = file_name.into_vec(); file.is_utf8 = true; From 929dc88d26053109599efe95fa4a000eb76f35b1 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 25 Apr 2026 16:48:52 -0600 Subject: [PATCH 16/23] fix name --- src/write.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/write.rs b/src/write.rs index 3b28dd919..c6ef8e044 100644 --- a/src/write.rs +++ b/src/write.rs @@ -899,7 +899,7 @@ impl ZipWriter { .try_inner_mut()? .seek(SeekFrom::Start(write_position))?; let mut new_data = src_data.clone(); - new_data.file_name_raw = dest_name.as_bytes().into(); + let dest_name_raw = dest_name.as_bytes(); new_data.file_name = dest_name.into(); new_data.header_start = write_position; let extra_data_start = write_position From d21f52fb6831766236f7259cd87785fb26d9115a Mon Sep 17 00:00:00 2001 From: n4n5 Date: Sat, 25 Apr 2026 16:49:30 -0600 Subject: [PATCH 17/23] bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 289884bfa..2c2dbb6e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zip" -version = "8.5.1" +version = "9.0.0" authors = [ "Mathijs van de Nes ", "Marli Frost ", From cd937ccdfe81f154fb32d2fc7be4d20a949b03d0 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:25:35 -0700 Subject: [PATCH 18/23] Rewrite using let-else --- src/write.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/write.rs b/src/write.rs index 1d8a095d9..9831dbf6a 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1411,9 +1411,8 @@ impl ZipWriter { let writer = self.inner.try_inner_mut()?; if !self.writing_raw { - let (file_name_raw, file) = match self.files.last_mut() { - None => return Ok(()), - Some(s) => s, + let Some((file_name_raw, file)) = self.files.last_mut() else { + return Ok(()) }; file.uncompressed_size = self.stats.bytes_written; From d1fdada7af89b04f01aac2fb561a66dfc54fd6e5 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:32:02 -0700 Subject: [PATCH 19/23] Fix clippy warnings --- src/extra_fields/extended_timestamp.rs | 24 ++++++++++++------------ src/extra_fields/zipinfo_utf8.rs | 4 ++-- src/read/stream.rs | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/extra_fields/extended_timestamp.rs b/src/extra_fields/extended_timestamp.rs index 0521fd5f2..283ab0469 100644 --- a/src/extra_fields/extended_timestamp.rs +++ b/src/extra_fields/extended_timestamp.rs @@ -222,21 +222,21 @@ mod tests { fn check_extended_timestamp_value() { let mut cursor = Cursor::new(&[0b0000_0001_u8, 0x00, 0x00, 0x00, 0x01]); let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5).unwrap(); - assert_eq!(result.mod_time(), Some(16777216)); + assert_eq!(result.mod_time(), Some(1 << 24)); assert_eq!(result.ac_time(), None); assert_eq!(result.cr_time(), None); let mut cursor = Cursor::new(&[0b0000_0010_u8, 0x00, 0x00, 0x00, 0x02]); let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5).unwrap(); assert_eq!(result.mod_time(), None); - assert_eq!(result.ac_time(), Some(33554432)); + assert_eq!(result.ac_time(), Some(2 << 24)); assert_eq!(result.cr_time(), None); let mut cursor = Cursor::new(&[0b0000_0100_u8, 0x00, 0x00, 0x00, 0x03]); let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5).unwrap(); assert_eq!(result.mod_time(), None); assert_eq!(result.ac_time(), None); - assert_eq!(result.cr_time(), Some(50331648)); + assert_eq!(result.cr_time(), Some(3 << 24)); let mut cursor = Cursor::new(&[ 0b0000_0011_u8, @@ -250,8 +250,8 @@ mod tests { 0x02, ]); let result = ExtendedTimestamp::try_from_reader(&mut cursor, 9).unwrap(); - assert_eq!(result.mod_time(), Some(16777216)); - assert_eq!(result.ac_time(), Some(33554432)); + assert_eq!(result.mod_time(), Some(1 << 24)); + assert_eq!(result.ac_time(), Some(2 << 24)); assert_eq!(result.cr_time(), None); let mut cursor = Cursor::new(&[ @@ -270,9 +270,9 @@ mod tests { 0x03, ]); let result = ExtendedTimestamp::try_from_reader(&mut cursor, 13).unwrap(); - assert_eq!(result.mod_time(), Some(16777216)); - assert_eq!(result.ac_time(), Some(33554432)); - assert_eq!(result.cr_time(), Some(50331648)); + assert_eq!(result.mod_time(), Some(1 << 24)); + assert_eq!(result.ac_time(), Some(2 << 24)); + assert_eq!(result.cr_time(), Some(3 << 24)); } #[test] @@ -280,7 +280,7 @@ mod tests { // in the central header let mut cursor = Cursor::new(&[0b0000_0111_u8, 0x00, 0x00, 0x00, 0x01]); let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5).unwrap(); - assert_eq!(result.mod_time(), Some(16777216)); + assert_eq!(result.mod_time(), Some(1 << 24)); assert_eq!(result.ac_time(), None); assert_eq!(result.cr_time(), None); @@ -301,8 +301,8 @@ mod tests { 0x03, ]); let result = ExtendedTimestamp::try_from_reader(&mut cursor, 13).unwrap(); - assert_eq!(result.mod_time(), Some(16777216)); - assert_eq!(result.ac_time(), Some(33554432)); - assert_eq!(result.cr_time(), Some(50331648)); + assert_eq!(result.mod_time(), Some(1 << 24)); + assert_eq!(result.ac_time(), Some(2 << 24)); + assert_eq!(result.cr_time(), Some(3 << 24)); } } diff --git a/src/extra_fields/zipinfo_utf8.rs b/src/extra_fields/zipinfo_utf8.rs index 493621ec6..675463b81 100644 --- a/src/extra_fields/zipinfo_utf8.rs +++ b/src/extra_fields/zipinfo_utf8.rs @@ -52,7 +52,7 @@ mod tests { #[test] fn unicode_extra_field_crc32_correct() { let data = [ - 0x01, 0xef, 0x39, 0x8e, 0x4b, 'u' as u8, 't' as u8, 'f' as u8, '-' as u8, '8' as u8, + 0x01, 0xef, 0x39, 0x8e, 0x4b, b'u', b't', b'f', b'-', b'8', ]; let extra = UnicodeExtraField::try_from_reader(&mut std::io::Cursor::new(data), 10).unwrap(); @@ -65,7 +65,7 @@ mod tests { #[test] fn unicode_extra_field_crc32_incorrect() { let data = [ - 0x01, 0x00, 0x00, 0x00, 0x00, 'u' as u8, 't' as u8, 'f' as u8, '-' as u8, '8' as u8, + 0x01, 0x00, 0x00, 0x00, 0x00, b'u', b't', b'f', b'-', b'8', ]; let extra = UnicodeExtraField::try_from_reader(&mut std::io::Cursor::new(data), 10).unwrap(); diff --git a/src/read/stream.rs b/src/read/stream.rs index ddad897aa..b3bd72a30 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -570,7 +570,7 @@ mod tests { let mut reader = Cursor::new(bytes); loop { - if read_zipfile_from_stream_with_compressed_size(&mut reader, compressed_size as u64) + if read_zipfile_from_stream_with_compressed_size(&mut reader, u64::from(compressed_size)) .unwrap() .is_none() { From d6b396049fef05cac176fb4c4fe473dd6ebcf85d Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:32:35 -0700 Subject: [PATCH 20/23] style: cargo fmt --all --- src/extra_fields/zipinfo_utf8.rs | 8 ++------ src/read/stream.rs | 9 ++++++--- src/write.rs | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/extra_fields/zipinfo_utf8.rs b/src/extra_fields/zipinfo_utf8.rs index 675463b81..f1a1bcf8e 100644 --- a/src/extra_fields/zipinfo_utf8.rs +++ b/src/extra_fields/zipinfo_utf8.rs @@ -51,9 +51,7 @@ mod tests { use crate::extra_fields::UnicodeExtraField; #[test] fn unicode_extra_field_crc32_correct() { - let data = [ - 0x01, 0xef, 0x39, 0x8e, 0x4b, b'u', b't', b'f', b'-', b'8', - ]; + let data = [0x01, 0xef, 0x39, 0x8e, 0x4b, b'u', b't', b'f', b'-', b'8']; let extra = UnicodeExtraField::try_from_reader(&mut std::io::Cursor::new(data), 10).unwrap(); let res = extra.unwrap_valid(b"abcdef"); @@ -64,9 +62,7 @@ mod tests { #[test] fn unicode_extra_field_crc32_incorrect() { - let data = [ - 0x01, 0x00, 0x00, 0x00, 0x00, b'u', b't', b'f', b'-', b'8', - ]; + let data = [0x01, 0x00, 0x00, 0x00, 0x00, b'u', b't', b'f', b'-', b'8']; let extra = UnicodeExtraField::try_from_reader(&mut std::io::Cursor::new(data), 10).unwrap(); let res = extra.unwrap_valid(b"abcdef"); diff --git a/src/read/stream.rs b/src/read/stream.rs index b3bd72a30..f6b71b41b 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -570,9 +570,12 @@ mod tests { let mut reader = Cursor::new(bytes); loop { - if read_zipfile_from_stream_with_compressed_size(&mut reader, u64::from(compressed_size)) - .unwrap() - .is_none() + if read_zipfile_from_stream_with_compressed_size( + &mut reader, + u64::from(compressed_size), + ) + .unwrap() + .is_none() { break; } diff --git a/src/write.rs b/src/write.rs index 9831dbf6a..ea2848336 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1412,7 +1412,7 @@ impl ZipWriter { if !self.writing_raw { let Some((file_name_raw, file)) = self.files.last_mut() else { - return Ok(()) + return Ok(()); }; file.uncompressed_size = self.stats.bytes_written; From 500f90ca8c5d9183ffe1f6ef9b8a89926c06a8c7 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:59:58 -0700 Subject: [PATCH 21/23] refactor: Replace Box<[u8]> with Arc<[u8]> so raw and UTF-8 filename can be backed by same slice (via Gemini 3.1 Pro) --- src/read.rs | 42 ++++++++++++++++++++++++++--------------- src/read/stream.rs | 5 +++-- src/read/zip_archive.rs | 6 +++--- src/types.rs | 8 ++++---- src/write.rs | 33 +++++++++++++++++++------------- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/read.rs b/src/read.rs index 18b4d842f..0a63df9d6 100644 --- a/src/read.rs +++ b/src/read.rs @@ -79,7 +79,7 @@ pub(crate) fn make_writable_dir_all>(outpath: T) -> Result<(), Zi pub(crate) fn make_symlink_impl( outpath: &Path, target_str: &str, - _existing_files: &IndexMap, T>, + _existing_files: &IndexMap, T>, ) -> ZipResult<()> { std::os::unix::fs::symlink(Path::new(&target_str), outpath)?; Ok(()) @@ -89,7 +89,7 @@ pub(crate) fn make_symlink_impl( pub(crate) fn make_symlink_impl( outpath: &Path, target_str: &str, - existing_files: &IndexMap, T>, + existing_files: &IndexMap, T>, ) -> ZipResult<()> { let target = Path::new(OsStr::new(&target_str)); let target_is_dir_from_archive = @@ -113,7 +113,7 @@ pub(crate) fn make_symlink_impl( pub(crate) fn make_symlink( outpath: &Path, target: &[u8], - #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, + #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, ) -> ZipResult<()> { let Ok(target_str) = std::str::from_utf8(target) else { return Err(invalid!("Invalid UTF-8 as symlink target")); @@ -125,7 +125,7 @@ pub(crate) fn make_symlink( pub(crate) fn make_symlink( outpath: &Path, target: &[u8], - #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, + #[cfg_attr(not(any(windows, unix)), allow(unused))] existing_files: &IndexMap, T>, ) -> ZipResult<()> { let Ok(_) = std::str::from_utf8(target) else { return Err(invalid!("Invalid UTF-8 as symlink target")); @@ -219,7 +219,7 @@ impl ZipArchive { pub(crate) fn merge_contents( &mut self, mut w: W, - ) -> ZipResult, ZipFileData>> { + ) -> ZipResult, ZipFileData>> { if self.shared.files.is_empty() { return Ok(IndexMap::new()); } @@ -456,7 +456,7 @@ impl ZipArchive { pub(crate) fn central_header_to_zip_file( reader: &mut R, central_directory: &CentralDirectoryInfo, -) -> ZipResult<(ZipFileData, Box<[u8]>)> { +) -> ZipResult<(ZipFileData, Arc<[u8]>)> { let central_header_start = reader.stream_position()?; // Parse central header @@ -495,7 +495,7 @@ fn central_header_to_zip_file_inner( archive_offset: u64, central_header_start: u64, block: ZipCentralEntryBlock, -) -> ZipResult<(ZipFileData, Box<[u8]>)> { +) -> ZipResult<(ZipFileData, Arc<[u8]>)> { let ZipCentralEntryBlock { // magic, version_made_by, @@ -522,11 +522,7 @@ fn central_header_to_zip_file_inner( let mut file_name_raw = read_variable_length_byte_field(reader, file_name_length as usize)?; let extra_field = read_variable_length_byte_field(reader, extra_field_length as usize)?; let file_comment_raw = read_variable_length_byte_field(reader, file_comment_length as usize)?; - let file_name: Box = if is_utf8 { - String::from_utf8_lossy(&file_name_raw).into() - } else { - file_name_raw.from_cp437()?.into() - }; + let file_comment: Box = if is_utf8 { String::from_utf8_lossy(&file_comment_raw).into() } else { @@ -544,7 +540,7 @@ fn central_header_to_zip_file_inner( compressed_size: compressed_size.into(), uncompressed_size: uncompressed_size.into(), flags, - file_name, + file_name: Arc::from(""), // temporary extra_field: Some(Arc::from(extra_field)), central_extra_field: None, file_comment, @@ -560,6 +556,23 @@ fn central_header_to_zip_file_inner( }; parse_extra_field(&mut result, &mut file_name_raw)?; + let is_utf8 = ZipFlags::matching(result.flags, ZipFlags::LanguageEncoding); + let file_name_arc: Arc; + let file_name_raw_arc: Arc<[u8]>; + if is_utf8 { + if let Ok(s) = std::str::from_utf8(&file_name_raw) { + file_name_arc = Arc::from(s); + file_name_raw_arc = unsafe { Arc::from_raw(Arc::into_raw(file_name_arc.clone()) as *const [u8]) }; + } else { + file_name_arc = String::from_utf8_lossy(&file_name_raw).into(); + file_name_raw_arc = file_name_raw.into(); + } + } else { + file_name_arc = file_name_raw.from_cp437()?.into(); + file_name_raw_arc = file_name_raw.into(); + } + result.file_name = file_name_arc; + let aes_enabled = result.compression_method == CompressionMethod::AES; if aes_enabled && result.aes_mode.is_none() { return Err(invalid!("AES encryption without AES extra data field")); @@ -571,7 +584,7 @@ fn central_header_to_zip_file_inner( .checked_add(archive_offset) .ok_or(invalid!("Archive header is too large"))?; - Ok((result, file_name_raw.into())) + Ok((result, file_name_raw_arc)) } pub(crate) fn parse_extra_field( @@ -706,7 +719,6 @@ pub(crate) fn parse_single_extra_field( // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt let unicode = UnicodeExtraField::try_from_reader(reader, len)?; let file_name = unicode.unwrap_valid(file_name_raw)?; - file.file_name = String::from_utf8(file_name.to_vec())?.into_boxed_str(); *file_name_raw = file_name.into_vec(); file.flags |= ZipFlags::LanguageEncoding.as_u16(); } diff --git a/src/read/stream.rs b/src/read/stream.rs index f6b71b41b..ebc6bc6d0 100644 --- a/src/read/stream.rs +++ b/src/read/stream.rs @@ -11,6 +11,7 @@ use indexmap::IndexMap; use std::borrow::Cow; use std::io::{self, Read}; use std::path::{Path, PathBuf}; +use std::sync::Arc; /// Stream decoder for zip. #[derive(Debug)] @@ -61,7 +62,7 @@ impl ZipStreamReader { /// Extraction is not atomic; If an error is encountered, some of the files /// may be left on disk. pub fn extract>(self, directory: P) -> ZipResult<()> { - struct Extractor(PathBuf, IndexMap, ()>); + struct Extractor(PathBuf, IndexMap, ()>); impl ZipStreamVisitor for Extractor { fn visit_file(&mut self, file: &mut ZipFile<'_, R>) -> ZipResult<()> { self.1.insert(file.name_raw().into(), ()); @@ -133,7 +134,7 @@ pub trait ZipStreamVisitor { /// Additional metadata for the file. #[derive(Debug)] -pub struct ZipStreamFileMetadata(ZipFileData, Box<[u8]>); +pub struct ZipStreamFileMetadata(ZipFileData, Arc<[u8]>); impl ZipStreamFileMetadata { /// Get the name of the file diff --git a/src/read/zip_archive.rs b/src/read/zip_archive.rs index cfbcb8e92..754f4ea6c 100644 --- a/src/read/zip_archive.rs +++ b/src/read/zip_archive.rs @@ -20,7 +20,7 @@ use std::sync::Arc; /// Immutable metadata about a `ZipArchive`. #[derive(Debug)] pub struct ZipArchiveMetadata { - pub(crate) files: IndexMap, ZipFileData>, + pub(crate) files: IndexMap, ZipFileData>, pub(crate) offset: u64, pub(crate) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. @@ -32,7 +32,7 @@ pub struct ZipArchiveMetadata { #[derive(Debug)] pub(crate) struct SharedBuilder { - pub(crate) files: Vec<(Box<[u8]>, ZipFileData)>, + pub(crate) files: Vec<(Arc<[u8]>, ZipFileData)>, pub(super) offset: u64, pub(super) dir_start: u64, // This isn't yet used anywhere, but it is here for use cases in the future. @@ -89,7 +89,7 @@ pub struct ZipArchive { impl ZipArchive { pub(crate) fn from_finalized_writer( - files: IndexMap, ZipFileData>, + files: IndexMap, ZipFileData>, comment: Box<[u8]>, zip64_extensible_data_sector: Option>, reader: R, diff --git a/src/types.rs b/src/types.rs index b7159d95c..090c31e9d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -184,7 +184,7 @@ pub struct ZipFileData { /// Size of the file when extracted pub uncompressed_size: u64, /// Name of the file - pub file_name: Box, + pub file_name: Arc, /// Extra field usually used for storage expansion pub extra_field: Option>, /// Extra field only written to central directory @@ -404,7 +404,7 @@ impl ZipFileData { #[allow(clippy::too_many_arguments)] pub(crate) fn initialize_local_block( - file_name: Box, + file_name: Arc, options: &FileOptions<'_, T>, raw_values: &ZipRawValues, header_start: u64, @@ -534,7 +534,7 @@ impl ZipFileData { return Err(e.into()); } - let file_name: Box = if is_utf8 { + let file_name: Arc = if is_utf8 { String::from_utf8_lossy(&file_name_raw).into() } else { file_name_raw @@ -893,7 +893,7 @@ mod tests { crc32: 0, compressed_size: 0, uncompressed_size: 0, - file_name: file_name.clone().into_boxed_str(), + file_name: file_name.into(), extra_field: None, central_extra_field: None, file_comment: String::with_capacity(0).into_boxed_str(), diff --git a/src/write.rs b/src/write.rs index ea2848336..dde97a348 100644 --- a/src/write.rs +++ b/src/write.rs @@ -137,6 +137,7 @@ pub(crate) mod zip_writer { use core::fmt::{Debug, Formatter}; use indexmap::IndexMap; use std::io::{Seek, Write}; + use std::sync::Arc; /// ZIP archive generator /// @@ -173,7 +174,7 @@ pub(crate) mod zip_writer { /// ``` pub struct ZipWriter { pub(super) inner: GenericZipWriter, - pub(super) files: IndexMap, ZipFileData>, + pub(super) files: IndexMap, ZipFileData>, pub(super) stats: ZipWriterStats, pub(super) writing_to_file: bool, pub(super) writing_raw: bool, @@ -900,8 +901,11 @@ impl ZipWriter { .try_inner_mut()? .seek(SeekFrom::Start(write_position))?; let mut new_data = src_data.clone(); - let dest_name_raw = dest_name.as_bytes(); - new_data.file_name = dest_name.into(); + let dest_name_string = dest_name.to_string(); + let dest_name_arc: Arc = dest_name_string.into(); + let dest_name_raw_arc: Arc<[u8]> = unsafe { Arc::from_raw(Arc::into_raw(dest_name_arc.clone()) as *const [u8]) }; + let dest_name_raw = dest_name_raw_arc.as_ref(); + new_data.file_name = dest_name_arc; new_data.header_start = write_position; let extra_data_start = write_position + (size_of::() + size_of::()) as u64 @@ -924,7 +928,7 @@ impl ZipWriter { new_data.data_start.get_or_init(|| data_start); new_data.central_header_start = 0; let block = new_data.local_block(dest_name_raw)?; - let index = self.insert_file_data(dest_name_raw, new_data)?; + let index = self.insert_file_data(dest_name_raw_arc.clone(), new_data)?; let new_data = &self.files[index]; let result: io::Result<()> = { let plain_writer = self.inner.try_inner_mut()?; @@ -1282,8 +1286,9 @@ impl ZipWriter { } #[cfg(feature = "aes-crypto")] let aes_mode = aes_mode.map(super::aes::AesModeOptions::to_tuple); - let file_name: Box = name.to_string().into_boxed_str(); - let file_name_raw = file_name.as_bytes().to_vec(); + let file_name: Arc = name.to_string().into(); + let file_name_raw_arc: Arc<[u8]> = unsafe { Arc::from_raw(Arc::into_raw(file_name.clone()) as *const [u8]) }; + let file_name_raw = file_name_raw_arc.as_ref(); let mut file = ZipFileData::initialize_local_block( file_name, &options, @@ -1309,7 +1314,7 @@ impl ZipWriter { } file.version_made_by = file.version_made_by.max(file.version_needed() as u8); file.extra_data_start = Some(header_end); - let index = self.insert_file_data(&file_name_raw, file)?; + let index = self.insert_file_data(file_name_raw_arc.clone(), file)?; self.writing_to_file = true; let result: ZipResult<()> = { ExtendedFileOptions::validate_extra_data(&extra_data, false)?; @@ -1387,11 +1392,11 @@ impl ZipWriter { Ok(()) } - fn insert_file_data(&mut self, file_name_raw: &[u8], file: ZipFileData) -> ZipResult { - if self.files.contains_key(file_name_raw) { + fn insert_file_data(&mut self, file_name_raw: Arc<[u8]>, file: ZipFileData) -> ZipResult { + if self.files.contains_key(file_name_raw.as_ref()) { return Err(invalid!("Duplicate filename: {}", file.file_name)); } - let (index, _) = self.files.insert_full(file_name_raw.into(), file); + let (index, _) = self.files.insert_full(file_name_raw, file); Ok(index) } @@ -1984,10 +1989,12 @@ impl ZipWriter { } let src_index = self.index_by_name(src_name.as_bytes())?; let mut dest_data = self.files[src_index].clone(); - dest_data.file_name = dest_name.into(); - let file_name_raw = dest_name.as_bytes(); + let dest_name_string = dest_name.to_string(); + let dest_name_arc: Arc = dest_name_string.into(); + let dest_name_raw_arc: Arc<[u8]> = unsafe { Arc::from_raw(Arc::into_raw(dest_name_arc.clone()) as *const [u8]) }; + dest_data.file_name = dest_name_arc; dest_data.central_header_start = 0; - self.insert_file_data(file_name_raw, dest_data)?; + self.insert_file_data(dest_name_raw_arc, dest_data)?; Ok(()) } From 64819301ae331e09588e3f3e69c2d8a388da7418 Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:00:27 -0700 Subject: [PATCH 22/23] cargo clippy --fix --- src/write.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/write.rs b/src/write.rs index dde97a348..87b4305b6 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1319,11 +1319,11 @@ impl ZipWriter { let result: ZipResult<()> = { ExtendedFileOptions::validate_extra_data(&extra_data, false)?; let file = &mut self.files[index]; - let block = file.local_block(&file_name_raw)?; + let block = file.local_block(file_name_raw)?; let writer = self.inner.try_inner_mut()?; block.write(writer)?; // file name - writer.write_all(&file_name_raw)?; + writer.write_all(file_name_raw)?; if extra_data_len > 0 { writer.write_all(&extra_data)?; file.extra_field = Some(Arc::from(extra_data.into_boxed_slice())); From a3a0428f71120d207961d250d935d85e730a855d Mon Sep 17 00:00:00 2001 From: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:00:45 -0700 Subject: [PATCH 23/23] cargo fmt --all --- src/read.rs | 3 ++- src/write.rs | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/read.rs b/src/read.rs index 0a63df9d6..98a0c8549 100644 --- a/src/read.rs +++ b/src/read.rs @@ -562,7 +562,8 @@ fn central_header_to_zip_file_inner( if is_utf8 { if let Ok(s) = std::str::from_utf8(&file_name_raw) { file_name_arc = Arc::from(s); - file_name_raw_arc = unsafe { Arc::from_raw(Arc::into_raw(file_name_arc.clone()) as *const [u8]) }; + file_name_raw_arc = + unsafe { Arc::from_raw(Arc::into_raw(file_name_arc.clone()) as *const [u8]) }; } else { file_name_arc = String::from_utf8_lossy(&file_name_raw).into(); file_name_raw_arc = file_name_raw.into(); diff --git a/src/write.rs b/src/write.rs index 87b4305b6..7bbeb102a 100644 --- a/src/write.rs +++ b/src/write.rs @@ -903,7 +903,8 @@ impl ZipWriter { let mut new_data = src_data.clone(); let dest_name_string = dest_name.to_string(); let dest_name_arc: Arc = dest_name_string.into(); - let dest_name_raw_arc: Arc<[u8]> = unsafe { Arc::from_raw(Arc::into_raw(dest_name_arc.clone()) as *const [u8]) }; + let dest_name_raw_arc: Arc<[u8]> = + unsafe { Arc::from_raw(Arc::into_raw(dest_name_arc.clone()) as *const [u8]) }; let dest_name_raw = dest_name_raw_arc.as_ref(); new_data.file_name = dest_name_arc; new_data.header_start = write_position; @@ -1287,7 +1288,8 @@ impl ZipWriter { #[cfg(feature = "aes-crypto")] let aes_mode = aes_mode.map(super::aes::AesModeOptions::to_tuple); let file_name: Arc = name.to_string().into(); - let file_name_raw_arc: Arc<[u8]> = unsafe { Arc::from_raw(Arc::into_raw(file_name.clone()) as *const [u8]) }; + let file_name_raw_arc: Arc<[u8]> = + unsafe { Arc::from_raw(Arc::into_raw(file_name.clone()) as *const [u8]) }; let file_name_raw = file_name_raw_arc.as_ref(); let mut file = ZipFileData::initialize_local_block( file_name, @@ -1392,7 +1394,11 @@ impl ZipWriter { Ok(()) } - fn insert_file_data(&mut self, file_name_raw: Arc<[u8]>, file: ZipFileData) -> ZipResult { + fn insert_file_data( + &mut self, + file_name_raw: Arc<[u8]>, + file: ZipFileData, + ) -> ZipResult { if self.files.contains_key(file_name_raw.as_ref()) { return Err(invalid!("Duplicate filename: {}", file.file_name)); } @@ -1991,7 +1997,8 @@ impl ZipWriter { let mut dest_data = self.files[src_index].clone(); let dest_name_string = dest_name.to_string(); let dest_name_arc: Arc = dest_name_string.into(); - let dest_name_raw_arc: Arc<[u8]> = unsafe { Arc::from_raw(Arc::into_raw(dest_name_arc.clone()) as *const [u8]) }; + let dest_name_raw_arc: Arc<[u8]> = + unsafe { Arc::from_raw(Arc::into_raw(dest_name_arc.clone()) as *const [u8]) }; dest_data.file_name = dest_name_arc; dest_data.central_header_start = 0; self.insert_file_data(dest_name_raw_arc, dest_data)?;