From 90226cb0f384d18f9201c8cb0d8f7dec2f19761c Mon Sep 17 00:00:00 2001 From: ttyS3 <41882455+ttys3@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:02:02 +0000 Subject: [PATCH 1/5] feat(rar): switch to unrar-ng 0.7.4 batch extract_all_with_callback Migrate RAR decompression from upstream unrar 0.5.7 to the maintained fork unrar-ng 0.7.4 via Cargo dep alias (source-level use unrar::* preserved). Replace the per-file read_header + extract_with_base loop with OpenArchive::extract_all_with_callback, which uses the C batch path internally and avoids per-file FFI overhead -- noticeably faster for archives with many small files. Behavioral notes: - Directory entries in the archive are now materialized on disk by the C library; the previous loop explicitly skipped non-file headers so empty directories were not created. Most users expect the archive's original directory layout, so this is treated as a fix. - Per-file errors are captured in the callback and surfaced as Error::Custom with a "failed to extract " title plus a human-readable detail, rather than relying on a bare From conversion (which omitted the filename context). The Err arm returns false to cancel the rest of the extraction -- matching the previous loop's ?-on-first-error semantics and preventing the first_err.is_none() guard from silently swallowing any subsequent per-file errors the C library might surface. - From now formats via Display (err.to_string()) instead of the previous Debug-formatted err.code, so messages such as "Wrong password was specified" replace the bare BadPassword enum debug text. Covers LargeDict and the new Unmapped(i32) fallback too. - Closure handles the new ExtractEvent::LargeDictWarning by emitting an info line with the required vs supported dictionary size, then returning false to reject the oversized dictionary so the DLL surfaces the failure as Err(Code::LargeDict). Returning true would permit extraction to proceed with a dictionary the build cannot actually decompress, which is not the behavior we want. - Pinned to 0.7.4 (not 0.7.3) because 0.7.3 fails to compile on Windows: the const offset_of assertions in unrar-ng-sys hard-coded the HeaderDataEx field offsets for 4-byte wchar_t (Linux/macOS) and panicked at compile time on Windows where wchar_t is 2 bytes. 0.7.4 parameterizes those offsets by sizeof(wchar_t). Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 16 ++++++------ Cargo.toml | 2 +- src/archive/rar.rs | 62 +++++++++++++++++++++++++++++++++------------- src/error.rs | 2 +- 4 files changed, 55 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d92e4b2ab..8379d3ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,7 +1202,7 @@ dependencies = [ "tempfile", "test-strategy", "time", - "unrar", + "unrar-ng", "xz2", "zip", "zstd", @@ -1830,22 +1830,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] -name = "unrar" -version = "0.5.8" +name = "unrar-ng" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ec61343a630d2b50d13216dea5125e157d3fc180a7d3f447d22fe146b648fc" +checksum = "2846729dd791bdcc5d65d30130808e9d75661ca75dc92dea4fea1614b4f7bd50" dependencies = [ "bitflags 2.8.0", "regex", - "unrar_sys", + "unrar-ng-sys", "widestring", ] [[package]] -name = "unrar_sys" -version = "0.5.8" +name = "unrar-ng-sys" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b77675b883cfbe6bf41e6b7a5cd6008e0a83ba497de3d96e41a064bbeead765" +checksum = "4b5cbe5d90923c67a24c1337c80a7478d93f9c5999ab3d7008a70678d60bad68" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 7ae38f409..b00bc4e1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ strum = { version = "0.28.0", features = ["derive"] } tar = "0.4.42" tempfile = "3.10.1" time = { version = "0.3.36", default-features = false } -unrar = { version = "0.5.7", optional = true } +unrar = { package = "unrar-ng", version = "0.7.4", optional = true } xz2 = "0.1.7" zip = { version = "6", default-features = false, features = [ "time", diff --git a/src/archive/rar.rs b/src/archive/rar.rs index 5c92af413..6b919971f 100644 --- a/src/archive/rar.rs +++ b/src/archive/rar.rs @@ -1,11 +1,14 @@ //! Contains RAR-specific building and unpacking functions -use std::path::Path; +use std::path::{Path, PathBuf}; -use unrar::Archive; +use unrar::{ + Archive, ExtractEvent, + error::{Code, UnrarError, When}, +}; use crate::{ - error::{Error, Result}, + error::{Error, FinalError, Result}, info, list::{FileInArchive, ListFileType}, utils::BytesFmt, @@ -19,24 +22,49 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, password: Optio None => Archive::new(archive_path), }; - let mut archive = archive.open_for_processing()?; - let mut files_unpacked = 0; + let archive = archive.open_for_processing()?; + + let mut files_unpacked: u64 = 0; + let mut first_err: Option<(PathBuf, i32)> = None; - while let Some(header) = archive.read_header()? { - let entry = header.entry(); - archive = if entry.is_file() { + let cb_result = archive.extract_all_with_callback(output_folder, |event| match event { + ExtractEvent::Start { filename, size } => { + info!("extracted ({}) {}", BytesFmt(size), filename.display()); + true + } + ExtractEvent::Ok { .. } => { + files_unpacked += 1; + true + } + ExtractEvent::Err { filename, error_code } => { + first_err = Some((filename, error_code)); + // Returning false cancels the rest of the extraction so any + // additional per-file errors don't get silently swallowed. + false + } + ExtractEvent::LargeDictWarning { + dict_size_kb, + max_dict_size_kb, + } => { info!( - "extracted ({}) {}", - BytesFmt(entry.unpacked_size), - entry.filename.display(), + "archive requires {} KiB dictionary; this build supports up to {} KiB", + dict_size_kb, max_dict_size_kb, ); - files_unpacked += 1; - header.extract_with_base(output_folder)? - } else { - header.skip()? - }; - } + // Reject the oversized dictionary so the DLL fails the + // operation with Code::LargeDict instead of silently + // proceeding with a result it cannot actually produce. + false + } + _ => true, + }); + if let Some((path, code)) = first_err { + let inner = UnrarError::from(Code::from(code), When::Process).to_string(); + return Err(Error::Custom { + reason: FinalError::with_title(format!("failed to extract {}", path.display())).detail(inner), + }); + } + let _status = cb_result?; Ok(files_unpacked) } diff --git a/src/error.rs b/src/error.rs index c05e93190..688d6612d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -249,7 +249,7 @@ impl From for Error { impl From for Error { fn from(err: unrar::error::UnrarError) -> Self { Self::Custom { - reason: FinalError::with_title("Unexpected error in rar archive").detail(format!("{:?}", err.code)), + reason: FinalError::with_title("Unexpected error in rar archive").detail(err.to_string()), } } } From a0809092dd52912d5433f46db6bd5281646754fb Mon Sep 17 00:00:00 2001 From: ttyS3 <41882455+ttys3@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:40:55 +0000 Subject: [PATCH 2/5] fix(rar): log filename only after successful extraction ExtractEvent::Start fires before per-file extraction begins, so the "extracted (size) filename" line printed too early -- before the bytes actually landed on disk, and even when the file subsequently errored out. Move the info call to ExtractEvent::Ok and stash the size from Start (Ok carries only the filename), so the log line now reflects a completed extraction. The wording matches zip/sevenz, which already log post-success. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/archive/rar.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/archive/rar.rs b/src/archive/rar.rs index 6b919971f..f59d46289 100644 --- a/src/archive/rar.rs +++ b/src/archive/rar.rs @@ -26,13 +26,15 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, password: Optio let mut files_unpacked: u64 = 0; let mut first_err: Option<(PathBuf, i32)> = None; + let mut pending_size: u64 = 0; let cb_result = archive.extract_all_with_callback(output_folder, |event| match event { - ExtractEvent::Start { filename, size } => { - info!("extracted ({}) {}", BytesFmt(size), filename.display()); + ExtractEvent::Start { size, .. } => { + pending_size = size; true } - ExtractEvent::Ok { .. } => { + ExtractEvent::Ok { filename } => { + info!("extracted ({}) {}", BytesFmt(pending_size), filename.display()); files_unpacked += 1; true } From d58327921a44223698f94dd105134175bb9b2f8a Mon Sep 17 00:00:00 2001 From: ttyS3 <41882455+ttys3@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:54:12 +0000 Subject: [PATCH 3/5] refactor(rar): use ExtractEvent::Ok.size field from unrar-ng 0.7.5 unrar-ng 0.7.5 added the uncompressed file size to the ExtractEvent::Ok variant (it was previously only on Start), so the ouch-side workaround that stashed pending_size between Start and Ok events is no longer necessary. Drop the pending_size local and the ExtractEvent::Start arm; read the size directly from Ok. Bumps the dep alias from 0.7.4 to 0.7.5 and syncs Cargo.lock. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- src/archive/rar.rs | 9 ++------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8379d3ce1..89844a8df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1831,9 +1831,9 @@ checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unrar-ng" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2846729dd791bdcc5d65d30130808e9d75661ca75dc92dea4fea1614b4f7bd50" +checksum = "ce040dcf1c5a2ffa16abc251813a0766fdf753afe468d6015800368cf1913b07" dependencies = [ "bitflags 2.8.0", "regex", @@ -1843,9 +1843,9 @@ dependencies = [ [[package]] name = "unrar-ng-sys" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b5cbe5d90923c67a24c1337c80a7478d93f9c5999ab3d7008a70678d60bad68" +checksum = "a121a227a18dfd67638398304b5a0544c7e328912d0c314cd2f80b4b504b51a7" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index b00bc4e1c..eb0763a71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ strum = { version = "0.28.0", features = ["derive"] } tar = "0.4.42" tempfile = "3.10.1" time = { version = "0.3.36", default-features = false } -unrar = { package = "unrar-ng", version = "0.7.4", optional = true } +unrar = { package = "unrar-ng", version = "0.7.5", optional = true } xz2 = "0.1.7" zip = { version = "6", default-features = false, features = [ "time", diff --git a/src/archive/rar.rs b/src/archive/rar.rs index f59d46289..e43320197 100644 --- a/src/archive/rar.rs +++ b/src/archive/rar.rs @@ -26,15 +26,10 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, password: Optio let mut files_unpacked: u64 = 0; let mut first_err: Option<(PathBuf, i32)> = None; - let mut pending_size: u64 = 0; let cb_result = archive.extract_all_with_callback(output_folder, |event| match event { - ExtractEvent::Start { size, .. } => { - pending_size = size; - true - } - ExtractEvent::Ok { filename } => { - info!("extracted ({}) {}", BytesFmt(pending_size), filename.display()); + ExtractEvent::Ok { filename, size } => { + info!("extracted ({}) {}", BytesFmt(size), filename.display()); files_unpacked += 1; true } From 3ac35fa9d190beaf8dcf1d9edd0d05926c2b2839 Mon Sep 17 00:00:00 2001 From: ttyS3 <41882455+ttys3@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:55:19 +0000 Subject: [PATCH 4/5] style(rar): use PathFmt for path formatting in logs and errors Other archive backends (zip, sevenz, tar) format paths with PathFmt, which wraps the path in quotes and strips the leading "./" noise via NoQuotePathFmt. Switch the rar backend's two raw .display() sites (the per-file extracted log and the failed-to-extract error title) over to PathFmt so the user-facing output matches the rest of the project. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/archive/rar.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/archive/rar.rs b/src/archive/rar.rs index e43320197..acc55575b 100644 --- a/src/archive/rar.rs +++ b/src/archive/rar.rs @@ -11,7 +11,7 @@ use crate::{ error::{Error, FinalError, Result}, info, list::{FileInArchive, ListFileType}, - utils::BytesFmt, + utils::{BytesFmt, PathFmt}, }; /// Unpacks the archive given by `archive_path` into the folder given by `output_folder`. @@ -29,7 +29,7 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, password: Optio let cb_result = archive.extract_all_with_callback(output_folder, |event| match event { ExtractEvent::Ok { filename, size } => { - info!("extracted ({}) {}", BytesFmt(size), filename.display()); + info!("extracted ({}) {}", BytesFmt(size), PathFmt(&filename)); files_unpacked += 1; true } @@ -58,7 +58,7 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, password: Optio if let Some((path, code)) = first_err { let inner = UnrarError::from(Code::from(code), When::Process).to_string(); return Err(Error::Custom { - reason: FinalError::with_title(format!("failed to extract {}", path.display())).detail(inner), + reason: FinalError::with_title(format!("failed to extract {}", PathFmt(&path))).detail(inner), }); } let _status = cb_result?; From 16636a784b56b4df18fb04526f8c875c0e6b9de8 Mon Sep 17 00:00:00 2001 From: ttyS3 <41882455+ttys3@users.noreply.github.com> Date: Mon, 4 May 2026 08:23:45 +0000 Subject: [PATCH 5/5] chore(deps): update unrar-ng to 0.7.6 Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89844a8df..c8c2538c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1831,9 +1831,9 @@ checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unrar-ng" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce040dcf1c5a2ffa16abc251813a0766fdf753afe468d6015800368cf1913b07" +checksum = "7881a7cefee603cdd23e6acd424ee62c64e088d39688f60d20649d978de55f41" dependencies = [ "bitflags 2.8.0", "regex", @@ -1843,9 +1843,9 @@ dependencies = [ [[package]] name = "unrar-ng-sys" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a121a227a18dfd67638398304b5a0544c7e328912d0c314cd2f80b4b504b51a7" +checksum = "afa06861a15d69012d3c1035510e9ca416031ae37b6d27bb7e3ab1a2984e1230" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index eb0763a71..38e179190 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ strum = { version = "0.28.0", features = ["derive"] } tar = "0.4.42" tempfile = "3.10.1" time = { version = "0.3.36", default-features = false } -unrar = { package = "unrar-ng", version = "0.7.5", optional = true } +unrar = { package = "unrar-ng", version = "0.7.6", optional = true } xz2 = "0.1.7" zip = { version = "6", default-features = false, features = [ "time",