Skip to content

Commit

Permalink
Restore support for Path and fix handling of ".."
Browse files Browse the repository at this point in the history
  • Loading branch information
Pr0methean committed Apr 20, 2024
1 parent 81e44d1 commit e412d8b
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 28 deletions.
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
- `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<Path>` arguments

### Changed

- `add_directory_from_path` and `start_file_from_path` are no longer deprecated.
18 changes: 17 additions & 1 deletion src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -654,7 +655,22 @@ impl<R: Read + Seek> ZipArchive<R> {
/// Get the index of a file entry by name, if it's present.
#[inline(always)]
pub fn index_for_name(&self, name: &str) -> Option<usize> {
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<T: AsRef<Path>>(&self, path: T) -> Option<usize> {
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>(
Expand Down
25 changes: 25 additions & 0 deletions src/spec.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<T: AsRef<Path>>(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("/")
}
61 changes: 36 additions & 25 deletions src/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -932,13 +934,9 @@ impl<W: Write + Seek> ZipWriter<W> {
///
/// 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<E: FileOptionExtension>(
pub fn start_file_from_path<E: FileOptionExtension, P: AsRef<Path>>(
&mut self,
path: &std::path::Path,
path: P,
options: FileOptions<E>,
) -> ZipResult<()> {
self.start_file(path_to_string(path), options)
Expand Down Expand Up @@ -1064,9 +1062,9 @@ impl<W: Write + Seek> ZipWriter<W> {
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<T: FileOptionExtension>(
pub fn add_directory_from_path<T: FileOptionExtension, P: AsRef<Path>>(
&mut self,
path: &std::path::Path,
path: P,
options: FileOptions<T>,
) -> ZipResult<()> {
self.add_directory(path_to_string(path), options)
Expand Down Expand Up @@ -1123,6 +1121,19 @@ impl<W: Write + Seek> ZipWriter<W> {
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<P: AsRef<Path>, T: AsRef<Path>, E: FileOptionExtension>(
&mut self,
path: P,
target: T,
options: FileOptions<E>,
) -> ZipResult<()> {
self.add_symlink(path_to_string(path), path_to_string(target), options)
}

fn finalize(&mut self) -> ZipResult<()> {
self.finish_file()?;

Expand Down Expand Up @@ -1677,19 +1688,6 @@ fn write_central_zip64_extra_field<T: Write>(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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit e412d8b

Please sign in to comment.