From df5b174d5ef02f5220fa7f132ef12e6c5c631851 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:39:01 +1000 Subject: [PATCH 01/19] chore(ci): ignore RUSTSEC-2026-0173 proc-macro-error2 advisory cargo deny fails on the new unmaintained advisory for proc-macro-error2, a transitive dependency with no safe upgrade path yet. --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index ba05aaf90d..33b6baf1cc 100644 --- a/deny.toml +++ b/deny.toml @@ -71,6 +71,7 @@ feature-depth = 1 # output a note when they are encountered. ignore = [ { id = "RUSTSEC-2024-0370", reason = "proc-macro-error dependency from sigstore crate - no safe upgrade available" }, + { id = "RUSTSEC-2026-0173", reason = "proc-macro-error2 unmaintained - transitive via age/mlua/tabled/rops, no safe upgrade available" }, { id = "RUSTSEC-2023-0071", reason = "rsa crate Marvin attack vulnerability from sigstore crate - no safe upgrade available" }, { id = "RUSTSEC-2025-0119", reason = "number_prefix crate is unmaintained - used by indicatif/self_update, no safe upgrade available" }, # rustls-webpki 0.102.8 advisories — pulled in transitively by sigstore-tsa From dd21792b372384566dcc29773d25cb2c7edcb114 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sat, 6 Jun 2026 05:08:16 +1000 Subject: [PATCH 02/19] refactor(file): split unarchive from untar --- src/backend/aqua.rs | 34 +---- src/backend/http.rs | 15 +- src/backend/spm.rs | 6 +- src/backend/static_helpers.rs | 15 +- src/cli/generate/tool_stub.rs | 19 +-- src/file.rs | 270 ++++++++++++++++++++++++++-------- src/plugins/core/java.rs | 28 ++-- src/plugins/core/python.rs | 9 +- src/plugins/core/zig.rs | 9 +- 9 files changed, 260 insertions(+), 145 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 27f1473202..c8882f9b45 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -7,7 +7,7 @@ use crate::backend::static_helpers::get_filename_from_url; use crate::cli::args::BackendArg; use crate::cli::version::{ARCH, OS}; use crate::config::Settings; -use crate::file::{TarFormat, TarOptions}; +use crate::file::{ArchiveOptions, TarFormat}; use crate::http::HTTP; use crate::install_context::InstallContext; use crate::lockfile::{PlatformInfo, ProvenanceType}; @@ -2282,13 +2282,14 @@ impl AquaBackend { let first_bin_path = bin_paths .first() .expect("at least one bin path should exist"); - let tar_opts = TarOptions { + let archive_opts = ArchiveOptions { pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(TarFormat::from_ext(format)) + single_file_dest: Some(first_bin_path), + ..Default::default() }; let mut make_executable = false; if let AquaPackageType::GithubArchive = pkg.r#type { - file::untar(&tarball_path, &install_path, &tar_opts)?; + file::unarchive_with_format(&tarball_path, &install_path, format, &archive_opts)?; } else if let AquaPackageType::GithubContent = pkg.r#type { file::create_dir_all(&install_path)?; file::copy(&tarball_path, first_bin_path)?; @@ -2297,34 +2298,13 @@ impl AquaBackend { file::create_dir_all(&install_path)?; file::copy(&tarball_path, first_bin_path)?; make_executable = true; - } else if format.starts_with("tar") || (format == "7z" && cfg!(windows)) { - file::untar(&tarball_path, &install_path, &tar_opts)?; - make_executable = true; - } else if format == "zip" { - file::unzip(&tarball_path, &install_path, &Default::default())?; - make_executable = true; - } else if format == "gz" { - file::create_dir_all(&install_path)?; - file::un_gz(&tarball_path, first_bin_path)?; - make_executable = true; - } else if format == "xz" { - file::create_dir_all(&install_path)?; - file::un_xz(&tarball_path, first_bin_path)?; - make_executable = true; - } else if format == "zst" { - file::create_dir_all(&install_path)?; - file::un_zst(&tarball_path, first_bin_path)?; - make_executable = true; - } else if format == "bz2" { - file::create_dir_all(&install_path)?; - file::un_bz2(&tarball_path, first_bin_path)?; - make_executable = true; } else if format == "dmg" { file::un_dmg(&tarball_path, &install_path)?; } else if format == "pkg" { file::un_pkg(&tarball_path, &install_path)?; } else { - bail!("unsupported format: {}", format); + file::unarchive_with_format(&tarball_path, &install_path, format, &archive_opts)?; + make_executable = true; } if make_executable { diff --git a/src/backend/http.rs b/src/backend/http.rs index 960406833c..1c0295387e 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -393,14 +393,7 @@ impl HttpBackend { pr.set_message(format!("extract {}", file_info.file_name())); } - file::untar( - file_path, - &dest_file, - &file::TarOptions { - pr, - ..file::TarOptions::new(file_info.format) - }, - )?; + file::un_compressed_file(file_path, &dest_file, file_info.format)?; file::make_executable(&dest_file)?; Ok(ExtractionType::RawFile { filename }) @@ -451,14 +444,14 @@ impl HttpBackend { strip_components = Some(1); } - let tar_opts = file::TarOptions { - format: file_info.format, + let archive_opts = file::ArchiveOptions { strip_components: strip_components.unwrap_or(0), pr, preserve_mtime: false, + ..Default::default() }; - file::untar(file_path, dest, &tar_opts)?; + file::unarchive(file_path, dest, file_info.format, &archive_opts)?; // Handle rename_exe option for archives if let Some(rename_to) = opts.rename_exe() { diff --git a/src/backend/spm.rs b/src/backend/spm.rs index 732a6eac7e..632d145eb7 100644 --- a/src/backend/spm.rs +++ b/src/backend/spm.rs @@ -483,11 +483,7 @@ impl SPMBackend { self.verify_checksum(ctx, tv, &download_path)?; ctx.pr.set_message(format!("extract {}", asset.name)); - file::untar( - &download_path, - &bundle_dir, - &file::TarOptions::new(file::TarFormat::Zip), - )?; + file::unzip(&download_path, &bundle_dir, &Default::default())?; let triples = swift_target_triples(ctx, self, tv).await?; let binaries = artifactbundle_binaries(&bundle_dir, &triples)?; diff --git a/src/backend/static_helpers.rs b/src/backend/static_helpers.rs index 642c20052f..88c57980df 100644 --- a/src/backend/static_helpers.rs +++ b/src/backend/static_helpers.rs @@ -435,14 +435,7 @@ pub fn install_artifact( install_path.join(cleaned_name) }; - file::untar( - file_path, - &dest, - &file::TarOptions { - pr, - ..file::TarOptions::new(format) - }, - )?; + file::un_compressed_file(file_path, &dest, format)?; file::make_executable(&dest)?; } else if format == file::TarFormat::Raw { @@ -479,14 +472,14 @@ pub fn install_artifact( debug!("Auto-detected single directory archive, extracting with strip_components=1"); strip_components = Some(1); } - let tar_opts = file::TarOptions { + let archive_opts = file::ArchiveOptions { strip_components: strip_components.unwrap_or(0), pr, - ..file::TarOptions::new(format) + ..Default::default() }; // Extract with determined strip_components - file::untar(file_path, &install_path, &tar_opts)?; + file::unarchive(file_path, &install_path, format, &archive_opts)?; // Extract just the repo name from tool_name (e.g., "opsgenie/opsgenie-lamp" -> "opsgenie-lamp") let full_tool_name = tv.ba().tool_name.as_str(); diff --git a/src/cli/generate/tool_stub.rs b/src/cli/generate/tool_stub.rs index b697bfed57..d8577df85e 100644 --- a/src/cli/generate/tool_stub.rs +++ b/src/cli/generate/tool_stub.rs @@ -4,7 +4,7 @@ use crate::backend::platform_target::PlatformTarget; use crate::backend::static_helpers::get_filename_from_url; use crate::cli::tool_stub::ToolStubFile; use crate::config::Config; -use crate::file::{self, TarFormat, TarOptions}; +use crate::file::{self, TarFormat}; use crate::http::HTTP; use crate::lockfile::PlatformInfo; use crate::minisign; @@ -508,16 +508,17 @@ exec "$MISE_BIN" tool-stub "$0" "$@" std::fs::create_dir_all(&extracted_dir)?; // Try extraction using mise's built-in extraction logic (reuse the passed progress reporter) - let tar_opts = TarOptions { + let format = TarFormat::from_file_name( + &archive_path + .file_name() + .unwrap_or_default() + .to_string_lossy(), + ); + let archive_opts = file::ArchiveOptions { pr: Some(pr), - ..TarOptions::new(TarFormat::from_file_name( - &archive_path - .file_name() - .unwrap_or_default() - .to_string_lossy(), - )) + ..Default::default() }; - file::untar(archive_path, &extracted_dir, &tar_opts)?; + file::unarchive(archive_path, &extracted_dir, format, &archive_opts)?; // Check if strip_components would be applied during actual installation let format = diff --git a/src/file.rs b/src/file.rs index 19b518db83..72d44218d7 100644 --- a/src/file.rs +++ b/src/file.rs @@ -881,6 +881,22 @@ pub fn un_bz2(input: &Path, dest: &Path) -> Result<()> { Ok(()) } +pub fn un_compressed_file(input: &Path, dest: &Path, format: TarFormat) -> Result<()> { + if let Some(parent) = dest.parent() + && !parent.as_os_str().is_empty() + { + create_dir_all(parent)?; + } + + match format { + TarFormat::Gz => un_gz(input, dest), + TarFormat::Xz => un_xz(input, dest), + TarFormat::Zst => un_zst(input, dest), + TarFormat::Bz2 => un_bz2(input, dest), + _ => bail!("unsupported compressed file format: {}", format), + } +} + #[derive(Debug, Clone, Copy, PartialEq, strum::EnumString, strum::Display)] pub enum TarFormat { #[strum(serialize = "tar.gz", serialize = "tgz")] @@ -947,6 +963,28 @@ impl TarFormat { } } + pub fn is_tar_archive(&self) -> bool { + matches!( + self, + TarFormat::TarGz + | TarFormat::TarXz + | TarFormat::TarBz2 + | TarFormat::TarZst + | TarFormat::Tar + ) + } + + pub fn is_compressed_file(&self) -> bool { + matches!( + self, + TarFormat::Gz | TarFormat::Xz | TarFormat::Bz2 | TarFormat::Zst + ) + } + + fn can_open_with_tar(&self) -> bool { + self.is_tar_archive() || *self == TarFormat::Raw + } + pub fn extension(&self) -> Option<&'static str> { match self { TarFormat::TarGz => Some("tar.gz"), @@ -965,6 +1003,36 @@ impl TarFormat { } } +pub struct ArchiveOptions<'a> { + pub strip_components: usize, + pub pr: Option<&'a dyn SingleReport>, + /// When false, files will be extracted with current timestamp instead of archive's mtime + pub preserve_mtime: bool, + pub single_file_dest: Option<&'a Path>, +} + +impl<'a> Default for ArchiveOptions<'a> { + fn default() -> Self { + Self { + strip_components: 0, + pr: None, + preserve_mtime: true, + single_file_dest: None, + } + } +} + +impl<'a> ArchiveOptions<'a> { + fn tar_options(&self, format: TarFormat) -> TarOptions<'a> { + TarOptions { + format, + strip_components: self.strip_components, + pr: self.pr, + preserve_mtime: self.preserve_mtime, + } + } +} + pub struct TarOptions<'a> { pub format: TarFormat, pub strip_components: usize, @@ -984,24 +1052,84 @@ impl<'a> TarOptions<'a> { } } -pub fn untar(archive: &Path, dest: &Path, opts: &TarOptions) -> Result<()> { - if opts.format == TarFormat::Zip { - return unzip( +pub fn unarchive_with_format( + archive: &Path, + dest: &Path, + format: &str, + opts: &ArchiveOptions, +) -> Result<()> { + let archive_format = TarFormat::from_ext(format); + if archive_format == TarFormat::Raw && !format.eq_ignore_ascii_case("raw") { + bail!("unsupported format: {}", format); + } + unarchive(archive, dest, archive_format, opts) +} + +pub fn unarchive( + archive: &Path, + dest: &Path, + format: TarFormat, + opts: &ArchiveOptions, +) -> Result<()> { + match format { + format if format.can_open_with_tar() => untar(archive, dest, &opts.tar_options(format)), + TarFormat::Zip => unzip( archive, dest, &ZipOptions { strip_components: opts.strip_components, }, - ); - } else if opts.format == TarFormat::SevenZip { - #[cfg(windows)] - return un7z( - archive, - dest, - &SevenZipOptions { - strip_components: opts.strip_components, - }, - ); + ), + TarFormat::SevenZip => { + #[cfg(windows)] + { + return un7z( + archive, + dest, + &SevenZipOptions { + strip_components: opts.strip_components, + }, + ); + } + #[cfg(not(windows))] + { + bail!("7z format not supported on this platform"); + } + } + format if format.is_compressed_file() => { + let out_path = opts + .single_file_dest + .map(Path::to_path_buf) + .unwrap_or_else(|| compressed_file_dest(archive, dest)); + un_compressed_file(archive, &out_path, format) + } + TarFormat::Raw => unreachable!("raw is handled as a legacy tar format"), + TarFormat::TarGz + | TarFormat::TarXz + | TarFormat::TarBz2 + | TarFormat::TarZst + | TarFormat::Tar + | TarFormat::Gz + | TarFormat::Xz + | TarFormat::Bz2 + | TarFormat::Zst => unreachable!("handled by guarded match arms above"), + } +} + +fn compressed_file_dest(archive: &Path, dest: &Path) -> PathBuf { + if dest.is_dir() { + let name = archive + .file_stem() + .unwrap_or_else(|| archive.file_name().unwrap()); + dest.join(name) + } else { + dest.to_path_buf() + } +} + +pub fn untar(archive: &Path, dest: &Path, opts: &TarOptions) -> Result<()> { + if !opts.format.can_open_with_tar() { + bail!("untar only supports tar formats, got {}", opts.format); } debug!("tar -xf {} -C {}", archive.display(), dest.display()); @@ -1019,27 +1147,6 @@ pub fn untar(archive: &Path, dest: &Path, opts: &TarOptions) -> Result<()> { }; let format = opts.format; - if !format.is_archive() && format != TarFormat::Raw { - let mut reader = open_tar(format, archive)?; - // If dest is a directory, join with the archive filename (minus extension) - // If dest is not a dir, assume it's the target file path - let out_path = if dest.is_dir() { - let name = archive - .file_stem() - .unwrap_or_else(|| archive.file_name().unwrap()); - dest.join(name) - } else { - dest.to_path_buf() - }; - - if let Some(parent) = out_path.parent() { - create_dir_all(parent).wrap_err_with(err)?; - } - let mut out = File::create(&out_path).wrap_err_with(err)?; - std::io::copy(&mut reader, &mut out).wrap_err_with(err)?; - return Ok(()); - } - let tar = open_tar(format, archive)?; // TODO: put this back in when we can read+write in parallel // let mut cur = Cursor::new(vec![]); @@ -1125,11 +1232,14 @@ fn open_tar(format: TarFormat, archive: &Path) -> Result> let f = File::open(archive)?; Ok(match format { // TODO: we probably shouldn't assume raw is tar.gz, but this was to retain existing behavior - TarFormat::TarGz | TarFormat::Gz | TarFormat::Raw => Box::new(GzDecoder::new(f)), - TarFormat::TarXz | TarFormat::Xz => Box::new(xz2::read::XzDecoder::new(f)), - TarFormat::TarBz2 | TarFormat::Bz2 => Box::new(BzDecoder::new(f)), - TarFormat::TarZst | TarFormat::Zst => Box::new(zstd::stream::read::Decoder::new(f)?), + TarFormat::TarGz | TarFormat::Raw => Box::new(GzDecoder::new(f)), + TarFormat::TarXz => Box::new(xz2::read::XzDecoder::new(f)), + TarFormat::TarBz2 => Box::new(BzDecoder::new(f)), + TarFormat::TarZst => Box::new(zstd::stream::read::Decoder::new(f)?), TarFormat::Tar => Box::new(f), + TarFormat::Gz | TarFormat::Xz | TarFormat::Bz2 | TarFormat::Zst => { + bail!("{} is not a tar archive", format) + } TarFormat::Zip => bail!("zip format not supported"), TarFormat::SevenZip => bail!("7z format not supported"), }) @@ -1913,7 +2023,7 @@ mod tests { } #[test] - fn test_untar_single_file() { + fn test_unarchive_single_file() { use flate2::Compression; use flate2::write::GzEncoder; use std::io::Write; @@ -1929,16 +2039,8 @@ mod tests { encoder.write_all(b"hello world").unwrap(); encoder.finish().unwrap(); - // untar (decompress) it - untar( - &src_path, - &dest_path, - &TarOptions { - pr: None, - ..TarOptions::new(TarFormat::Gz) - }, - ) - .unwrap(); + // unarchive (decompress) it + unarchive(&src_path, &dest_path, TarFormat::Gz, &Default::default()).unwrap(); // Verify output assert!(dest_path.exists()); @@ -1948,7 +2050,7 @@ mod tests { } #[test] - fn test_untar_single_file_to_dir() { + fn test_unarchive_single_file_to_dir() { use flate2::Compression; use flate2::write::GzEncoder; use std::io::Write; @@ -1965,16 +2067,8 @@ mod tests { encoder.write_all(b"hello world").unwrap(); encoder.finish().unwrap(); - // untar (decompress) it - untar( - &src_path, - &dest_dir, - &TarOptions { - pr: None, - ..TarOptions::new(TarFormat::Gz) - }, - ) - .unwrap(); + // unarchive (decompress) it + unarchive(&src_path, &dest_dir, TarFormat::Gz, &Default::default()).unwrap(); // Verify output - should be out_dir/test_file let expected_path = dest_dir.join("test_file"); @@ -1984,6 +2078,62 @@ mod tests { assert_eq!(content, "hello world"); } + #[test] + fn test_unarchive_single_file_creates_parent_dir() { + use flate2::Compression; + use flate2::write::GzEncoder; + use std::io::Write; + use tempfile::tempdir; + + let dir = tempdir().unwrap(); + let src_path = dir.path().join("test.gz"); + let dest_path = dir.path().join("missing").join("test-out"); + + let file = File::create(&src_path).unwrap(); + let mut encoder = GzEncoder::new(file, Compression::default()); + encoder.write_all(b"hello world").unwrap(); + encoder.finish().unwrap(); + + unarchive( + &src_path, + dir.path(), + TarFormat::Gz, + &ArchiveOptions { + single_file_dest: Some(&dest_path), + ..Default::default() + }, + ) + .unwrap(); + + assert!(dest_path.exists()); + assert!(dest_path.is_file()); + let content = std::fs::read_to_string(&dest_path).unwrap(); + assert_eq!(content, "hello world"); + } + + #[test] + fn test_untar_rejects_single_file_compression() { + use tempfile::tempdir; + + let dir = tempdir().unwrap(); + let src_path = dir.path().join("test.gz"); + let dest_path = dir.path().join("test-out"); + let err = untar( + &src_path, + &dest_path, + &TarOptions { + pr: None, + ..TarOptions::new(TarFormat::Gz) + }, + ) + .unwrap_err(); + + assert!( + format!("{err:#}").contains("untar only supports tar formats"), + "{err:#}" + ); + } + #[tokio::test] async fn test_remove_file_async_if_exists_when_file_exists() { let dir = tempfile::tempdir().unwrap(); diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 91e319175c..0faec2d711 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -13,7 +13,7 @@ use crate::cli::args::BackendArg; use crate::cli::version::OS; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; -use crate::file::{TarFormat, TarOptions}; +use crate::file::{ArchiveOptions, TarFormat}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; use crate::lockfile::PlatformInfo; @@ -218,19 +218,19 @@ impl JavaPlugin { ) -> Result<()> { let filename = tarball_path.file_name().unwrap().to_string_lossy(); pr.set_message(format!("extract {filename}")); - match m.file_type.as_deref() { - Some("zip") => file::unzip(tarball_path, &tv.download_path(), &Default::default())?, - _ => file::untar( - tarball_path, - &tv.download_path(), - &TarOptions { - pr: Some(pr), - ..TarOptions::new(TarFormat::from_file_name( - &tarball_path.file_name().unwrap().to_string_lossy(), - )) - }, - )?, - } + let format = match m.file_type.as_deref() { + Some("zip") => TarFormat::Zip, + _ => TarFormat::from_file_name(&tarball_path.file_name().unwrap().to_string_lossy()), + }; + file::unarchive( + tarball_path, + &tv.download_path(), + format, + &ArchiveOptions { + pr: Some(pr), + ..Default::default() + }, + )?; self.move_to_install_path(tv, m) } diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index 5ecaf76247..973c2df0c5 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -7,7 +7,7 @@ use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; -use crate::file::{TarFormat, TarOptions, display_path}; +use crate::file::{ArchiveOptions, TarFormat, display_path}; use crate::git::{CloneOptions, Git}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; @@ -353,13 +353,14 @@ impl PythonPlugin { } file::remove_all(&install)?; - file::untar( + file::unarchive( &tarball_path, &install, - &TarOptions { + TarFormat::from_file_name(filename), + &ArchiveOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(TarFormat::from_file_name(filename)) + ..Default::default() }, )?; if !install.join("bin").exists() { diff --git a/src/plugins/core/zig.rs b/src/plugins/core/zig.rs index e818e0d38f..4b1cae79e0 100644 --- a/src/plugins/core/zig.rs +++ b/src/plugins/core/zig.rs @@ -11,7 +11,7 @@ use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; use crate::duration::DAILY; -use crate::file::{TarFormat, TarOptions}; +use crate::file::{ArchiveOptions, TarFormat}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; use crate::lockfile::{PlatformInfo, ProvenanceType}; @@ -138,13 +138,14 @@ impl ZigPlugin { let filename = tarball_path.file_name().unwrap().to_string_lossy(); ctx.pr.set_message(format!("extract {filename}")); file::remove_all(tv.install_path())?; - file::untar( + file::unarchive( tarball_path, &tv.install_path(), - &TarOptions { + TarFormat::from_file_name(&filename), + &ArchiveOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(TarFormat::from_file_name(&filename)) + ..Default::default() }, )?; From dafdf39fe093fb2c57166c58d218a32a585a9a03 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sat, 6 Jun 2026 05:40:04 +1000 Subject: [PATCH 03/19] test(file): cover unarchive zip dispatch --- src/file.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/file.rs b/src/file.rs index 72d44218d7..69d1b874d3 100644 --- a/src/file.rs +++ b/src/file.rs @@ -2111,6 +2111,31 @@ mod tests { assert_eq!(content, "hello world"); } + #[test] + fn test_unarchive_zip() { + use std::io::Write; + use tempfile::tempdir; + + let dir = tempdir().unwrap(); + let src_path = dir.path().join("test.zip"); + let dest_dir = dir.path().join("out_dir"); + + let file = File::create(&src_path).unwrap(); + let mut zip = zip::ZipWriter::new(file); + zip.start_file("pkg/tool", zip::write::SimpleFileOptions::default()) + .unwrap(); + zip.write_all(b"hello world").unwrap(); + zip.finish().unwrap(); + + unarchive(&src_path, &dest_dir, TarFormat::Zip, &Default::default()).unwrap(); + + let extracted_path = dest_dir.join("pkg").join("tool"); + assert!(extracted_path.exists()); + assert!(extracted_path.is_file()); + let content = std::fs::read_to_string(&extracted_path).unwrap(); + assert_eq!(content, "hello world"); + } + #[test] fn test_untar_rejects_single_file_compression() { use tempfile::tempdir; From e0c33c4a2d18be46b684bac90852540cccbfca07 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:14:10 +1000 Subject: [PATCH 04/19] refactor(file): add tbz alias to TarFormat --- src/file.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/file.rs b/src/file.rs index 69d1b874d3..806c5df4b3 100644 --- a/src/file.rs +++ b/src/file.rs @@ -907,7 +907,7 @@ pub enum TarFormat { TarXz, #[strum(serialize = "xz")] Xz, - #[strum(serialize = "tar.bz2", serialize = "tbz2")] + #[strum(serialize = "tar.bz2", serialize = "tbz2", serialize = "tbz")] TarBz2, #[strum(serialize = "bz2")] Bz2, @@ -2008,6 +2008,8 @@ mod tests { assert_eq!(TarFormat::from_file_name("foo.txz"), TarFormat::TarXz); assert_eq!(TarFormat::from_file_name("foo.tar.bz2"), TarFormat::TarBz2); assert_eq!(TarFormat::from_file_name("foo.tbz2"), TarFormat::TarBz2); + assert_eq!(TarFormat::from_file_name("foo.tbz"), TarFormat::TarBz2); + assert_eq!(TarFormat::from_ext("tbz"), TarFormat::TarBz2); assert_eq!(TarFormat::from_file_name("foo.tar.zst"), TarFormat::TarZst); assert_eq!(TarFormat::from_file_name("foo.tzst"), TarFormat::TarZst); assert_eq!(TarFormat::from_file_name("foo.tar"), TarFormat::Tar); From b99f3273b8459585d0300c4148d2d3ad715d16b9 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:22:01 +1000 Subject: [PATCH 05/19] refactor(file): rename un_compressed_file to decompress_file --- src/backend/http.rs | 2 +- src/backend/static_helpers.rs | 2 +- src/file.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index 1c0295387e..14c5477119 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -393,7 +393,7 @@ impl HttpBackend { pr.set_message(format!("extract {}", file_info.file_name())); } - file::un_compressed_file(file_path, &dest_file, file_info.format)?; + file::decompress_file(file_path, &dest_file, file_info.format)?; file::make_executable(&dest_file)?; Ok(ExtractionType::RawFile { filename }) diff --git a/src/backend/static_helpers.rs b/src/backend/static_helpers.rs index 88c57980df..1028929236 100644 --- a/src/backend/static_helpers.rs +++ b/src/backend/static_helpers.rs @@ -435,7 +435,7 @@ pub fn install_artifact( install_path.join(cleaned_name) }; - file::un_compressed_file(file_path, &dest, format)?; + file::decompress_file(file_path, &dest, format)?; file::make_executable(&dest)?; } else if format == file::TarFormat::Raw { diff --git a/src/file.rs b/src/file.rs index 806c5df4b3..5c7b8d8f23 100644 --- a/src/file.rs +++ b/src/file.rs @@ -881,7 +881,7 @@ pub fn un_bz2(input: &Path, dest: &Path) -> Result<()> { Ok(()) } -pub fn un_compressed_file(input: &Path, dest: &Path, format: TarFormat) -> Result<()> { +pub fn decompress_file(input: &Path, dest: &Path, format: TarFormat) -> Result<()> { if let Some(parent) = dest.parent() && !parent.as_os_str().is_empty() { @@ -1101,7 +1101,7 @@ pub fn unarchive( .single_file_dest .map(Path::to_path_buf) .unwrap_or_else(|| compressed_file_dest(archive, dest)); - un_compressed_file(archive, &out_path, format) + decompress_file(archive, &out_path, format) } TarFormat::Raw => unreachable!("raw is handled as a legacy tar format"), TarFormat::TarGz From 63acd6c4f20f10866a4d4c52c5a97f4d38097abb Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:25:58 +1000 Subject: [PATCH 06/19] refactor(aqua-registry): stop canonicalizing format aliases TarFormat::from_ext handles tgz/tbz/txz aliases at extraction time. --- crates/aqua-registry/src/types.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/crates/aqua-registry/src/types.rs b/crates/aqua-registry/src/types.rs index c5765cefb3..6b7fd089ef 100644 --- a/crates/aqua-registry/src/types.rs +++ b/crates/aqua-registry/src/types.rs @@ -470,12 +470,7 @@ impl AquaPackage { for format in formats { if asset_name.ends_with(&format!(".{format}")) { - return match format { - "tgz" => "tar.gz", - "txz" => "tar.xz", - "tbz2" | "tbz" => "tar.bz2", - _ => format, - }; + return format; } } "raw" @@ -570,12 +565,7 @@ impl AquaPackage { }; self.detect_format(&asset) } else { - match self.format.as_str() { - "tgz" => "tar.gz", - "txz" => "tar.xz", - "tbz2" | "tbz" => "tar.bz2", - format => format, - } + self.format.as_str() }; Ok(format) } From 897bb3268146533f27706efab04b26202513c0f6 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:30:38 +1000 Subject: [PATCH 07/19] refactor(java): detect archive format from filename only --- src/plugins/core/java.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 0faec2d711..d5b2632805 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -218,10 +218,7 @@ impl JavaPlugin { ) -> Result<()> { let filename = tarball_path.file_name().unwrap().to_string_lossy(); pr.set_message(format!("extract {filename}")); - let format = match m.file_type.as_deref() { - Some("zip") => TarFormat::Zip, - _ => TarFormat::from_file_name(&tarball_path.file_name().unwrap().to_string_lossy()), - }; + let format = TarFormat::from_file_name(&filename); file::unarchive( tarball_path, &tv.download_path(), From 740cddef4ea6085163a9175a87c2aec970d511e5 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:33:47 +1000 Subject: [PATCH 08/19] refactor(file): remove unarchive_with_format wrapper Call TarFormat::from_ext and unarchive directly from the aqua backend. --- src/backend/aqua.rs | 11 +++++++++-- src/file.rs | 13 ------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index c8882f9b45..b5192d52d5 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -2287,9 +2287,13 @@ impl AquaBackend { single_file_dest: Some(first_bin_path), ..Default::default() }; + let archive_format = TarFormat::from_ext(format); let mut make_executable = false; if let AquaPackageType::GithubArchive = pkg.r#type { - file::unarchive_with_format(&tarball_path, &install_path, format, &archive_opts)?; + if archive_format == TarFormat::Raw && !format.eq_ignore_ascii_case("raw") { + bail!("unsupported format: {}", format); + } + file::unarchive(&tarball_path, &install_path, archive_format, &archive_opts)?; } else if let AquaPackageType::GithubContent = pkg.r#type { file::create_dir_all(&install_path)?; file::copy(&tarball_path, first_bin_path)?; @@ -2303,7 +2307,10 @@ impl AquaBackend { } else if format == "pkg" { file::un_pkg(&tarball_path, &install_path)?; } else { - file::unarchive_with_format(&tarball_path, &install_path, format, &archive_opts)?; + if archive_format == TarFormat::Raw && !format.eq_ignore_ascii_case("raw") { + bail!("unsupported format: {}", format); + } + file::unarchive(&tarball_path, &install_path, archive_format, &archive_opts)?; make_executable = true; } diff --git a/src/file.rs b/src/file.rs index 5c7b8d8f23..4bd9ad75f6 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1052,19 +1052,6 @@ impl<'a> TarOptions<'a> { } } -pub fn unarchive_with_format( - archive: &Path, - dest: &Path, - format: &str, - opts: &ArchiveOptions, -) -> Result<()> { - let archive_format = TarFormat::from_ext(format); - if archive_format == TarFormat::Raw && !format.eq_ignore_ascii_case("raw") { - bail!("unsupported format: {}", format); - } - unarchive(archive, dest, archive_format, opts) -} - pub fn unarchive( archive: &Path, dest: &Path, From 4f47e4bc5264ebfa7184153e28c9d63924deb796 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:36:48 +1000 Subject: [PATCH 09/19] refactor: inline archive opts and trim aqua format check --- src/backend/aqua.rs | 3 --- src/cli/generate/tool_stub.rs | 14 +++++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index b5192d52d5..7328f8d886 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -2290,9 +2290,6 @@ impl AquaBackend { let archive_format = TarFormat::from_ext(format); let mut make_executable = false; if let AquaPackageType::GithubArchive = pkg.r#type { - if archive_format == TarFormat::Raw && !format.eq_ignore_ascii_case("raw") { - bail!("unsupported format: {}", format); - } file::unarchive(&tarball_path, &install_path, archive_format, &archive_opts)?; } else if let AquaPackageType::GithubContent = pkg.r#type { file::create_dir_all(&install_path)?; diff --git a/src/cli/generate/tool_stub.rs b/src/cli/generate/tool_stub.rs index d8577df85e..7bcd0abf6f 100644 --- a/src/cli/generate/tool_stub.rs +++ b/src/cli/generate/tool_stub.rs @@ -514,11 +514,15 @@ exec "$MISE_BIN" tool-stub "$0" "$@" .unwrap_or_default() .to_string_lossy(), ); - let archive_opts = file::ArchiveOptions { - pr: Some(pr), - ..Default::default() - }; - file::unarchive(archive_path, &extracted_dir, format, &archive_opts)?; + file::unarchive( + archive_path, + &extracted_dir, + format, + &file::ArchiveOptions { + pr: Some(pr), + ..Default::default() + }, + )?; // Check if strip_components would be applied during actual installation let format = From 4c5e386a504ad0039d3296ecc88612b35cf63bad Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:40:31 +1000 Subject: [PATCH 10/19] refactor(file): rename TarFormat and split extract from decompress - rename TarFormat to ArchiveFormat; from_ext returns Option - replace unarchive/ArchiveOptions with extract_archive/ExtractOptions - route aqua compressed assets through decompress_file directly --- src/backend/aqua.rs | 24 ++- src/backend/asset_matcher.rs | 6 +- src/backend/github.rs | 4 +- src/backend/http.rs | 14 +- src/backend/static_helpers.rs | 14 +- src/cli/generate/tool_stub.rs | 12 +- src/file.rs | 342 ++++++++++++++++------------------ src/plugins/asdf_plugin.rs | 3 +- src/plugins/core/erlang.rs | 4 +- src/plugins/core/go.rs | 4 +- src/plugins/core/java.rs | 8 +- src/plugins/core/node.rs | 6 +- src/plugins/core/python.rs | 8 +- src/plugins/core/ruby.rs | 4 +- src/plugins/core/swift.rs | 2 +- src/plugins/core/zig.rs | 8 +- src/plugins/vfox_plugin.rs | 3 +- 17 files changed, 228 insertions(+), 238 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index 7328f8d886..f9194554d0 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -7,7 +7,7 @@ use crate::backend::static_helpers::get_filename_from_url; use crate::cli::args::BackendArg; use crate::cli::version::{ARCH, OS}; use crate::config::Settings; -use crate::file::{ArchiveOptions, TarFormat}; +use crate::file::{ArchiveFormat, ExtractOptions}; use crate::http::HTTP; use crate::install_context::InstallContext; use crate::lockfile::{PlatformInfo, ProvenanceType}; @@ -1272,7 +1272,8 @@ impl AquaBackend { v: &str, ) -> Result { let format = pkg.format(v, os(), arch())?; - let format = TarFormat::from_ext(format); + let format = ArchiveFormat::from_ext(format) + .ok_or_else(|| eyre!("unsupported archive format for SLSA content check"))?; if !format.is_archive() { return Err(eyre!( "SLSA provenance subject mismatch and content-level fallback is only supported for archives" @@ -2282,15 +2283,15 @@ impl AquaBackend { let first_bin_path = bin_paths .first() .expect("at least one bin path should exist"); - let archive_opts = ArchiveOptions { + let extract_opts = ExtractOptions { pr: Some(ctx.pr.as_ref()), - single_file_dest: Some(first_bin_path), ..Default::default() }; - let archive_format = TarFormat::from_ext(format); + let archive_format = ArchiveFormat::from_ext(format); let mut make_executable = false; if let AquaPackageType::GithubArchive = pkg.r#type { - file::unarchive(&tarball_path, &install_path, archive_format, &archive_opts)?; + let archive_format = archive_format.unwrap_or(ArchiveFormat::TarGz); + file::extract_archive(&tarball_path, &install_path, archive_format, &extract_opts)?; } else if let AquaPackageType::GithubContent = pkg.r#type { file::create_dir_all(&install_path)?; file::copy(&tarball_path, first_bin_path)?; @@ -2304,11 +2305,16 @@ impl AquaBackend { } else if format == "pkg" { file::un_pkg(&tarball_path, &install_path)?; } else { - if archive_format == TarFormat::Raw && !format.eq_ignore_ascii_case("raw") { + let Some(archive_format) = archive_format else { bail!("unsupported format: {}", format); + }; + if archive_format.is_compressed_file() { + file::decompress_file(&tarball_path, first_bin_path, archive_format)?; + make_executable = true; + } else { + file::extract_archive(&tarball_path, &install_path, archive_format, &extract_opts)?; + make_executable = true; } - file::unarchive(&tarball_path, &install_path, archive_format, &archive_opts)?; - make_executable = true; } if make_executable { diff --git a/src/backend/asset_matcher.rs b/src/backend/asset_matcher.rs index f87b4b09f6..081fd8932d 100644 --- a/src/backend/asset_matcher.rs +++ b/src/backend/asset_matcher.rs @@ -22,7 +22,7 @@ use std::sync::LazyLock; use super::platform_target::PlatformTarget; use super::platform_tokens::is_platform_or_version_token; use super::static_helpers::get_filename_from_url; -use crate::file::TarFormat; +use crate::file::ArchiveFormat; use crate::http::HTTP; // ========== Platform Detection Types (from asset_detector) ========== @@ -352,9 +352,9 @@ impl AssetPicker { } fn score_format_preferences(&self, asset: &str) -> i32 { - let format = TarFormat::from_file_name(asset); + let format = ArchiveFormat::from_file_name(asset); - if format == TarFormat::Zip { + if format == ArchiveFormat::Zip { if self.target_os == "windows" { return 15; } else { diff --git a/src/backend/github.rs b/src/backend/github.rs index 01331730e8..76524016d5 100644 --- a/src/backend/github.rs +++ b/src/backend/github.rs @@ -1880,9 +1880,9 @@ impl UnifiedGitBackend { ) -> Result { let raw_opts = tv.request.options(); let format = if let Some(format_opt) = lookup_with_fallback(&raw_opts, "format") { - file::TarFormat::from_ext(&format_opt) + file::ArchiveFormat::from_ext(&format_opt).unwrap_or(file::ArchiveFormat::Raw) } else { - file::TarFormat::from_file_name( + file::ArchiveFormat::from_file_name( &file_path.file_name().unwrap_or_default().to_string_lossy(), ) }; diff --git a/src/backend/http.rs b/src/backend/http.rs index 14c5477119..f4e2c83f1e 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -57,7 +57,7 @@ struct FileInfo { /// File extension extension: String, /// Detected archive format - format: file::TarFormat, + format: file::ArchiveFormat, /// Whether this is a compressed single binary (not a tar archive) is_compressed_binary: bool, } @@ -81,7 +81,7 @@ impl FileInfo { }; let file_name = effective_path.file_name().unwrap().to_string_lossy(); - let format = file::TarFormat::from_file_name(&file_name); + let format = file::ArchiveFormat::from_file_name(&file_name); let extension = format .extension() @@ -94,7 +94,7 @@ impl FileInfo { .to_string() }); - let is_compressed_binary = !format.is_archive() && format != file::TarFormat::Raw; + let is_compressed_binary = !format.is_archive() && format != file::ArchiveFormat::Raw; Self { effective_path, @@ -286,7 +286,7 @@ impl HttpBackend { /// used different options (e.g., different `bin` name) fn extraction_type_from_cache(&self, cache_key: &str, file_info: &FileInfo) -> ExtractionType { // For archives, we don't need to detect the filename - if !file_info.is_compressed_binary && file_info.format != file::TarFormat::Raw { + if !file_info.is_compressed_binary && file_info.format != file::ArchiveFormat::Raw { return ExtractionType::Archive; } @@ -369,7 +369,7 @@ impl HttpBackend { if file_info.is_compressed_binary { self.extract_compressed_binary(dest, file_path, &file_info, opts, pr) - } else if file_info.format == file::TarFormat::Raw { + } else if file_info.format == file::ArchiveFormat::Raw { self.extract_raw_file(dest, file_path, &file_info, opts, pr) } else { self.extract_archive(tv, dest, file_path, &file_info, opts, pr) @@ -444,14 +444,14 @@ impl HttpBackend { strip_components = Some(1); } - let archive_opts = file::ArchiveOptions { + let extract_opts = file::ExtractOptions { strip_components: strip_components.unwrap_or(0), pr, preserve_mtime: false, ..Default::default() }; - file::unarchive(file_path, dest, file_info.format, &archive_opts)?; + file::extract_archive(file_path, dest, file_info.format, &extract_opts)?; // Handle rename_exe option for archives if let Some(rename_to) = opts.rename_exe() { diff --git a/src/backend/static_helpers.rs b/src/backend/static_helpers.rs index 1028929236..9457d2d356 100644 --- a/src/backend/static_helpers.rs +++ b/src/backend/static_helpers.rs @@ -399,12 +399,12 @@ pub fn install_artifact( file::remove_all(&install_path)?; file::create_dir_all(&install_path)?; - // Use TarFormat for format detection + // Use ArchiveFormat for format detection // Check for explicit format option first, then fall back to file extension let format = if let Some(format_opt) = lookup_with_fallback(opts, "format") { - file::TarFormat::from_ext(&format_opt) + file::ArchiveFormat::from_ext(&format_opt).unwrap_or(file::ArchiveFormat::Raw) } else { - file::TarFormat::from_file_name( + file::ArchiveFormat::from_file_name( &file_path.file_name().unwrap_or_default().to_string_lossy(), ) }; @@ -412,7 +412,7 @@ pub fn install_artifact( // Get file extension and detect format let file_name = file_path.file_name().unwrap().to_string_lossy(); - if !format.is_archive() && format != file::TarFormat::Raw { + if !format.is_archive() && format != file::ArchiveFormat::Raw { // Handle compressed single binary let ext = Path::new(&*file_name) .extension() @@ -438,7 +438,7 @@ pub fn install_artifact( file::decompress_file(file_path, &dest, format)?; file::make_executable(&dest)?; - } else if format == file::TarFormat::Raw { + } else if format == file::ArchiveFormat::Raw { // Copy the file directly to the bin_path directory or install_path if let Some(bin_path_template) = lookup_with_fallback(opts, "bin_path") { let bin_path = template_string(&bin_path_template, tv); @@ -472,14 +472,14 @@ pub fn install_artifact( debug!("Auto-detected single directory archive, extracting with strip_components=1"); strip_components = Some(1); } - let archive_opts = file::ArchiveOptions { + let extract_opts = file::ExtractOptions { strip_components: strip_components.unwrap_or(0), pr, ..Default::default() }; // Extract with determined strip_components - file::unarchive(file_path, &install_path, format, &archive_opts)?; + file::extract_archive(file_path, &install_path, format, &extract_opts)?; // Extract just the repo name from tool_name (e.g., "opsgenie/opsgenie-lamp" -> "opsgenie-lamp") let full_tool_name = tv.ba().tool_name.as_str(); diff --git a/src/cli/generate/tool_stub.rs b/src/cli/generate/tool_stub.rs index 7bcd0abf6f..6ccc31c874 100644 --- a/src/cli/generate/tool_stub.rs +++ b/src/cli/generate/tool_stub.rs @@ -4,7 +4,7 @@ use crate::backend::platform_target::PlatformTarget; use crate::backend::static_helpers::get_filename_from_url; use crate::cli::tool_stub::ToolStubFile; use crate::config::Config; -use crate::file::{self, TarFormat}; +use crate::file::{self, ArchiveFormat}; use crate::http::HTTP; use crate::lockfile::PlatformInfo; use crate::minisign; @@ -471,7 +471,7 @@ exec "$MISE_BIN" tool-stub "$0" "$@" let checksum = format!("blake3:{}", blake3::hash(&bytes).to_hex()); // Detect binary path if this is an archive - let bin_path = if TarFormat::from_file_name(&filename).is_archive() { + let bin_path = if ArchiveFormat::from_file_name(&filename).is_archive() { // Update progress message for extraction and reuse the same progress reporter pr.set_message(format!("extract {filename}")); match self @@ -508,17 +508,17 @@ exec "$MISE_BIN" tool-stub "$0" "$@" std::fs::create_dir_all(&extracted_dir)?; // Try extraction using mise's built-in extraction logic (reuse the passed progress reporter) - let format = TarFormat::from_file_name( + let format = ArchiveFormat::from_file_name( &archive_path .file_name() .unwrap_or_default() .to_string_lossy(), ); - file::unarchive( + file::extract_archive( archive_path, &extracted_dir, format, - &file::ArchiveOptions { + &file::ExtractOptions { pr: Some(pr), ..Default::default() }, @@ -526,7 +526,7 @@ exec "$MISE_BIN" tool-stub "$0" "$@" // Check if strip_components would be applied during actual installation let format = - TarFormat::from_file_name(&archive_path.file_name().unwrap().to_string_lossy()); + ArchiveFormat::from_file_name(&archive_path.file_name().unwrap().to_string_lossy()); let will_strip = file::should_strip_components(archive_path, format)?; // Find executable files diff --git a/src/file.rs b/src/file.rs index 4bd9ad75f6..675d30f5b6 100644 --- a/src/file.rs +++ b/src/file.rs @@ -881,7 +881,7 @@ pub fn un_bz2(input: &Path, dest: &Path) -> Result<()> { Ok(()) } -pub fn decompress_file(input: &Path, dest: &Path, format: TarFormat) -> Result<()> { +pub fn decompress_file(input: &Path, dest: &Path, format: ArchiveFormat) -> Result<()> { if let Some(parent) = dest.parent() && !parent.as_os_str().is_empty() { @@ -889,16 +889,16 @@ pub fn decompress_file(input: &Path, dest: &Path, format: TarFormat) -> Result<( } match format { - TarFormat::Gz => un_gz(input, dest), - TarFormat::Xz => un_xz(input, dest), - TarFormat::Zst => un_zst(input, dest), - TarFormat::Bz2 => un_bz2(input, dest), + ArchiveFormat::Gz => un_gz(input, dest), + ArchiveFormat::Xz => un_xz(input, dest), + ArchiveFormat::Zst => un_zst(input, dest), + ArchiveFormat::Bz2 => un_bz2(input, dest), _ => bail!("unsupported compressed file format: {}", format), } } #[derive(Debug, Clone, Copy, PartialEq, strum::EnumString, strum::Display)] -pub enum TarFormat { +pub enum ArchiveFormat { #[strum(serialize = "tar.gz", serialize = "tgz")] TarGz, #[strum(serialize = "gz")] @@ -925,105 +925,104 @@ pub enum TarFormat { Raw, } -impl TarFormat { +impl ArchiveFormat { pub fn from_file_name(filename: &str) -> Self { let filename = filename.to_lowercase(); if let Some(idx) = filename.rfind(".tar.") { let ext = &filename[idx + 1..]; - let fmt = Self::from_ext(ext); - if fmt != TarFormat::Raw { + if let Some(fmt) = Self::from_ext(ext) { return fmt; } } if let Some(ext) = Path::new(&filename).extension().and_then(|s| s.to_str()) { - Self::from_ext(ext) + Self::from_ext(ext).unwrap_or(ArchiveFormat::Raw) } else { - TarFormat::Raw + ArchiveFormat::Raw } } - pub fn from_ext(ext: &str) -> Self { - ext.to_lowercase().parse().unwrap_or(TarFormat::Raw) + pub fn from_ext(ext: &str) -> Option { + ext.to_lowercase().parse().ok() } pub fn is_archive(&self) -> bool { match self { - TarFormat::TarGz - | TarFormat::TarXz - | TarFormat::TarBz2 - | TarFormat::TarZst - | TarFormat::Tar - | TarFormat::Zip - | TarFormat::SevenZip => true, - TarFormat::Gz | TarFormat::Xz | TarFormat::Bz2 | TarFormat::Zst | TarFormat::Raw => { - false - } + ArchiveFormat::TarGz + | ArchiveFormat::TarXz + | ArchiveFormat::TarBz2 + | ArchiveFormat::TarZst + | ArchiveFormat::Tar + | ArchiveFormat::Zip + | ArchiveFormat::SevenZip => true, + ArchiveFormat::Gz + | ArchiveFormat::Xz + | ArchiveFormat::Bz2 + | ArchiveFormat::Zst + | ArchiveFormat::Raw => false, } } pub fn is_tar_archive(&self) -> bool { matches!( self, - TarFormat::TarGz - | TarFormat::TarXz - | TarFormat::TarBz2 - | TarFormat::TarZst - | TarFormat::Tar + ArchiveFormat::TarGz + | ArchiveFormat::TarXz + | ArchiveFormat::TarBz2 + | ArchiveFormat::TarZst + | ArchiveFormat::Tar ) } pub fn is_compressed_file(&self) -> bool { matches!( self, - TarFormat::Gz | TarFormat::Xz | TarFormat::Bz2 | TarFormat::Zst + ArchiveFormat::Gz | ArchiveFormat::Xz | ArchiveFormat::Bz2 | ArchiveFormat::Zst ) } fn can_open_with_tar(&self) -> bool { - self.is_tar_archive() || *self == TarFormat::Raw + self.is_tar_archive() || *self == ArchiveFormat::Raw } pub fn extension(&self) -> Option<&'static str> { match self { - TarFormat::TarGz => Some("tar.gz"), - TarFormat::Gz => Some("gz"), - TarFormat::TarXz => Some("tar.xz"), - TarFormat::Xz => Some("xz"), - TarFormat::TarBz2 => Some("tar.bz2"), - TarFormat::Bz2 => Some("bz2"), - TarFormat::TarZst => Some("tar.zst"), - TarFormat::Zst => Some("zst"), - TarFormat::Tar => Some("tar"), - TarFormat::Zip => Some("zip"), - TarFormat::SevenZip => Some("7z"), - TarFormat::Raw => None, + ArchiveFormat::TarGz => Some("tar.gz"), + ArchiveFormat::Gz => Some("gz"), + ArchiveFormat::TarXz => Some("tar.xz"), + ArchiveFormat::Xz => Some("xz"), + ArchiveFormat::TarBz2 => Some("tar.bz2"), + ArchiveFormat::Bz2 => Some("bz2"), + ArchiveFormat::TarZst => Some("tar.zst"), + ArchiveFormat::Zst => Some("zst"), + ArchiveFormat::Tar => Some("tar"), + ArchiveFormat::Zip => Some("zip"), + ArchiveFormat::SevenZip => Some("7z"), + ArchiveFormat::Raw => None, } } } -pub struct ArchiveOptions<'a> { +pub struct ExtractOptions<'a> { pub strip_components: usize, pub pr: Option<&'a dyn SingleReport>, /// When false, files will be extracted with current timestamp instead of archive's mtime pub preserve_mtime: bool, - pub single_file_dest: Option<&'a Path>, } -impl<'a> Default for ArchiveOptions<'a> { +impl<'a> Default for ExtractOptions<'a> { fn default() -> Self { Self { strip_components: 0, pr: None, preserve_mtime: true, - single_file_dest: None, } } } -impl<'a> ArchiveOptions<'a> { - fn tar_options(&self, format: TarFormat) -> TarOptions<'a> { +impl<'a> ExtractOptions<'a> { + fn tar_options(&self, format: ArchiveFormat) -> TarOptions<'a> { TarOptions { format, strip_components: self.strip_components, @@ -1034,7 +1033,7 @@ impl<'a> ArchiveOptions<'a> { } pub struct TarOptions<'a> { - pub format: TarFormat, + pub format: ArchiveFormat, pub strip_components: usize, pub pr: Option<&'a dyn SingleReport>, /// When false, files will be extracted with current timestamp instead of archive's mtime @@ -1042,7 +1041,7 @@ pub struct TarOptions<'a> { } impl<'a> TarOptions<'a> { - pub fn new(format: TarFormat) -> Self { + pub fn new(format: ArchiveFormat) -> Self { Self { format, strip_components: 0, @@ -1052,22 +1051,27 @@ impl<'a> TarOptions<'a> { } } -pub fn unarchive( +pub fn extract_archive( archive: &Path, dest: &Path, - format: TarFormat, - opts: &ArchiveOptions, + format: ArchiveFormat, + opts: &ExtractOptions, ) -> Result<()> { match format { - format if format.can_open_with_tar() => untar(archive, dest, &opts.tar_options(format)), - TarFormat::Zip => unzip( + ArchiveFormat::TarGz + | ArchiveFormat::TarXz + | ArchiveFormat::TarBz2 + | ArchiveFormat::TarZst + | ArchiveFormat::Tar + | ArchiveFormat::Raw => untar(archive, dest, &opts.tar_options(format)), + ArchiveFormat::Zip => unzip( archive, dest, &ZipOptions { strip_components: opts.strip_components, }, ), - TarFormat::SevenZip => { + ArchiveFormat::SevenZip => { #[cfg(windows)] { return un7z( @@ -1083,34 +1087,9 @@ pub fn unarchive( bail!("7z format not supported on this platform"); } } - format if format.is_compressed_file() => { - let out_path = opts - .single_file_dest - .map(Path::to_path_buf) - .unwrap_or_else(|| compressed_file_dest(archive, dest)); - decompress_file(archive, &out_path, format) + ArchiveFormat::Gz | ArchiveFormat::Xz | ArchiveFormat::Bz2 | ArchiveFormat::Zst => { + bail!("extract_archive does not support compressed single-file format: {format}") } - TarFormat::Raw => unreachable!("raw is handled as a legacy tar format"), - TarFormat::TarGz - | TarFormat::TarXz - | TarFormat::TarBz2 - | TarFormat::TarZst - | TarFormat::Tar - | TarFormat::Gz - | TarFormat::Xz - | TarFormat::Bz2 - | TarFormat::Zst => unreachable!("handled by guarded match arms above"), - } -} - -fn compressed_file_dest(archive: &Path, dest: &Path) -> PathBuf { - if dest.is_dir() { - let name = archive - .file_stem() - .unwrap_or_else(|| archive.file_name().unwrap()); - dest.join(name) - } else { - dest.to_path_buf() } } @@ -1215,20 +1194,20 @@ pub fn untar(archive: &Path, dest: &Path, opts: &TarOptions) -> Result<()> { Ok(()) } -fn open_tar(format: TarFormat, archive: &Path) -> Result> { +fn open_tar(format: ArchiveFormat, archive: &Path) -> Result> { let f = File::open(archive)?; Ok(match format { // TODO: we probably shouldn't assume raw is tar.gz, but this was to retain existing behavior - TarFormat::TarGz | TarFormat::Raw => Box::new(GzDecoder::new(f)), - TarFormat::TarXz => Box::new(xz2::read::XzDecoder::new(f)), - TarFormat::TarBz2 => Box::new(BzDecoder::new(f)), - TarFormat::TarZst => Box::new(zstd::stream::read::Decoder::new(f)?), - TarFormat::Tar => Box::new(f), - TarFormat::Gz | TarFormat::Xz | TarFormat::Bz2 | TarFormat::Zst => { + ArchiveFormat::TarGz | ArchiveFormat::Raw => Box::new(GzDecoder::new(f)), + ArchiveFormat::TarXz => Box::new(xz2::read::XzDecoder::new(f)), + ArchiveFormat::TarBz2 => Box::new(BzDecoder::new(f)), + ArchiveFormat::TarZst => Box::new(zstd::stream::read::Decoder::new(f)?), + ArchiveFormat::Tar => Box::new(f), + ArchiveFormat::Gz | ArchiveFormat::Xz | ArchiveFormat::Bz2 | ArchiveFormat::Zst => { bail!("{} is not a tar archive", format) } - TarFormat::Zip => bail!("zip format not supported"), - TarFormat::SevenZip => bail!("7z format not supported"), + ArchiveFormat::Zip => bail!("zip format not supported"), + ArchiveFormat::SevenZip => bail!("7z format not supported"), }) } @@ -1382,7 +1361,7 @@ fn skip_curdir_components(path: &Path) -> impl Iterator Result> { +pub fn inspect_tar_contents(archive: &Path, format: ArchiveFormat) -> Result> { let tar = open_tar(format, archive)?; let mut archive = Archive::new(tar); let mut top_level_components = std::collections::HashMap::new(); @@ -1472,10 +1451,10 @@ pub fn inspect_7z_contents(_archive: &Path) -> Result> { } /// Determines if strip_components=1 should be applied based on archive structure -pub fn should_strip_components(archive: &Path, format: TarFormat) -> Result { +pub fn should_strip_components(archive: &Path, format: ArchiveFormat) -> Result { let top_level_entries = match format { - TarFormat::Zip => inspect_zip_contents(archive)?, - TarFormat::SevenZip => inspect_7z_contents(archive)?, + ArchiveFormat::Zip => inspect_zip_contents(archive)?, + ArchiveFormat::SevenZip => inspect_7z_contents(archive)?, _ => inspect_tar_contents(archive, format)?, }; @@ -1502,7 +1481,7 @@ pub struct ArchiveContent { /// fail closed instead of being ignored. pub fn archive_content_files( archive_path: &Path, - format: TarFormat, + format: ArchiveFormat, strip_components: usize, ) -> Result> { if strip_components > 1 { @@ -1510,16 +1489,20 @@ pub fn archive_content_files( } match format { - TarFormat::TarGz - | TarFormat::TarXz - | TarFormat::TarBz2 - | TarFormat::TarZst - | TarFormat::Tar => archive_content_files_tar(archive_path, format, strip_components), - TarFormat::Zip => archive_content_files_zip(archive_path, strip_components), - TarFormat::SevenZip => { + ArchiveFormat::TarGz + | ArchiveFormat::TarXz + | ArchiveFormat::TarBz2 + | ArchiveFormat::TarZst + | ArchiveFormat::Tar => archive_content_files_tar(archive_path, format, strip_components), + ArchiveFormat::Zip => archive_content_files_zip(archive_path, strip_components), + ArchiveFormat::SevenZip => { bail!("content-level SLSA verification does not support 7z archives") } - TarFormat::Gz | TarFormat::Xz | TarFormat::Bz2 | TarFormat::Zst | TarFormat::Raw => { + ArchiveFormat::Gz + | ArchiveFormat::Xz + | ArchiveFormat::Bz2 + | ArchiveFormat::Zst + | ArchiveFormat::Raw => { bail!("content-level SLSA verification only supports archive formats") } } @@ -1527,7 +1510,7 @@ pub fn archive_content_files( fn archive_content_files_tar( archive_path: &Path, - format: TarFormat, + format: ArchiveFormat, strip_components: usize, ) -> Result> { let tar = open_tar(format, archive_path)?; @@ -1674,7 +1657,7 @@ mod tests { builder.finish().unwrap(); } - let files = archive_content_files(&archive_path, TarFormat::Tar, 1).unwrap(); + let files = archive_content_files(&archive_path, ArchiveFormat::Tar, 1).unwrap(); assert_eq!(files.len(), 1); assert_eq!(files[0].name, "tool"); assert_eq!(files[0].sha256, hex::encode(Sha256::digest(b"tool"))); @@ -1698,7 +1681,7 @@ mod tests { builder.finish().unwrap(); } - let err = archive_content_files(&archive_path, TarFormat::Tar, 0).unwrap_err(); + let err = archive_content_files(&archive_path, ArchiveFormat::Tar, 0).unwrap_err(); assert!(err.to_string().contains("non-regular archive entry")); } @@ -1811,7 +1794,7 @@ mod tests { // For now, we'll test with a nonexistent file to ensure the function // returns false when it can't read the archive let non_existent_path = Path::new("/non/existent/archive.tar.gz"); - let result = should_strip_components(non_existent_path, TarFormat::TarGz); + let result = should_strip_components(non_existent_path, ArchiveFormat::TarGz); assert!(result.is_err()); // Should fail to open nonexistent file // Note: To properly test this function, we would need actual tar archives @@ -1888,7 +1871,7 @@ mod tests { gz.finish().unwrap(); // Now test inspect_tar_contents - let result = inspect_tar_contents(temp_file.path(), TarFormat::TarGz).unwrap(); + let result = inspect_tar_contents(temp_file.path(), ArchiveFormat::TarGz).unwrap(); // Should have 3 top-level entries: dir1, dir2, standalone // NOT a single "." entry @@ -1915,7 +1898,7 @@ mod tests { } // Verify should_strip_components returns false (multiple top-level entries) - let should_strip = should_strip_components(temp_file.path(), TarFormat::TarGz).unwrap(); + let should_strip = should_strip_components(temp_file.path(), ArchiveFormat::TarGz).unwrap(); assert!( !should_strip, "Should NOT strip components for multi-entry archive" @@ -1988,31 +1971,65 @@ mod tests { } #[test] - fn test_tar_format_from_file_name() { - assert_eq!(TarFormat::from_file_name("foo.tar.gz"), TarFormat::TarGz); - assert_eq!(TarFormat::from_file_name("foo.tgz"), TarFormat::TarGz); - assert_eq!(TarFormat::from_file_name("foo.tar.xz"), TarFormat::TarXz); - assert_eq!(TarFormat::from_file_name("foo.txz"), TarFormat::TarXz); - assert_eq!(TarFormat::from_file_name("foo.tar.bz2"), TarFormat::TarBz2); - assert_eq!(TarFormat::from_file_name("foo.tbz2"), TarFormat::TarBz2); - assert_eq!(TarFormat::from_file_name("foo.tbz"), TarFormat::TarBz2); - assert_eq!(TarFormat::from_ext("tbz"), TarFormat::TarBz2); - assert_eq!(TarFormat::from_file_name("foo.tar.zst"), TarFormat::TarZst); - assert_eq!(TarFormat::from_file_name("foo.tzst"), TarFormat::TarZst); - assert_eq!(TarFormat::from_file_name("foo.tar"), TarFormat::Tar); - assert_eq!(TarFormat::from_file_name("foo.zip"), TarFormat::Zip); - assert_eq!(TarFormat::from_file_name("foo.vsix"), TarFormat::Zip); - assert_eq!(TarFormat::from_file_name("foo.7z"), TarFormat::SevenZip); - assert_eq!(TarFormat::from_file_name("foo.gz"), TarFormat::Gz); - assert_eq!(TarFormat::from_file_name("foo.xz"), TarFormat::Xz); - assert_eq!(TarFormat::from_file_name("foo.bz2"), TarFormat::Bz2); - assert_eq!(TarFormat::from_file_name("foo.zst"), TarFormat::Zst); - assert_eq!(TarFormat::from_file_name("foo"), TarFormat::Raw); - assert_eq!(TarFormat::from_file_name("foo.txt"), TarFormat::Raw); + fn test_archive_format_from_file_name() { + assert_eq!( + ArchiveFormat::from_file_name("foo.tar.gz"), + ArchiveFormat::TarGz + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.tgz"), + ArchiveFormat::TarGz + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.tar.xz"), + ArchiveFormat::TarXz + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.txz"), + ArchiveFormat::TarXz + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.tar.bz2"), + ArchiveFormat::TarBz2 + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.tbz2"), + ArchiveFormat::TarBz2 + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.tbz"), + ArchiveFormat::TarBz2 + ); + assert_eq!(ArchiveFormat::from_ext("tbz"), Some(ArchiveFormat::TarBz2)); + assert_eq!(ArchiveFormat::from_ext("tar.br"), None); + assert_eq!( + ArchiveFormat::from_file_name("foo.tar.zst"), + ArchiveFormat::TarZst + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.tzst"), + ArchiveFormat::TarZst + ); + assert_eq!(ArchiveFormat::from_file_name("foo.tar"), ArchiveFormat::Tar); + assert_eq!(ArchiveFormat::from_file_name("foo.zip"), ArchiveFormat::Zip); + assert_eq!( + ArchiveFormat::from_file_name("foo.vsix"), + ArchiveFormat::Zip + ); + assert_eq!( + ArchiveFormat::from_file_name("foo.7z"), + ArchiveFormat::SevenZip + ); + assert_eq!(ArchiveFormat::from_file_name("foo.gz"), ArchiveFormat::Gz); + assert_eq!(ArchiveFormat::from_file_name("foo.xz"), ArchiveFormat::Xz); + assert_eq!(ArchiveFormat::from_file_name("foo.bz2"), ArchiveFormat::Bz2); + assert_eq!(ArchiveFormat::from_file_name("foo.zst"), ArchiveFormat::Zst); + assert_eq!(ArchiveFormat::from_file_name("foo"), ArchiveFormat::Raw); + assert_eq!(ArchiveFormat::from_file_name("foo.txt"), ArchiveFormat::Raw); } #[test] - fn test_unarchive_single_file() { + fn test_decompress_file() { use flate2::Compression; use flate2::write::GzEncoder; use std::io::Write; @@ -2022,16 +2039,13 @@ mod tests { let src_path = dir.path().join("test.gz"); let dest_path = dir.path().join("test-out"); - // Create a dummy gzip file let file = File::create(&src_path).unwrap(); let mut encoder = GzEncoder::new(file, Compression::default()); encoder.write_all(b"hello world").unwrap(); encoder.finish().unwrap(); - // unarchive (decompress) it - unarchive(&src_path, &dest_path, TarFormat::Gz, &Default::default()).unwrap(); + decompress_file(&src_path, &dest_path, ArchiveFormat::Gz).unwrap(); - // Verify output assert!(dest_path.exists()); assert!(dest_path.is_file()); let content = std::fs::read_to_string(&dest_path).unwrap(); @@ -2039,36 +2053,7 @@ mod tests { } #[test] - fn test_unarchive_single_file_to_dir() { - use flate2::Compression; - use flate2::write::GzEncoder; - use std::io::Write; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let src_path = dir.path().join("test_file.gz"); - let dest_dir = dir.path().join("out_dir"); - std::fs::create_dir(&dest_dir).unwrap(); - - // Create a dummy gzip file - let file = File::create(&src_path).unwrap(); - let mut encoder = GzEncoder::new(file, Compression::default()); - encoder.write_all(b"hello world").unwrap(); - encoder.finish().unwrap(); - - // unarchive (decompress) it - unarchive(&src_path, &dest_dir, TarFormat::Gz, &Default::default()).unwrap(); - - // Verify output - should be out_dir/test_file - let expected_path = dest_dir.join("test_file"); - assert!(expected_path.exists()); - assert!(expected_path.is_file()); - let content = std::fs::read_to_string(&expected_path).unwrap(); - assert_eq!(content, "hello world"); - } - - #[test] - fn test_unarchive_single_file_creates_parent_dir() { + fn test_decompress_file_creates_parent_dir() { use flate2::Compression; use flate2::write::GzEncoder; use std::io::Write; @@ -2083,16 +2068,7 @@ mod tests { encoder.write_all(b"hello world").unwrap(); encoder.finish().unwrap(); - unarchive( - &src_path, - dir.path(), - TarFormat::Gz, - &ArchiveOptions { - single_file_dest: Some(&dest_path), - ..Default::default() - }, - ) - .unwrap(); + decompress_file(&src_path, &dest_path, ArchiveFormat::Gz).unwrap(); assert!(dest_path.exists()); assert!(dest_path.is_file()); @@ -2101,7 +2077,7 @@ mod tests { } #[test] - fn test_unarchive_zip() { + fn test_extract_archive_zip() { use std::io::Write; use tempfile::tempdir; @@ -2116,7 +2092,13 @@ mod tests { zip.write_all(b"hello world").unwrap(); zip.finish().unwrap(); - unarchive(&src_path, &dest_dir, TarFormat::Zip, &Default::default()).unwrap(); + extract_archive( + &src_path, + &dest_dir, + ArchiveFormat::Zip, + &ExtractOptions::default(), + ) + .unwrap(); let extracted_path = dest_dir.join("pkg").join("tool"); assert!(extracted_path.exists()); @@ -2137,7 +2119,7 @@ mod tests { &dest_path, &TarOptions { pr: None, - ..TarOptions::new(TarFormat::Gz) + ..TarOptions::new(ArchiveFormat::Gz) }, ) .unwrap_err(); diff --git a/src/plugins/asdf_plugin.rs b/src/plugins/asdf_plugin.rs index 4f5744456f..3c7675f1be 100644 --- a/src/plugins/asdf_plugin.rs +++ b/src/plugins/asdf_plugin.rs @@ -192,7 +192,8 @@ impl AsdfPlugin { pr.set_message("extracting zip file".to_string()); - let strip_components = file::should_strip_components(&temp_archive, file::TarFormat::Zip)?; + let strip_components = + file::should_strip_components(&temp_archive, file::ArchiveFormat::Zip)?; file::unzip( &temp_archive, diff --git a/src/plugins/core/erlang.rs b/src/plugins/core/erlang.rs index 013e403126..d2cc711d9f 100644 --- a/src/plugins/core/erlang.rs +++ b/src/plugins/core/erlang.rs @@ -149,7 +149,7 @@ impl ErlangPlugin { &tv.download_path(), &TarOptions { pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(file::TarFormat::TarGz) + ..TarOptions::new(file::ArchiveFormat::TarGz) }, )?; @@ -236,7 +236,7 @@ impl ErlangPlugin { &tv.install_path(), &TarOptions { pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(file::TarFormat::TarGz) + ..TarOptions::new(file::ArchiveFormat::TarGz) }, )?; Ok(Some(tv)) diff --git a/src/plugins/core/go.rs b/src/plugins/core/go.rs index 6453db2d93..174157cbe1 100644 --- a/src/plugins/core/go.rs +++ b/src/plugins/core/go.rs @@ -8,7 +8,7 @@ use crate::backend::{Backend, VersionInfo}; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; -use crate::file::{TarFormat, TarOptions}; +use crate::file::{ArchiveFormat, TarOptions}; use crate::http::HTTP; use crate::install_context::InstallContext; use crate::lockfile::PlatformInfo; @@ -162,7 +162,7 @@ impl GoPlugin { tmp_extract_path.path(), &TarOptions { pr: Some(pr), - ..TarOptions::new(TarFormat::TarGz) + ..TarOptions::new(ArchiveFormat::TarGz) }, )?; } diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index d5b2632805..48ecf97c3e 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -13,7 +13,7 @@ use crate::cli::args::BackendArg; use crate::cli::version::OS; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; -use crate::file::{ArchiveOptions, TarFormat}; +use crate::file::{ArchiveFormat, ExtractOptions}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; use crate::lockfile::PlatformInfo; @@ -218,12 +218,12 @@ impl JavaPlugin { ) -> Result<()> { let filename = tarball_path.file_name().unwrap().to_string_lossy(); pr.set_message(format!("extract {filename}")); - let format = TarFormat::from_file_name(&filename); - file::unarchive( + let format = ArchiveFormat::from_file_name(&filename); + file::extract_archive( tarball_path, &tv.download_path(), format, - &ArchiveOptions { + &ExtractOptions { pr: Some(pr), ..Default::default() }, diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index 9d2132bdf5..2d00573274 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -9,7 +9,7 @@ use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::settings::DEFAULT_NODE_MIRROR_URL; use crate::config::{Config, Settings}; -use crate::file::{TarFormat, TarOptions}; +use crate::file::{ArchiveFormat, TarOptions}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; use crate::lockfile::PlatformInfo; @@ -119,7 +119,7 @@ impl NodePlugin { &TarOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(TarFormat::TarGz) + ..TarOptions::new(ArchiveFormat::TarGz) }, )?; Ok(()) @@ -189,7 +189,7 @@ impl NodePlugin { opts.build_dir.parent().unwrap(), &TarOptions { pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(TarFormat::TarGz) + ..TarOptions::new(ArchiveFormat::TarGz) }, )?; self.exec_configure(ctx, opts, tv)?; diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index 973c2df0c5..1a9214b1a0 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -7,7 +7,7 @@ use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; -use crate::file::{ArchiveOptions, TarFormat, display_path}; +use crate::file::{ArchiveFormat, ExtractOptions, display_path}; use crate::git::{CloneOptions, Git}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; @@ -353,11 +353,11 @@ impl PythonPlugin { } file::remove_all(&install)?; - file::unarchive( + file::extract_archive( &tarball_path, &install, - TarFormat::from_file_name(filename), - &ArchiveOptions { + ArchiveFormat::from_file_name(filename), + &ExtractOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), ..Default::default() diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index 9fef5ac426..e2fe02801b 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -187,7 +187,7 @@ impl RubyPlugin { } let strip_components = - file::should_strip_components(&temp_archive, file::TarFormat::Zip)?; + file::should_strip_components(&temp_archive, file::ArchiveFormat::Zip)?; file::unzip( &temp_archive, @@ -796,7 +796,7 @@ impl RubyPlugin { &file::TarOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..file::TarOptions::new(file::TarFormat::TarGz) + ..file::TarOptions::new(file::ArchiveFormat::TarGz) }, )?; diff --git a/src/plugins/core/swift.rs b/src/plugins/core/swift.rs index 0fa2e51a58..b2400475fe 100644 --- a/src/plugins/core/swift.rs +++ b/src/plugins/core/swift.rs @@ -96,7 +96,7 @@ impl SwiftPlugin { &file::TarOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..file::TarOptions::new(file::TarFormat::TarGz) + ..file::TarOptions::new(file::ArchiveFormat::TarGz) }, )?; } diff --git a/src/plugins/core/zig.rs b/src/plugins/core/zig.rs index 4b1cae79e0..494f8b3033 100644 --- a/src/plugins/core/zig.rs +++ b/src/plugins/core/zig.rs @@ -11,7 +11,7 @@ use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; use crate::duration::DAILY; -use crate::file::{ArchiveOptions, TarFormat}; +use crate::file::{ArchiveFormat, ExtractOptions}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; use crate::lockfile::{PlatformInfo, ProvenanceType}; @@ -138,11 +138,11 @@ impl ZigPlugin { let filename = tarball_path.file_name().unwrap().to_string_lossy(); ctx.pr.set_message(format!("extract {filename}")); file::remove_all(tv.install_path())?; - file::unarchive( + file::extract_archive( tarball_path, &tv.install_path(), - TarFormat::from_file_name(&filename), - &ArchiveOptions { + ArchiveFormat::from_file_name(&filename), + &ExtractOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), ..Default::default() diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 4b368cf58e..89a3e2ffdd 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -159,7 +159,8 @@ impl VfoxPlugin { pr.set_message("extracting zip file".to_string()); - let strip_components = file::should_strip_components(&temp_archive, file::TarFormat::Zip)?; + let strip_components = + file::should_strip_components(&temp_archive, file::ArchiveFormat::Zip)?; file::unzip( &temp_archive, From c27b66ce371bb063931891fa98a5b0b343690f5d Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:47:03 +1000 Subject: [PATCH 11/19] fix(aqua): fail clearly on unimplemented extraction formats --- src/backend/aqua.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index f9194554d0..db4c552c55 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -2304,6 +2304,8 @@ impl AquaBackend { file::un_dmg(&tarball_path, &install_path)?; } else if format == "pkg" { file::un_pkg(&tarball_path, &install_path)?; + } else if is_unimplemented_aqua_extraction_format(format) { + bail!("aqua format {format} is not yet implemented"); } else { let Some(archive_format) = archive_format else { bail!("unsupported format: {}", format); @@ -2641,6 +2643,15 @@ fn ends_with_v(s: &str) -> bool { s.ends_with('v') || s.ends_with('V') } +/// Keep in sync with `aqua_registry::AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS`. +const AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS: &[&str] = &[ + "tar.br", "tbr", "tar.lz4", "tlz4", "lz4", "tar.sz", "tsz", "sz", "rar", +]; + +fn is_unimplemented_aqua_extraction_format(format: &str) -> bool { + AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS.contains(&format) +} + fn complete_windows_ext( mut path: PathBuf, pkg: &AquaPackage, From cc685ca6f755cf4fe570939dda7f12b8156a8db0 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:53:19 +1000 Subject: [PATCH 12/19] refactor(file): add unsupported aqua formats to ArchiveFormat Move tar.br, lz4, sz, rar and aliases into ArchiveFormat and fail via unimplemented! in open_tar and related extraction paths instead of a separate aqua-specific format list. --- src/backend/aqua.rs | 18 ++-------- src/file.rs | 83 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index db4c552c55..ce840340e4 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -2304,12 +2304,7 @@ impl AquaBackend { file::un_dmg(&tarball_path, &install_path)?; } else if format == "pkg" { file::un_pkg(&tarball_path, &install_path)?; - } else if is_unimplemented_aqua_extraction_format(format) { - bail!("aqua format {format} is not yet implemented"); - } else { - let Some(archive_format) = archive_format else { - bail!("unsupported format: {}", format); - }; + } else if let Some(archive_format) = archive_format { if archive_format.is_compressed_file() { file::decompress_file(&tarball_path, first_bin_path, archive_format)?; make_executable = true; @@ -2317,6 +2312,8 @@ impl AquaBackend { file::extract_archive(&tarball_path, &install_path, archive_format, &extract_opts)?; make_executable = true; } + } else { + bail!("unsupported format: {}", format); } if make_executable { @@ -2643,15 +2640,6 @@ fn ends_with_v(s: &str) -> bool { s.ends_with('v') || s.ends_with('V') } -/// Keep in sync with `aqua_registry::AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS`. -const AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS: &[&str] = &[ - "tar.br", "tbr", "tar.lz4", "tlz4", "lz4", "tar.sz", "tsz", "sz", "rar", -]; - -fn is_unimplemented_aqua_extraction_format(format: &str) -> bool { - AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS.contains(&format) -} - fn complete_windows_ext( mut path: PathBuf, pkg: &AquaPackage, diff --git a/src/file.rs b/src/file.rs index 675d30f5b6..cf2b45f6f7 100644 --- a/src/file.rs +++ b/src/file.rs @@ -893,6 +893,9 @@ pub fn decompress_file(input: &Path, dest: &Path, format: ArchiveFormat) -> Resu ArchiveFormat::Xz => un_xz(input, dest), ArchiveFormat::Zst => un_zst(input, dest), ArchiveFormat::Bz2 => un_bz2(input, dest), + ArchiveFormat::Lz4 | ArchiveFormat::Sz => { + unimplemented!("{format} format not supported") + } _ => bail!("unsupported compressed file format: {}", format), } } @@ -921,6 +924,18 @@ pub enum ArchiveFormat { Zip, #[strum(serialize = "7z")] SevenZip, + #[strum(serialize = "tar.br", serialize = "tbr")] + TarBr, + #[strum(serialize = "tar.lz4", serialize = "tlz4")] + TarLz4, + #[strum(serialize = "lz4")] + Lz4, + #[strum(serialize = "tar.sz", serialize = "tsz")] + TarSz, + #[strum(serialize = "sz")] + Sz, + #[strum(serialize = "rar")] + Rar, #[strum(serialize = "raw")] Raw, } @@ -955,11 +970,17 @@ impl ArchiveFormat { | ArchiveFormat::TarZst | ArchiveFormat::Tar | ArchiveFormat::Zip - | ArchiveFormat::SevenZip => true, + | ArchiveFormat::SevenZip + | ArchiveFormat::TarBr + | ArchiveFormat::TarLz4 + | ArchiveFormat::TarSz + | ArchiveFormat::Rar => true, ArchiveFormat::Gz | ArchiveFormat::Xz | ArchiveFormat::Bz2 | ArchiveFormat::Zst + | ArchiveFormat::Lz4 + | ArchiveFormat::Sz | ArchiveFormat::Raw => false, } } @@ -972,13 +993,21 @@ impl ArchiveFormat { | ArchiveFormat::TarBz2 | ArchiveFormat::TarZst | ArchiveFormat::Tar + | ArchiveFormat::TarBr + | ArchiveFormat::TarLz4 + | ArchiveFormat::TarSz ) } pub fn is_compressed_file(&self) -> bool { matches!( self, - ArchiveFormat::Gz | ArchiveFormat::Xz | ArchiveFormat::Bz2 | ArchiveFormat::Zst + ArchiveFormat::Gz + | ArchiveFormat::Xz + | ArchiveFormat::Bz2 + | ArchiveFormat::Zst + | ArchiveFormat::Lz4 + | ArchiveFormat::Sz ) } @@ -999,6 +1028,12 @@ impl ArchiveFormat { ArchiveFormat::Tar => Some("tar"), ArchiveFormat::Zip => Some("zip"), ArchiveFormat::SevenZip => Some("7z"), + ArchiveFormat::TarBr => Some("tar.br"), + ArchiveFormat::TarLz4 => Some("tar.lz4"), + ArchiveFormat::Lz4 => Some("lz4"), + ArchiveFormat::TarSz => Some("tar.sz"), + ArchiveFormat::Sz => Some("sz"), + ArchiveFormat::Rar => Some("rar"), ArchiveFormat::Raw => None, } } @@ -1063,6 +1098,9 @@ pub fn extract_archive( | ArchiveFormat::TarBz2 | ArchiveFormat::TarZst | ArchiveFormat::Tar + | ArchiveFormat::TarBr + | ArchiveFormat::TarLz4 + | ArchiveFormat::TarSz | ArchiveFormat::Raw => untar(archive, dest, &opts.tar_options(format)), ArchiveFormat::Zip => unzip( archive, @@ -1087,9 +1125,15 @@ pub fn extract_archive( bail!("7z format not supported on this platform"); } } - ArchiveFormat::Gz | ArchiveFormat::Xz | ArchiveFormat::Bz2 | ArchiveFormat::Zst => { + ArchiveFormat::Gz + | ArchiveFormat::Xz + | ArchiveFormat::Bz2 + | ArchiveFormat::Zst + | ArchiveFormat::Lz4 + | ArchiveFormat::Sz => { bail!("extract_archive does not support compressed single-file format: {format}") } + ArchiveFormat::Rar => unimplemented!("rar format not supported"), } } @@ -1208,6 +1252,12 @@ fn open_tar(format: ArchiveFormat, archive: &Path) -> Result bail!("zip format not supported"), ArchiveFormat::SevenZip => bail!("7z format not supported"), + ArchiveFormat::TarBr | ArchiveFormat::TarLz4 | ArchiveFormat::TarSz => { + unimplemented!("{format} format not supported") + } + ArchiveFormat::Lz4 | ArchiveFormat::Sz | ArchiveFormat::Rar => { + bail!("{} is not a tar archive", format) + } }) } @@ -1493,7 +1543,10 @@ pub fn archive_content_files( | ArchiveFormat::TarXz | ArchiveFormat::TarBz2 | ArchiveFormat::TarZst - | ArchiveFormat::Tar => archive_content_files_tar(archive_path, format, strip_components), + | ArchiveFormat::Tar + | ArchiveFormat::TarBr + | ArchiveFormat::TarLz4 + | ArchiveFormat::TarSz => archive_content_files_tar(archive_path, format, strip_components), ArchiveFormat::Zip => archive_content_files_zip(archive_path, strip_components), ArchiveFormat::SevenZip => { bail!("content-level SLSA verification does not support 7z archives") @@ -1502,9 +1555,12 @@ pub fn archive_content_files( | ArchiveFormat::Xz | ArchiveFormat::Bz2 | ArchiveFormat::Zst + | ArchiveFormat::Lz4 + | ArchiveFormat::Sz | ArchiveFormat::Raw => { bail!("content-level SLSA verification only supports archive formats") } + ArchiveFormat::Rar => unimplemented!("rar format not supported"), } } @@ -2001,7 +2057,24 @@ mod tests { ArchiveFormat::TarBz2 ); assert_eq!(ArchiveFormat::from_ext("tbz"), Some(ArchiveFormat::TarBz2)); - assert_eq!(ArchiveFormat::from_ext("tar.br"), None); + assert_eq!( + ArchiveFormat::from_ext("tar.br"), + Some(ArchiveFormat::TarBr) + ); + assert_eq!(ArchiveFormat::from_ext("tbr"), Some(ArchiveFormat::TarBr)); + assert_eq!( + ArchiveFormat::from_ext("tar.lz4"), + Some(ArchiveFormat::TarLz4) + ); + assert_eq!(ArchiveFormat::from_ext("tlz4"), Some(ArchiveFormat::TarLz4)); + assert_eq!(ArchiveFormat::from_ext("lz4"), Some(ArchiveFormat::Lz4)); + assert_eq!( + ArchiveFormat::from_ext("tar.sz"), + Some(ArchiveFormat::TarSz) + ); + assert_eq!(ArchiveFormat::from_ext("tsz"), Some(ArchiveFormat::TarSz)); + assert_eq!(ArchiveFormat::from_ext("sz"), Some(ArchiveFormat::Sz)); + assert_eq!(ArchiveFormat::from_ext("rar"), Some(ArchiveFormat::Rar)); assert_eq!( ArchiveFormat::from_file_name("foo.tar.zst"), ArchiveFormat::TarZst From 79912e94a0a89c56863375150065faf48b5c5287 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:18:34 +1000 Subject: [PATCH 13/19] fix(java): prefer metadata file_type for archive extraction Use ArchiveFormat::from_ext on JavaMetadata.file_type when present and fall back to filename detection only when metadata omits the format. --- src/plugins/core/java.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index 48ecf97c3e..b456a54722 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -218,7 +218,11 @@ impl JavaPlugin { ) -> Result<()> { let filename = tarball_path.file_name().unwrap().to_string_lossy(); pr.set_message(format!("extract {filename}")); - let format = ArchiveFormat::from_file_name(&filename); + let format = m + .file_type + .as_deref() + .and_then(ArchiveFormat::from_ext) + .unwrap_or_else(|| ArchiveFormat::from_file_name(&filename)); file::extract_archive( tarball_path, &tv.download_path(), From d9d5b2cd6802fd61972ae6afb909512623c2bdb3 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:19:27 +1000 Subject: [PATCH 14/19] refactor(aqua): deduplicate make_executable in archive install --- src/backend/aqua.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index ce840340e4..540730ec67 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -2307,11 +2307,10 @@ impl AquaBackend { } else if let Some(archive_format) = archive_format { if archive_format.is_compressed_file() { file::decompress_file(&tarball_path, first_bin_path, archive_format)?; - make_executable = true; } else { file::extract_archive(&tarball_path, &install_path, archive_format, &extract_opts)?; - make_executable = true; } + make_executable = true; } else { bail!("unsupported format: {}", format); } From cf707093cf3b5e9cb7e2bfffbb1cb7faf0d66f56 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:14:25 +1000 Subject: [PATCH 15/19] fix(file): return errors for unsupported archive formats Replace unimplemented! with bail! for tar.br, lz4, sz, and rar so encountering these formats surfaces a CLI error instead of panicking. --- src/file.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/file.rs b/src/file.rs index cf2b45f6f7..ce30c62412 100644 --- a/src/file.rs +++ b/src/file.rs @@ -894,7 +894,7 @@ pub fn decompress_file(input: &Path, dest: &Path, format: ArchiveFormat) -> Resu ArchiveFormat::Zst => un_zst(input, dest), ArchiveFormat::Bz2 => un_bz2(input, dest), ArchiveFormat::Lz4 | ArchiveFormat::Sz => { - unimplemented!("{format} format not supported") + bail!("{format} format not supported") } _ => bail!("unsupported compressed file format: {}", format), } @@ -1133,7 +1133,7 @@ pub fn extract_archive( | ArchiveFormat::Sz => { bail!("extract_archive does not support compressed single-file format: {format}") } - ArchiveFormat::Rar => unimplemented!("rar format not supported"), + ArchiveFormat::Rar => bail!("rar format not supported"), } } @@ -1253,7 +1253,7 @@ fn open_tar(format: ArchiveFormat, archive: &Path) -> Result bail!("zip format not supported"), ArchiveFormat::SevenZip => bail!("7z format not supported"), ArchiveFormat::TarBr | ArchiveFormat::TarLz4 | ArchiveFormat::TarSz => { - unimplemented!("{format} format not supported") + bail!("{format} format not supported") } ArchiveFormat::Lz4 | ArchiveFormat::Sz | ArchiveFormat::Rar => { bail!("{} is not a tar archive", format) @@ -1560,7 +1560,7 @@ pub fn archive_content_files( | ArchiveFormat::Raw => { bail!("content-level SLSA verification only supports archive formats") } - ArchiveFormat::Rar => unimplemented!("rar format not supported"), + ArchiveFormat::Rar => bail!("rar format not supported"), } } From cbf921e6ec8137627a2b85acf19d5948dcc94d5b Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:20:50 +1000 Subject: [PATCH 16/19] fix(http): remove needless ExtractOptions struct update Satisfy clippy::needless_update after all ExtractOptions fields were set explicitly in extract_archive. --- src/backend/http.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index f4e2c83f1e..f25de8b0fa 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -448,7 +448,6 @@ impl HttpBackend { strip_components: strip_components.unwrap_or(0), pr, preserve_mtime: false, - ..Default::default() }; file::extract_archive(file_path, dest, file_info.format, &extract_opts)?; From 78f446994851c6d7c0377eda967c50cdef6f8669 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:07:39 +1000 Subject: [PATCH 17/19] refactor(file): derive is_archive from is_tar_archive --- src/file.rs | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/file.rs b/src/file.rs index ce30c62412..631634db34 100644 --- a/src/file.rs +++ b/src/file.rs @@ -963,26 +963,11 @@ impl ArchiveFormat { } pub fn is_archive(&self) -> bool { - match self { - ArchiveFormat::TarGz - | ArchiveFormat::TarXz - | ArchiveFormat::TarBz2 - | ArchiveFormat::TarZst - | ArchiveFormat::Tar - | ArchiveFormat::Zip - | ArchiveFormat::SevenZip - | ArchiveFormat::TarBr - | ArchiveFormat::TarLz4 - | ArchiveFormat::TarSz - | ArchiveFormat::Rar => true, - ArchiveFormat::Gz - | ArchiveFormat::Xz - | ArchiveFormat::Bz2 - | ArchiveFormat::Zst - | ArchiveFormat::Lz4 - | ArchiveFormat::Sz - | ArchiveFormat::Raw => false, - } + self.is_tar_archive() + || matches!( + self, + ArchiveFormat::Zip | ArchiveFormat::SevenZip | ArchiveFormat::Rar + ) } pub fn is_tar_archive(&self) -> bool { From 4e3faf7024cf2c99049e56cf6df4264055d89ece Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:15:24 +1000 Subject: [PATCH 18/19] refactor(file): derive ArchiveFormat extension from strum Display --- src/backend/http.rs | 17 +++++++---------- src/file.rs | 23 ++--------------------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/backend/http.rs b/src/backend/http.rs index f25de8b0fa..f48e17c3ab 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -83,16 +83,13 @@ impl FileInfo { let file_name = effective_path.file_name().unwrap().to_string_lossy(); let format = file::ArchiveFormat::from_file_name(&file_name); - let extension = format - .extension() - .map(|s| s.to_string()) - .unwrap_or_else(|| { - effective_path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_string() - }); + let extension = format.extension().unwrap_or_else(|| { + effective_path + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string() + }); let is_compressed_binary = !format.is_archive() && format != file::ArchiveFormat::Raw; diff --git a/src/file.rs b/src/file.rs index 631634db34..b734ccfeb9 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1000,27 +1000,8 @@ impl ArchiveFormat { self.is_tar_archive() || *self == ArchiveFormat::Raw } - pub fn extension(&self) -> Option<&'static str> { - match self { - ArchiveFormat::TarGz => Some("tar.gz"), - ArchiveFormat::Gz => Some("gz"), - ArchiveFormat::TarXz => Some("tar.xz"), - ArchiveFormat::Xz => Some("xz"), - ArchiveFormat::TarBz2 => Some("tar.bz2"), - ArchiveFormat::Bz2 => Some("bz2"), - ArchiveFormat::TarZst => Some("tar.zst"), - ArchiveFormat::Zst => Some("zst"), - ArchiveFormat::Tar => Some("tar"), - ArchiveFormat::Zip => Some("zip"), - ArchiveFormat::SevenZip => Some("7z"), - ArchiveFormat::TarBr => Some("tar.br"), - ArchiveFormat::TarLz4 => Some("tar.lz4"), - ArchiveFormat::Lz4 => Some("lz4"), - ArchiveFormat::TarSz => Some("tar.sz"), - ArchiveFormat::Sz => Some("sz"), - ArchiveFormat::Rar => Some("rar"), - ArchiveFormat::Raw => None, - } + pub fn extension(&self) -> Option { + (*self != ArchiveFormat::Raw).then(|| self.to_string()) } } From f4d83dde730e39104c57e5e8296edf19fc8a03e6 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:27:23 +1000 Subject: [PATCH 19/19] refactor(file): remove TarOptions in favor of ExtractOptions --- src/file.rs | 50 +++++++++----------------------------- src/plugins/core/erlang.rs | 12 +++++---- src/plugins/core/go.rs | 7 +++--- src/plugins/core/node.rs | 12 +++++---- src/plugins/core/ruby.rs | 5 ++-- src/plugins/core/swift.rs | 5 ++-- 6 files changed, 35 insertions(+), 56 deletions(-) diff --git a/src/file.rs b/src/file.rs index b734ccfeb9..b62591130c 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1022,36 +1022,6 @@ impl<'a> Default for ExtractOptions<'a> { } } -impl<'a> ExtractOptions<'a> { - fn tar_options(&self, format: ArchiveFormat) -> TarOptions<'a> { - TarOptions { - format, - strip_components: self.strip_components, - pr: self.pr, - preserve_mtime: self.preserve_mtime, - } - } -} - -pub struct TarOptions<'a> { - pub format: ArchiveFormat, - pub strip_components: usize, - pub pr: Option<&'a dyn SingleReport>, - /// When false, files will be extracted with current timestamp instead of archive's mtime - pub preserve_mtime: bool, -} - -impl<'a> TarOptions<'a> { - pub fn new(format: ArchiveFormat) -> Self { - Self { - format, - strip_components: 0, - pr: None, - preserve_mtime: true, - } - } -} - pub fn extract_archive( archive: &Path, dest: &Path, @@ -1067,7 +1037,7 @@ pub fn extract_archive( | ArchiveFormat::TarBr | ArchiveFormat::TarLz4 | ArchiveFormat::TarSz - | ArchiveFormat::Raw => untar(archive, dest, &opts.tar_options(format)), + | ArchiveFormat::Raw => untar(archive, dest, format, opts), ArchiveFormat::Zip => unzip( archive, dest, @@ -1103,9 +1073,14 @@ pub fn extract_archive( } } -pub fn untar(archive: &Path, dest: &Path, opts: &TarOptions) -> Result<()> { - if !opts.format.can_open_with_tar() { - bail!("untar only supports tar formats, got {}", opts.format); +pub fn untar( + archive: &Path, + dest: &Path, + format: ArchiveFormat, + opts: &ExtractOptions, +) -> Result<()> { + if !format.can_open_with_tar() { + bail!("untar only supports tar formats, got {}", format); } debug!("tar -xf {} -C {}", archive.display(), dest.display()); @@ -1122,7 +1097,6 @@ pub fn untar(archive: &Path, dest: &Path, opts: &TarOptions) -> Result<()> { format!("failed to extract tar: {archive} to {dest}") }; - let format = opts.format; let tar = open_tar(format, archive)?; // TODO: put this back in when we can read+write in parallel // let mut cur = Cursor::new(vec![]); @@ -2156,10 +2130,8 @@ mod tests { let err = untar( &src_path, &dest_path, - &TarOptions { - pr: None, - ..TarOptions::new(ArchiveFormat::Gz) - }, + ArchiveFormat::Gz, + &ExtractOptions::default(), ) .unwrap_err(); diff --git a/src/plugins/core/erlang.rs b/src/plugins/core/erlang.rs index d2cc711d9f..ffd58889f0 100644 --- a/src/plugins/core/erlang.rs +++ b/src/plugins/core/erlang.rs @@ -7,7 +7,7 @@ use crate::backend::platform_target::PlatformTarget; use crate::cli::args::BackendArg; use crate::config::{Config, Settings}; #[cfg(unix)] -use crate::file::TarOptions; +use crate::file::ExtractOptions; use crate::file::display_path; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; @@ -147,9 +147,10 @@ impl ErlangPlugin { file::untar( &tarball_path, &tv.download_path(), - &TarOptions { + file::ArchiveFormat::TarGz, + &ExtractOptions { pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(file::ArchiveFormat::TarGz) + ..Default::default() }, )?; @@ -234,9 +235,10 @@ impl ErlangPlugin { file::untar( &tarball_path, &tv.install_path(), - &TarOptions { + file::ArchiveFormat::TarGz, + &ExtractOptions { pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(file::ArchiveFormat::TarGz) + ..Default::default() }, )?; Ok(Some(tv)) diff --git a/src/plugins/core/go.rs b/src/plugins/core/go.rs index 174157cbe1..c46066c158 100644 --- a/src/plugins/core/go.rs +++ b/src/plugins/core/go.rs @@ -8,7 +8,7 @@ use crate::backend::{Backend, VersionInfo}; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::{Config, Settings}; -use crate::file::{ArchiveFormat, TarOptions}; +use crate::file::{ArchiveFormat, ExtractOptions}; use crate::http::HTTP; use crate::install_context::InstallContext; use crate::lockfile::PlatformInfo; @@ -160,9 +160,10 @@ impl GoPlugin { file::untar( tarball_path, tmp_extract_path.path(), - &TarOptions { + ArchiveFormat::TarGz, + &ExtractOptions { pr: Some(pr), - ..TarOptions::new(ArchiveFormat::TarGz) + ..Default::default() }, )?; } diff --git a/src/plugins/core/node.rs b/src/plugins/core/node.rs index 2d00573274..ed6fa95cdb 100644 --- a/src/plugins/core/node.rs +++ b/src/plugins/core/node.rs @@ -9,7 +9,7 @@ use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; use crate::config::settings::DEFAULT_NODE_MIRROR_URL; use crate::config::{Config, Settings}; -use crate::file::{ArchiveFormat, TarOptions}; +use crate::file::{ArchiveFormat, ExtractOptions}; use crate::http::{HTTP, HTTP_FETCH}; use crate::install_context::InstallContext; use crate::lockfile::PlatformInfo; @@ -116,10 +116,11 @@ impl NodePlugin { file::untar( &opts.binary_tarball_path, &opts.install_path, - &TarOptions { + ArchiveFormat::TarGz, + &ExtractOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(ArchiveFormat::TarGz) + ..Default::default() }, )?; Ok(()) @@ -187,9 +188,10 @@ impl NodePlugin { file::untar( &opts.source_tarball_path, opts.build_dir.parent().unwrap(), - &TarOptions { + ArchiveFormat::TarGz, + &ExtractOptions { pr: Some(ctx.pr.as_ref()), - ..TarOptions::new(ArchiveFormat::TarGz) + ..Default::default() }, )?; self.exec_configure(ctx, opts, tv)?; diff --git a/src/plugins/core/ruby.rs b/src/plugins/core/ruby.rs index e2fe02801b..d31ed5ac61 100644 --- a/src/plugins/core/ruby.rs +++ b/src/plugins/core/ruby.rs @@ -793,10 +793,11 @@ impl RubyPlugin { file::untar( &tarball_path, &install_path, - &file::TarOptions { + file::ArchiveFormat::TarGz, + &file::ExtractOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..file::TarOptions::new(file::ArchiveFormat::TarGz) + ..Default::default() }, )?; diff --git a/src/plugins/core/swift.rs b/src/plugins/core/swift.rs index b2400475fe..61af4ed427 100644 --- a/src/plugins/core/swift.rs +++ b/src/plugins/core/swift.rs @@ -93,10 +93,11 @@ impl SwiftPlugin { file::untar( tarball_path, &tv.install_path(), - &file::TarOptions { + file::ArchiveFormat::TarGz, + &file::ExtractOptions { strip_components: 1, pr: Some(ctx.pr.as_ref()), - ..file::TarOptions::new(file::ArchiveFormat::TarGz) + ..Default::default() }, )?; }