diff --git a/Cargo.lock b/Cargo.lock index 71b8cf9..32b10b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 5d31493..cb88f5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [] diff --git a/src/archive/zip.rs b/src/archive/zip.rs index 28546a1..4130beb 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -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))] @@ -286,7 +287,7 @@ where Ok(bytes) } -fn display_zip_comment_if_exists(file: &ZipFile) { +fn display_zip_comment_if_exists(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 +312,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(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(file_path: &Path, file: &ZipFile<'_, R>) -> crate::Result<()> { use std::fs::Permissions; if let Some(mode) = file.unix_mode() { diff --git a/src/error.rs b/src/error.rs index b3c33df..75148b4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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 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()), + }, } } }