From e412d8b6df663d3e9b565808de28df57ba1b07f4 Mon Sep 17 00:00:00 2001 From: Chris Hennick Date: Sat, 20 Apr 2024 14:05:11 -0700 Subject: [PATCH] Restore support for Path and fix handling of ".." --- CHANGELOG.md | 9 ++++++-- src/read.rs | 18 +++++++++++++++- src/spec.rs | 25 +++++++++++++++++++++ src/write.rs | 61 +++++++++++++++++++++++++++++++--------------------- 4 files changed, 85 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a9d5c1..2de1b83e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -273,5 +273,10 @@ ### Added - - `index_for_name`: get the index of a file given its name, without initializing metadata or needing to mutably borrow - the `ZipArchive`. \ No newline at end of file + - `index_for_name`, `index_for_path`, `name_for_index`: get the index of a file given its path or vice-versa, without + initializing metadata from the local-file header or needing to mutably borrow the `ZipArchive`. + - `add_symlink_from_path`: create a symlink using `AsRef` arguments + +### Changed + + - `add_directory_from_path` and `start_file_from_path` are no longer deprecated. \ No newline at end of file diff --git a/src/read.rs b/src/read.rs index f9c92498..bca18dbd 100644 --- a/src/read.rs +++ b/src/read.rs @@ -86,6 +86,7 @@ pub(crate) mod zip_archive { #[cfg(feature = "lzma")] use crate::read::lzma::LzmaDecoder; use crate::result::ZipError::InvalidPassword; +use crate::spec::path_to_string; pub use zip_archive::ZipArchive; #[allow(clippy::large_enum_variant)] @@ -654,7 +655,22 @@ impl ZipArchive { /// Get the index of a file entry by name, if it's present. #[inline(always)] pub fn index_for_name(&self, name: &str) -> Option { - self.shared.names_map.get(name).map(|index_ref| *index_ref) + self.shared.names_map.get(name).copied() + } + + /// Get the index of a file entry by path, if it's present. + #[inline(always)] + pub fn index_for_path>(&self, path: T) -> Option { + self.index_for_name(&path_to_string(path)) + } + + /// Get the name of a file entry, if it's present. + #[inline(always)] + pub fn name_for_index(&self, index: usize) -> Option<&str> { + self.shared + .files + .get(index) + .map(|file_data| &*file_data.file_name) } fn by_name_with_optional_password<'a>( diff --git a/src/spec.rs b/src/spec.rs index d7cdd7c6..2bb1489a 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,7 +1,9 @@ use crate::result::{ZipError, ZipResult}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use std::borrow::Cow; use std::io; use std::io::prelude::*; +use std::path::{Component, Path}; pub const LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x04034b50; pub const CENTRAL_DIRECTORY_HEADER_SIGNATURE: u32 = 0x02014b50; @@ -210,3 +212,26 @@ impl Zip64CentralDirectoryEnd { Ok(()) } } + +/// Converts a path to the ZIP format (forward-slash-delimited and normalized). +pub(crate) fn path_to_string>(path: T) -> String { + let mut normalized_components = Vec::new(); + + // Empty element ensures the path has a leading slash, with no extra allocation after the join + normalized_components.push(Cow::Borrowed("")); + + for component in path.as_ref().components() { + match component { + Component::Normal(os_str) => { + normalized_components.push(os_str.to_string_lossy()); + } + Component::ParentDir => { + if normalized_components.len() > 1 { + normalized_components.pop(); + } + } + _ => {} + } + } + normalized_components.join("/") +} diff --git a/src/write.rs b/src/write.rs index f52fda46..8a492075 100644 --- a/src/write.rs +++ b/src/write.rs @@ -45,6 +45,7 @@ use zopfli::Options; #[cfg(feature = "deflate-zopfli")] use std::io::BufWriter; +use std::path::Path; #[cfg(feature = "zstd")] use zstd::stream::write::Encoder as ZstdEncoder; @@ -134,6 +135,7 @@ pub use self::sealed::FileOptionExtension; use crate::result::ZipError::InvalidArchive; #[cfg(feature = "lzma")] use crate::result::ZipError::UnsupportedArchive; +use crate::spec::path_to_string; use crate::write::GenericZipWriter::{Closed, Storer}; use crate::zipcrypto::ZipCryptoKeys; use crate::CompressionMethod::Stored; @@ -932,13 +934,9 @@ impl ZipWriter { /// /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal' /// Components, such as a starting '/' or '..' and '.'. - #[deprecated( - since = "0.5.7", - note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead." - )] - pub fn start_file_from_path( + pub fn start_file_from_path>( &mut self, - path: &std::path::Path, + path: P, options: FileOptions, ) -> ZipResult<()> { self.start_file(path_to_string(path), options) @@ -1064,9 +1062,9 @@ impl ZipWriter { since = "0.5.7", note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead." )] - pub fn add_directory_from_path( + pub fn add_directory_from_path>( &mut self, - path: &std::path::Path, + path: P, options: FileOptions, ) -> ZipResult<()> { self.add_directory(path_to_string(path), options) @@ -1123,6 +1121,19 @@ impl ZipWriter { Ok(()) } + /// Add a symlink entry, taking Paths to the location and target as arguments. + /// + /// This function ensures that the '/' path separator is used. It also ignores all non 'Normal' + /// Components, such as a starting '/' or '..' and '.'. + pub fn add_symlink_from_path, T: AsRef, E: FileOptionExtension>( + &mut self, + path: P, + target: T, + options: FileOptions, + ) -> ZipResult<()> { + self.add_symlink(path_to_string(path), path_to_string(target), options) + } + fn finalize(&mut self) -> ZipResult<()> { self.finish_file()?; @@ -1677,19 +1688,6 @@ fn write_central_zip64_extra_field(writer: &mut T, file: &ZipFileData) Ok(size) } -fn path_to_string(path: &std::path::Path) -> String { - let mut path_str = String::new(); - for component in path.components() { - if let std::path::Component::Normal(os_str) = component { - if !path_str.is_empty() { - path_str.push('/'); - } - path_str.push_str(&os_str.to_string_lossy()); - } - } - path_str -} - #[cfg(not(feature = "unreserved"))] const EXTRA_FIELD_MAPPING: [u16; 49] = [ 0x0001, 0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016, @@ -1709,6 +1707,7 @@ mod test { use crate::ZipArchive; use std::io; use std::io::{Read, Write}; + use std::path::PathBuf; #[test] fn write_empty_zip() { @@ -1786,6 +1785,22 @@ mod test { ); } + #[test] + fn test_path_normalization() { + let mut path = PathBuf::new(); + path.push("foo"); + path.push("bar"); + path.push(".."); + path.push("."); + path.push("example.txt"); + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .start_file_from_path(path, SimpleFileOptions::default()) + .unwrap(); + let archive = ZipArchive::new(writer.finish().unwrap()).unwrap(); + assert_eq!(Some("/foo/example.txt"), archive.name_for_index(0)); + } + #[test] fn write_symlink_wonky_paths() { let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); @@ -1845,18 +1860,14 @@ mod test { assert_eq!(result.get_ref(), &v); } - #[cfg(test)] const RT_TEST_TEXT: &str = "And I can't stop thinking about the moments that I lost to you\ And I can't stop thinking of things I used to do\ And I can't stop making bad decisions\ And I can't stop eating stuff you make me chew\ I put on a smile like you wanna see\ Another day goes by that I long to be like you"; - #[cfg(test)] const RT_TEST_FILENAME: &str = "subfolder/sub-subfolder/can't_stop.txt"; - #[cfg(test)] const SECOND_FILENAME: &str = "different_name.xyz"; - #[cfg(test)] const THIRD_FILENAME: &str = "third_name.xyz"; #[test]