Merge 15b2b06a72047202acd04a971002670fdf705438 into d0494504a1df4e13ecd4eeae0495066c81060cad

This commit is contained in:
Mathias Zhang 2025-07-15 15:50:16 -03:00 committed by GitHub
commit 864d3c99a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 151 additions and 83 deletions

108
Cargo.lock generated
View File

@ -93,6 +93,15 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "assert_cmd"
version = "2.0.16"
@ -126,12 +135,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bindgen"
version = "0.63.0"
@ -452,9 +455,9 @@ dependencies = [
[[package]]
name = "constant_time_eq"
version = "0.1.5"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "core_affinity"
@ -529,6 +532,17 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "diff"
version = "0.1.13"
@ -570,6 +584,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.10"
@ -611,9 +631,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.0"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"libz-sys",
@ -690,8 +710,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"wasm-bindgen",
"windows-targets",
]
@ -731,6 +753,12 @@ dependencies = [
"thiserror",
]
[[package]]
name = "hashbrown"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
[[package]]
name = "heck"
version = "0.5.0"
@ -786,6 +814,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indexmap"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "infer"
version = "0.16.0"
@ -1164,27 +1202,14 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "pbkdf2"
version = "0.11.0"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]]
@ -2135,21 +2160,44 @@ dependencies = [
]
[[package]]
name = "zip"
version = "0.6.6"
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "zip"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899"
dependencies = [
"aes",
"byteorder",
"arbitrary",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",
"getrandom 0.3.1",
"hmac",
"indexmap",
"memchr",
"pbkdf2",
"sha1",
"time",
"zeroize",
]
[[package]]

View File

