Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/aqua-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ pub use cache::RegistryCache;
pub use codec::{decode_package_rkyv, encode_package_rkyv};
pub use compiled::{CompiledRegistry, ParsedRegistry};
pub use types::{
AquaChecksum, AquaChecksumType, AquaCosign, AquaFile, AquaMinisignType, AquaPackage,
AquaPackageType, AquaVar, RegistryYaml,
AQUA_ASSET_FORMATS, AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS, AquaChecksum, AquaChecksumType,
AquaCosign, AquaFile, AquaMinisignType, AquaPackage, AquaPackageType, AquaVar, RegistryYaml,
is_unimplemented_aqua_extraction_format,
};

use thiserror::Error;
Expand Down
129 changes: 94 additions & 35 deletions crates/aqua-registry/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,23 +462,7 @@ impl AquaPackage {

/// Detect the format of an archive based on its filename
fn detect_format(&self, asset_name: &str) -> &'static str {
let formats = [
"tar.br", "tar.bz2", "tar.gz", "tar.lz4", "tar.sz", "tar.xz", "tbr", "tbz", "tbz2",
"tgz", "tlz4", "tsz", "txz", "tar.zst", "zip", "7z", "gz", "bz2", "lz4", "sz", "xz",
"zst", "dmg", "pkg", "rar", "tar",
];

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,
};
}
}
"raw"
remove_ext_from_asset(asset_name).1
}

fn append_ext_enabled(&self) -> bool {
Expand Down Expand Up @@ -570,12 +554,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)
}
Expand Down Expand Up @@ -881,6 +860,38 @@ fn split_version_prefix(version: &str) -> (String, String) {
)
}

pub const AQUA_ASSET_FORMATS: &[&str] = &[
"tar.br", "tar.bz2", "tar.gz", "tar.lz4", "tar.sz", "tar.xz", "tbr", "tbz", "tbz2", "tgz",
"tlz4", "tsz", "txz", "tar.zst", "zip", "7z", "gz", "bz2", "lz4", "sz", "xz", "zst", "dmg",
"pkg", "rar", "tar",
];

/// Aqua asset suffixes recognized for filename parsing but not yet supported by mise extraction.
pub const AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS: &[&str] = &[
"tar.br", "tbr", "tar.lz4", "tlz4", "lz4", "tar.sz", "tsz", "sz", "rar",
];

pub fn is_unimplemented_aqua_extraction_format(format: &str) -> bool {
AQUA_UNIMPLEMENTED_EXTRACTION_FORMATS.contains(&format)
}

fn remove_ext_from_asset(asset: &str) -> (&str, &'static str) {
for &format in AQUA_ASSET_FORMATS {
if let Some(asset_without_ext) = strip_asset_format_suffix(asset, format) {
return (asset_without_ext, format);
}
}
(asset, "raw")
}

fn asset_without_ext(asset: &str) -> &str {
remove_ext_from_asset(asset).0
}

fn strip_asset_format_suffix<'a>(asset: &'a str, format: &str) -> Option<&'a str> {
asset.strip_suffix(format)?.strip_suffix('.')
}

