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

View File

@ -42,7 +42,7 @@ tempfile = "3.10.1"
time = { version = "0.3.36", default-features = false } time = { version = "0.3.36", default-features = false }
unrar = { version = "0.5.7", optional = true } unrar = { version = "0.5.7", optional = true }
xz2 = "0.1.7" xz2 = "0.1.7"
zip = { version = "0.6.6", default-features = false, features = [ zip = { version = "4.2", default-features = false, features = [
"time", "time",
"aes-crypto", "aes-crypto",
] } ] }
@ -76,7 +76,7 @@ test-strategy = "0.4.0"
[features] [features]
default = ["unrar", "use_zlib", "use_zstd_thin", "bzip3"] 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"] use_zstd_thin = ["zstd/thin"]
allow_piped_choice = [] allow_piped_choice = []

View File

@ -41,9 +41,7 @@ where
for idx in 0..archive.len() { for idx in 0..archive.len() {
let mut file = match password { let mut file = match password {
Some(password) => archive Some(password) => archive.by_index_decrypt(idx, password)?,
.by_index_decrypt(idx, password)?
.map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file"))?,
None => archive.by_index(idx)?, None => archive.by_index(idx)?,
}; };
let file_path = match file.enclosed_name() { let file_path = match file.enclosed_name() {
@ -65,6 +63,9 @@ where
info(format!("File {} extracted to \"{}\"", idx, file_path.display())); info(format!("File {} extracted to \"{}\"", idx, file_path.display()));
} }
fs::create_dir_all(&file_path)?; fs::create_dir_all(&file_path)?;
#[cfg(unix)]
unix_set_permissions(&file_path, &file)?;
} }
_is_file @ false => { _is_file @ false => {
if let Some(path) = file_path.parent() { if let Some(path) = file_path.parent() {
@ -74,15 +75,6 @@ where
} }
let file_path = strip_cur_dir(file_path.as_path()); 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 mode = file.unix_mode();
let is_symlink = mode.is_some_and(|mode| mode & 0o170000 == 0o120000); let is_symlink = mode.is_some_and(|mode| mode & 0o170000 == 0o120000);
@ -90,6 +82,10 @@ where
let mut target = String::new(); let mut target = String::new();
file.read_to_string(&mut target)?; file.read_to_string(&mut target)?;
if !quiet {
info(format!("linking {} -> {}", file_path.display(), target));
}
#[cfg(unix)] #[cfg(unix)]
std::os::unix::fs::symlink(&target, file_path)?; std::os::unix::fs::symlink(&target, file_path)?;
#[cfg(windows)] #[cfg(windows)]
@ -97,14 +93,21 @@ where
} else { } else {
let mut output_file = fs::File::create(file_path)?; let mut output_file = fs::File::create(file_path)?;
io::copy(&mut file, &mut output_file)?; io::copy(&mut file, &mut output_file)?;
}
set_last_modified_time(&file, file_path)?; set_last_modified_time(&file, file_path)?;
}
}
#[cfg(unix)] #[cfg(unix)]
unix_set_permissions(&file_path, &file)?; unix_set_permissions(&file_path, &file)?;
}
// same reason is in _is_dir: long, often not needed text
if !quiet {
info(format!(
"extracted ({}) {:?}",
Bytes::new(file.size()),
file_path.display(),
));
}
}
}
unpacked_files += 1; unpacked_files += 1;
} }
@ -136,9 +139,7 @@ where
for idx in 0..archive.len() { for idx in 0..archive.len() {
let file_in_archive = (|| { let file_in_archive = (|| {
let zip_result = match password.clone() { let zip_result = match password.clone() {
Some(password) => archive Some(password) => archive.by_index_decrypt(idx, &password),
.by_index_decrypt(idx, &password)?
.map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file")),
None => archive.by_index(idx), None => archive.by_index(idx),
}; };
@ -147,7 +148,7 @@ where
Err(e) => return Err(e.into()), 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(); let is_dir = file.is_dir();
Ok(FileInArchive { path, is_dir }) Ok(FileInArchive { path, is_dir })
@ -174,7 +175,7 @@ where
let mut writer = zip::ZipWriter::new(writer); let mut writer = zip::ZipWriter::new(writer);
// always use ZIP64 to allow compression of files larger than 4GB // 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 // 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); let output_handle = Handle::from_path(output_path);
#[cfg(not(unix))] #[cfg(not(unix))]
@ -241,6 +242,8 @@ where
FinalError::with_title("Zip requires that all directories names are valid UTF-8") FinalError::with_title("Zip requires that all directories names are valid UTF-8")
.detail(format!("File at '{path:?}' has a non-UTF-8 name")) .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() { if metadata.is_dir() {
writer.add_directory(entry_name, options)?; writer.add_directory(entry_name, options)?;
@ -250,6 +253,8 @@ where
FinalError::with_title("Zip requires that all directories names are valid UTF-8") 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")) .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. // 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. // 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) 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(); let comment = file.comment();
if !comment.is_empty() { if !comment.is_empty() {
// Zip file comments seem to be pretty rare, but if they are used, // 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() .unwrap_or_default()
} }
fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> { fn set_last_modified_time<R: Read>(zip_file: &ZipFile<'_, R>, path: &Path) -> crate::Result<()> {
let modification_time = zip_file.last_modified().to_time(); // Extract modification time from zip file and convert to FileTime
let file_time = zip_file
let Ok(time_in_seconds) = modification_time else { .last_modified()
return Ok(()); .and_then(|datetime| OffsetDateTime::try_from(datetime).ok())
}; .map(|time| {
// Zip does not support nanoseconds, so we can assume zero here // Zip does not support nanoseconds, so we can assume zero here
let modification_time = FileTime::from_unix_time(time_in_seconds.unix_timestamp(), 0); FileTime::from_unix_time(time.unix_timestamp(), 0)
});
// Set the modification time if available
if let Some(modification_time) = file_time {
set_file_mtime(path, modification_time)?; set_file_mtime(path, modification_time)?;
}
Ok(()) Ok(())
} }
#[cfg(unix)] #[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; use std::fs::Permissions;
if let Some(mode) = file.unix_mode() { if let Some(mode) = file.unix_mode() {

View File

@ -27,11 +27,11 @@ pub enum Error {
/// NEEDS MORE CONTEXT /// NEEDS MORE CONTEXT
AlreadyExists { error_title: String }, AlreadyExists { error_title: String },
/// From zip::result::ZipError::InvalidArchive /// From zip::result::ZipError::InvalidArchive
InvalidZipArchive(&'static str), InvalidZipArchive(String),
/// Detected from io::Error if .kind() is io::ErrorKind::PermissionDenied /// Detected from io::Error if .kind() is io::ErrorKind::PermissionDenied
PermissionDenied { error_title: String }, PermissionDenied { error_title: String },
/// From zip::result::ZipError::UnsupportedArchive /// From zip::result::ZipError::UnsupportedArchive
UnsupportedZipArchive(&'static str), UnsupportedZipArchive(String),
/// We don't support compressing the root folder. /// We don't support compressing the root folder.
CompressingRootFolder, CompressingRootFolder,
/// Specialized walkdir's io::Error wrapper with additional information on the error /// 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; use zip::result::ZipError;
match err { match err {
ZipError::Io(io_err) => Self::from(io_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 { ZipError::FileNotFound => Self::Custom {
reason: FinalError::with_title("Unexpected error in zip archive").detail("File not found"), 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) { fn print_entry(out: &mut impl Write, name: impl std::fmt::Display, is_dir: bool) {
use crate::utils::colors::*; use crate::utils::colors::*;
if is_dir { if !is_dir {
// if colors are deactivated, print final / to mark directories // Not a directory -> just print the file name
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
let _ = writeln!(out, "{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 /// Since archives store files as a list of entries -> without direct