@ -42,7 +42,7 @@ tempfile = "3.10.1"
time = { version = "0.3.36", default-features = false }
unrar = { version = "0.5.7", optional = true }
xz2 = "0.1.7"
zip = { version = "0.6.6", default-features = false, features = [
zip = { version = "4.2", default-features = false, features = [
"time",
"aes-crypto",
] }
@ -76,7 +76,7 @@ test-strategy = "0.4.0"
[features]
default = ["unrar", "use_zlib", "use_zstd_thin", "bzip3"]
use_zlib = ["flate2/zlib", "gzp/deflate_zlib", "zip/deflate-zlib"]
use_zlib = ["flate2/zlib", "gzp/deflate_zlib", "zip/deflate-flate2-zlib"]
use_zstd_thin = ["zstd/thin"]
allow_piped_choice = []

View File

@ -41,9 +41,7 @@ where
for idx in 0..archive.len() {
let mut file = match password {
Some(password) => archive
.by_index_decrypt(idx, password)?
.map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file"))?,
Some(password) => archive.by_index_decrypt(idx, password)?,
None => archive.by_index(idx)?,
};
let file_path = match file.enclosed_name() {
@ -65,6 +63,9 @@ where
info(format!("File {} extracted to \"{}\"", idx, file_path.display()));
}
fs::create_dir_all(&file_path)?;
#[cfg(unix)]
unix_set_permissions(&file_path, &file)?;
}
_is_file @ false => {
if let Some(path) = file_path.parent() {
@ -74,15 +75,6 @@ where
}
let file_path = strip_cur_dir(file_path.as_path());
// same reason is in _is_dir: long, often not needed text
if !quiet {
info(format!(
"extracted ({}) {:?}",
Bytes::new(file.size()),
file_path.display(),
));
}
let mode = file.unix_mode();
let is_symlink = mode.is_some_and(|mode| mode & 0o170000 == 0o120000);
@ -90,6 +82,10 @@ where
let mut target = String::new();
file.read_to_string(&mut target)?;
if !quiet {
info(format!("linking {} -> {}", file_path.display(), target));
}
#[cfg(unix)]
std::os::unix::fs::symlink(&target, file_path)?;
#[cfg(windows)]
@ -97,15 +93,22 @@ where
} else {
let mut output_file = fs::File::create(file_path)?;
io::copy(&mut file, &mut output_file)?;
set_last_modified_time(&file, file_path)?;
#[cfg(unix)]
unix_set_permissions(&file_path, &file)?;
}
set_last_modified_time(&file, file_path)?;
// same reason is in _is_dir: long, often not needed text
if !quiet {
info(format!(
"extracted ({}) {:?}",
Bytes::new(file.size()),
file_path.display(),
));
}
}
}
#[cfg(unix)]
unix_set_permissions(&file_path, &file)?;
unpacked_files += 1;
}
@ -136,9 +139,7 @@ where
for idx in 0..archive.len() {
let file_in_archive = (|| {
let zip_result = match password.clone() {
Some(password) => archive
.by_index_decrypt(idx, &password)?
.map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file")),
Some(password) => archive.by_index_decrypt(idx, &password),
None => archive.by_index(idx),
};
@ -147,7 +148,7 @@ where
Err(e) => return Err(e.into()),
};
let path = file.enclosed_name().unwrap_or(&*file.mangled_name()).to_owned();
let path = file.enclosed_name().unwrap_or_else(|| file.mangled_name()).to_owned();
let is_dir = file.is_dir();
Ok(FileInArchive { path, is_dir })
@ -174,7 +175,7 @@ where
let mut writer = zip::ZipWriter::new(writer);
// always use ZIP64 to allow compression of files larger than 4GB
// the format is widely supported and the extra 20B is negligible in most cases
let options = zip::write::FileOptions::default().large_file(true);
let options = zip::write::SimpleFileOptions::default().large_file(true);
let output_handle = Handle::from_path(output_path);
#[cfg(not(unix))]
@ -241,6 +242,8 @@ where
FinalError::with_title("Zip requires that all directories names are valid UTF-8")
.detail(format!("File at '{path:?}' has a non-UTF-8 name"))
})?;
// ZIP format requires forward slashes as path separators, regardless of platform
let entry_name = entry_name.replace(std::path::MAIN_SEPARATOR, "/");
if metadata.is_dir() {
writer.add_directory(entry_name, options)?;
@ -250,6 +253,8 @@ where
FinalError::with_title("Zip requires that all directories names are valid UTF-8")
.detail(format!("File at '{target_path:?}' has a non-UTF-8 name"))
})?;
// ZIP format requires forward slashes as path separators, regardless of platform
let target_name = target_name.replace(std::path::MAIN_SEPARATOR, "/");
// This approach writes the symlink target path as the content of the symlink entry.
// We detect symlinks during extraction by checking for the Unix symlink mode (0o120000) in the entry's permissions.
@ -286,7 +291,7 @@ where
Ok(bytes)
}
fn display_zip_comment_if_exists(file: &ZipFile) {
fn display_zip_comment_if_exists<R: Read>(file: &ZipFile<'_, R>) {
let comment = file.comment();
if !comment.is_empty() {
// Zip file comments seem to be pretty rare, but if they are used,
@ -311,23 +316,26 @@ fn get_last_modified_time(file: &fs::File) -> DateTime {
.unwrap_or_default()
}
fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
let modification_time = zip_file.last_modified().to_time();
fn set_last_modified_time<R: Read>(zip_file: &ZipFile<'_, R>, path: &Path) -> crate::Result<()> {
// Extract modification time from zip file and convert to FileTime
let file_time = zip_file
.last_modified()
.and_then(|datetime| OffsetDateTime::try_from(datetime).ok())
.map(|time| {
// Zip does not support nanoseconds, so we can assume zero here
FileTime::from_unix_time(time.unix_timestamp(), 0)
});
let Ok(time_in_seconds) = modification_time else {
return Ok(());
};
// Zip does not support nanoseconds, so we can assume zero here
let modification_time = FileTime::from_unix_time(time_in_seconds.unix_timestamp(), 0);
set_file_mtime(path, modification_time)?;
// Set the modification time if available
if let Some(modification_time) = file_time {
set_file_mtime(path, modification_time)?;
}
Ok(())
}
#[cfg(unix)]
fn unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
fn unix_set_permissions<R: Read>(file_path: &Path, file: &ZipFile<'_, R>) -> crate::Result<()> {
use std::fs::Permissions;
if let Some(mode) = file.unix_mode() {

View File

@ -27,11 +27,11 @@ pub enum Error {
/// NEEDS MORE CONTEXT
AlreadyExists { error_title: String },
/// From zip::result::ZipError::InvalidArchive
InvalidZipArchive(&'static str),
InvalidZipArchive(String),
/// Detected from io::Error if .kind() is io::ErrorKind::PermissionDenied
PermissionDenied { error_title: String },
/// From zip::result::ZipError::UnsupportedArchive
UnsupportedZipArchive(&'static str),
UnsupportedZipArchive(String),
/// We don't support compressing the root folder.
CompressingRootFolder,
/// Specialized walkdir's io::Error wrapper with additional information on the error
@ -218,11 +218,17 @@ impl From<zip::result::ZipError> for Error {
use zip::result::ZipError;
match err {
ZipError::Io(io_err) => Self::from(io_err),
ZipError::InvalidArchive(filename) => Self::InvalidZipArchive(filename),
ZipError::InvalidArchive(filename) => Self::InvalidZipArchive(filename.to_string()),
ZipError::FileNotFound => Self::Custom {
reason: FinalError::with_title("Unexpected error in zip archive").detail("File not found"),
},
ZipError::UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename),
ZipError::UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename.to_string()),
ZipError::InvalidPassword => Self::InvalidPassword {
reason: "The provided password is incorrect".to_string(),
},
_ => Self::Custom {
reason: FinalError::with_title("Unexpected error in zip archive").detail(err.to_string()),
},
}
}
}

View File

@ -52,22 +52,28 @@ pub fn list_files(
fn print_entry(out: &mut impl Write, name: impl std::fmt::Display, is_dir: bool) {
use crate::utils::colors::*;
if is_dir {
// if colors are deactivated, print final / to mark directories
if BLUE.is_empty() {
let _ = writeln!(out, "{name}/");
// if in ACCESSIBLE mode, use colors but print final / in case colors
// aren't read out aloud with a screen reader or aren't printed on a
// braille reader
} else if is_running_in_accessible_mode() {
let _ = writeln!(out, "{}{}{}/{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
} else {
let _ = writeln!(out, "{}{}{}{}", *BLUE, *STYLE_BOLD, name, *ALL_RESET);
}
} else {
// not a dir -> just print the file name
if !is_dir {
// Not a directory -> just print the file name
let _ = writeln!(out, "{name}");
return;
}
// Handle directory display
let name_str = name.to_string();
let display_name = name_str.strip_suffix('/').unwrap_or(&name_str);
let output = if BLUE.is_empty() {
// Colors are deactivated, print final / to mark directories
format!("{display_name}/")
} else if is_running_in_accessible_mode() {
// Accessible mode: use colors but print final / for screen readers
format!("{}{}{}/{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
} else {
// Normal mode: use colors without trailing slash
format!("{}{}{}{}", *BLUE, *STYLE_BOLD, display_name, *ALL_RESET)
};
let _ = writeln!(out, "{output}");
}
/// Since archives store files as a list of entries -> without direct