impl AquaFile {
fn template_ctx(
&self,
Expand All @@ -890,18 +901,7 @@ impl AquaFile {
arch: &str,
) -> Result<HashMap<String, String>> {
let asset = pkg.asset(v, os, arch)?;
let asset = asset.strip_suffix(".tar.gz").unwrap_or(&asset);
let asset = asset.strip_suffix(".tar.xz").unwrap_or(asset);
let asset = asset.strip_suffix(".tar.bz2").unwrap_or(asset);
let asset = asset.strip_suffix(".gz").unwrap_or(asset);
let asset = asset.strip_suffix(".xz").unwrap_or(asset);
let asset = asset.strip_suffix(".bz2").unwrap_or(asset);
let asset = asset.strip_suffix(".zip").unwrap_or(asset);
let asset = asset.strip_suffix(".tar").unwrap_or(asset);
let asset = asset.strip_suffix(".tgz").unwrap_or(asset);
let asset = asset.strip_suffix(".txz").unwrap_or(asset);
let asset = asset.strip_suffix(".tbz2").unwrap_or(asset);
let asset = asset.strip_suffix(".tbz").unwrap_or(asset);
let asset = asset_without_ext(&asset);

let mut ctx = HashMap::new();
ctx.insert("AssetWithoutExt".to_string(), asset.to_string());
Expand Down Expand Up @@ -1396,6 +1396,65 @@ packages:
assert_eq!(result, Some("gradle-8.14.3/bin/gradle".to_string()));
}

#[test]
fn test_aqua_file_src_asset_without_ext_strips_zst() {
let pkg = AquaPackage {
repo_owner: "openai".to_string(),
repo_name: "codex".to_string(),
asset: "codex-{{.Arch}}-{{.OS}}.exe.{{.Format}}".to_string(),
format: "zst".to_string(),
replacements: HashMap::from([
("amd64".to_string(), "x86_64".to_string()),
("windows".to_string(), "pc-windows-msvc".to_string()),
]),
..Default::default()
};
let file = AquaFile {
name: "codex".to_string(),
src: Some("{{.AssetWithoutExt}}".to_string()),
..Default::default()
};

let result = file.src(&pkg, "0.133.0", "windows", "amd64").unwrap();

assert_eq!(result, Some("codex-x86_64-pc-windows-msvc.exe".to_string()));
}

#[test]
fn test_remove_ext_from_asset_uses_aqua_asset_formats() {
assert_eq!(
remove_ext_from_asset("tfcmt_linux_amd64.tar.gz"),
("tfcmt_linux_amd64", "tar.gz")
);
assert_eq!(
remove_ext_from_asset("tfcmt_linux_amd64.tgz"),
("tfcmt_linux_amd64", "tgz")
);
assert_eq!(
remove_ext_from_asset("tfcmt_linux_amd64.tbz"),
("tfcmt_linux_amd64", "tbz")
);
assert_eq!(remove_ext_from_asset("tool.tar.br"), ("tool", "tar.br"));
assert_eq!(
remove_ext_from_asset("codex-x86_64-pc-windows-msvc.exe.zst"),
("codex-x86_64-pc-windows-msvc.exe", "zst")
);
assert_eq!(remove_ext_from_asset("tfcmt.js"), ("tfcmt.js", "raw"));
assert_eq!(
remove_ext_from_asset("tfcmt_windows_amd64.exe"),
("tfcmt_windows_amd64.exe", "raw")
);
}
Comment on lines +1423 to +1447

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add test coverage for shorthand format normalization.

The test verifies that remove_ext_from_asset strips known extensions and handles the common .tgz"tar.gz" normalization, but does not cover the newly supported tar-based shorthands (.tbr, .tlz4, .tsz). Adding assertions for these formats will prevent regressions if the normalization logic is updated.

🧪 Suggested test additions
     assert_eq!(
         remove_ext_from_asset("tfcmt_linux_amd64.tgz"),
         ("tfcmt_linux_amd64", "tar.gz")
     );
+    assert_eq!(
+        remove_ext_from_asset("tool.tbr"),
+        ("tool", "tar.br")
+    );
+    assert_eq!(
+        remove_ext_from_asset("tool.tlz4"),
+        ("tool", "tar.lz4")
+    );
+    assert_eq!(
+        remove_ext_from_asset("tool.tsz"),
+        ("tool", "tar.sz")
+    );
     assert_eq!(remove_ext_from_asset("tool.tar.br"), ("tool", "tar.br"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aqua-registry/src/types.rs` around lines 1428 - 1448, The test
test_remove_ext_from_asset_uses_aqua_asset_formats is missing assertions for the
shorthand tar-based suffixes; update that test to add assertions calling
remove_ext_from_asset for filenames ending with .tbr, .tlz4, and .tsz and assert
they normalize to ("name", "tar.br"), ("name", "tar.lz4"), and ("name",
"tar.zst") respectively (e.g., "tool.tbr" -> ("tool","tar.br"), "tool.tlz4" ->
("tool","tar.lz4"), "tool.tsz" -> ("tool","tar.zst")); keep the test name and
placement and follow the same assert_eq! style as the other cases so the
normalization behavior is covered.


#[test]
fn test_is_unimplemented_aqua_extraction_format() {
assert!(is_unimplemented_aqua_extraction_format("tar.br"));
assert!(is_unimplemented_aqua_extraction_format("tbr"));
assert!(is_unimplemented_aqua_extraction_format("rar"));
assert!(!is_unimplemented_aqua_extraction_format("tar.gz"));
assert!(!is_unimplemented_aqua_extraction_format("zst"));
}

#[test]
fn test_aqua_file_src_empty_asset_produces_absolute_path() {
// When a linked version name like "brew" matches a wrong version_override
Expand Down
10 changes: 10 additions & 0 deletions e2e-win/backend/aqua_bin_path.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@ Describe 'backend_aqua' {
It 'executes tree-sitter via aqua backend on Windows' {
mise x aqua:tree-sitter/tree-sitter -- tree-sitter --version | Should -BeLike "tree-sitter *"
}

It 'installs gzipped Windows binaries with version dots via aqua backend' {
$paths = @(mise x aqua:evilmartians/lefthook@2.1.8 -- where.exe lefthook)
$paths[0] | Should -Match 'lefthook\.exe$'
}

It 'installs zstd-compressed Windows executables via aqua backend' {
$paths = @(mise x aqua:openai/codex@0.133.0 -- where.exe codex)
$paths[0] | Should -Match 'codex\.exe$'
}
}
48 changes: 20 additions & 28 deletions src/backend/aqua.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{ArchiveFormat, ExtractOptions};
use crate::http::HTTP;
use crate::install_context::InstallContext;
use crate::lockfile::{PlatformInfo, ProvenanceType};
Expand Down Expand Up @@ -1270,7 +1270,8 @@ impl AquaBackend {
v: &str,
) -> Result<bool> {
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"
Expand Down Expand Up @@ -2280,13 +2281,15 @@ impl AquaBackend {
let first_bin_path = bin_paths
.first()
.expect("at least one bin path should exist");
let tar_opts = TarOptions {
let extract_opts = ExtractOptions {
pr: Some(ctx.pr.as_ref()),
..TarOptions::new(TarFormat::from_ext(format))
..Default::default()
};
let archive_format = ArchiveFormat::from_ext(format);
let mut make_executable = false;
if let AquaPackageType::GithubArchive = pkg.r#type {
file::untar(&tarball_path, &install_path, &tar_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)?;
Expand All @@ -2295,34 +2298,23 @@ 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 if aqua_registry::is_unimplemented_aqua_extraction_format(format) {
bail!("aqua format {format} is not yet implemented");
} else {
bail!("unsupported format: {}", format);
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;
}
}

if make_executable {
Expand Down
6 changes: 3 additions & 3 deletions src/backend/asset_matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) ==========
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/backend/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1820,9 +1820,9 @@ impl UnifiedGitBackend {
) -> Result<bool> {
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(),
)
};
Expand Down
25 changes: 9 additions & 16 deletions src/backend/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)
Expand All @@ -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::decompress_file(file_path, &dest_file, file_info.format)?;

file::make_executable(&dest_file)?;
Ok(ExtractionType::RawFile { filename })
Expand Down Expand Up @@ -451,14 +444,14 @@ impl HttpBackend {
strip_components = Some(1);
}

let tar_opts = file::TarOptions {
format: file_info.format,
let extract_opts = file::ExtractOptions {
strip_components: strip_components.unwrap_or(0),
pr,
preserve_mtime: false,
..Default::default()
};

file::untar(file_path, dest, &tar_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() {
Expand Down
Loading
Loading