diff --git a/crates/aqua-registry/src/cache.rs b/crates/aqua-registry/src/cache.rs index b531cffd3b..c0b954755a 100644 --- a/crates/aqua-registry/src/cache.rs +++ b/crates/aqua-registry/src/cache.rs @@ -7,7 +7,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; -const COMPILED_REGISTRY_CACHE_VERSION: &str = "v1"; +const COMPILED_REGISTRY_CACHE_VERSION: &str = "v2"; #[derive(Debug, Clone)] pub struct RegistryCache { diff --git a/crates/aqua-registry/src/file_ext.rs b/crates/aqua-registry/src/file_ext.rs new file mode 100644 index 0000000000..8c16eed482 --- /dev/null +++ b/crates/aqua-registry/src/file_ext.rs @@ -0,0 +1,126 @@ +use std::borrow::Cow; + +pub(crate) fn append_str_ext(s: &str, ext: &str) -> String { + if ext.is_empty() { + return s.to_string(); + } + if ext.starts_with('.') { + format!("{s}{ext}") + } else { + format!("{s}.{ext}") + } +} + +pub(crate) fn file_ext(filename: &str, version: &str) -> Option { + let version = version.trim_start_matches(['v', 'V']); + let filename = file_name_without_version(filename, version); + filename + .rsplit_once('.') + .and_then(|(_, ext)| is_likely_file_extension(ext).then_some(ext.to_string())) +} + +pub(crate) fn file_ext_is_empty(filename: &str, version: &str) -> bool { + file_ext(filename, version).is_none() +} + +fn file_name_without_version<'a>(file_name: &'a str, version: &str) -> Cow<'a, str> { + if version.is_empty() { + return Cow::Borrowed(file_name); + } + + let mut stripped = None::; + let mut anchor = 0; + let mut search_start = 0; + while let Some(relative_start) = file_name[search_start..].find(version) { + let start = search_start + relative_start; + let end = start + version.len(); + if is_version_boundary_before(file_name, start) && is_version_boundary_after(file_name, end) + { + stripped + .get_or_insert_with(|| String::with_capacity(file_name.len())) + .push_str(&file_name[anchor..start]); + anchor = end; + search_start = end; + } else { + search_start = start + + file_name[start..] + .chars() + .next() + .map(char::len_utf8) + .unwrap_or(1); + } + } + + match stripped { + Some(mut stripped) => { + stripped.push_str(&file_name[anchor..]); + Cow::Owned(stripped) + } + None => Cow::Borrowed(file_name), + } +} + +fn is_version_boundary_before(s: &str, index: usize) -> bool { + let Some((prev_index, prev)) = s[..index].char_indices().next_back() else { + return true; + }; + match prev { + '-' | '_' | '+' | ' ' => true, + '.' => s[..prev_index] + .chars() + .next_back() + .is_none_or(|c| !c.is_ascii_digit()), + 'v' | 'V' => s[..prev_index] + .chars() + .next_back() + .is_none_or(|c| matches!(c, '-' | '_' | '+' | ' ' | '.')), + _ => false, + } +} + +fn is_version_boundary_after(s: &str, index: usize) -> bool { + let Some(next) = s[index..].chars().next() else { + return true; + }; + match next { + '-' | '_' | '+' | ' ' => true, + '.' => s[index + next.len_utf8()..] + .chars() + .next() + .is_none_or(|c| !c.is_ascii_digit()), + _ => false, + } +} + +fn is_likely_file_extension(ext: &str) -> bool { + !ext.is_empty() + && !ext.chars().all(|c| c.is_ascii_digit()) + && !ext.chars().any(|c| matches!(c, '-' | '_' | '+' | ' ')) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_ext_ignores_selected_version_dots() { + assert_eq!(file_ext("tool_1.0.0", "v1.0.0"), None); + assert_eq!(file_ext("tool.1.0.0", "v1.0.0"), None); + assert_eq!(file_ext("x1.8atool_1.8_win", "1.8"), None); + assert_eq!(file_ext("tool-1.1.1", "1.1"), None); + } + + #[test] + fn file_ext_preserves_real_extensions() { + assert_eq!(file_ext("arq.bat", "1.0.0"), Some("bat".to_string())); + assert_eq!(file_ext("tool.jar", "1.0.0"), Some("jar".to_string())); + assert_eq!( + file_ext("tool_1.0.0.bat", "v1.0.0"), + Some("bat".to_string()) + ); + assert_eq!( + file_ext("tool_1.0.0.ps1", "v1.0.0"), + Some("ps1".to_string()) + ); + } +} diff --git a/crates/aqua-registry/src/lib.rs b/crates/aqua-registry/src/lib.rs index 7a0febabf4..98bdc2cdf4 100644 --- a/crates/aqua-registry/src/lib.rs +++ b/crates/aqua-registry/src/lib.rs @@ -8,6 +8,7 @@ mod cache; mod codec; mod compiled; +mod file_ext; mod template; pub mod types; diff --git a/crates/aqua-registry/src/types.rs b/crates/aqua-registry/src/types.rs index 4a8142bb2d..4a04e160ac 100644 --- a/crates/aqua-registry/src/types.rs +++ b/crates/aqua-registry/src/types.rs @@ -1,3 +1,4 @@ +use crate::file_ext::{append_str_ext, file_ext, file_ext_is_empty}; use expr::{Context, Environment, Program, Value}; use eyre::{Result, eyre}; use indexmap::IndexSet; @@ -61,7 +62,9 @@ pub struct AquaPackage { pub format: String, pub rosetta2: bool, pub windows_arm_emulation: bool, - pub complete_windows_ext: bool, + pub complete_windows_ext: Option, + pub windows_ext: String, + pub append_ext: Option, pub supported_envs: Vec, pub files: Vec, pub vars: Vec, @@ -352,7 +355,9 @@ impl Default for AquaPackage { format: String::new(), rosetta2: false, windows_arm_emulation: false, - complete_windows_ext: true, + complete_windows_ext: None, + windows_ext: String::new(), + append_ext: None, supported_envs: Vec::new(), files: Vec::new(), vars: Vec::new(), @@ -476,6 +481,79 @@ impl AquaPackage { "raw" } + fn append_ext_enabled(&self) -> bool { + self.append_ext.unwrap_or(true) + } + + pub fn windows_ext(&self) -> &str { + if self.windows_ext.is_empty() { + match self.r#type { + AquaPackageType::GithubArchive | AquaPackageType::GithubContent => ".sh", + _ => ".exe", + } + } else { + &self.windows_ext + } + } + + pub fn complete_windows_ext_enabled(&self) -> bool { + match self.complete_windows_ext { + Some(complete) => complete, + None => !matches!( + self.r#type, + AquaPackageType::GithubArchive | AquaPackageType::GithubContent + ), + } + } + + fn complete_windows_ext(&self, s: &str) -> String { + if self.complete_windows_ext_enabled() { + append_str_ext(s, self.windows_ext()) + } else { + s.to_string() + } + } + + fn os_file_ext_is_empty(&self, s: &str, version: &str) -> bool { + let filename = s.rsplit('/').next().unwrap_or_default(); + file_ext_is_empty(filename, version) + } + + fn os_file_ext(&self, s: &str, version: &str) -> Option { + let filename = s.rsplit('/').next().unwrap_or_default(); + file_ext(filename, version) + } + + fn append_ext(&self, s: String) -> String { + if !self.append_ext_enabled() || self.format.is_empty() || self.format == "raw" { + return s; + } + if self.detect_format(&s) != "raw" || s.ends_with(&format!(".{}", self.format)) { + return s; + } + format!("{}.{}", s, self.format) + } + + fn asset_without_appended_ext( + &self, + v: &str, + overrides: &HashMap, + os: &str, + arch: &str, + ) -> Result { + if self.asset.is_empty() && self.url.split('/').count() > "//".len() { + let asset = self.url.rsplit('/').next().unwrap_or(""); + self.parse_aqua_str(asset, v, overrides, os, arch) + } else { + self.parse_aqua_str(&self.asset, v, overrides, os, arch) + } + } + + fn finish_asset(&self, asset: String, v: &str, os: &str) -> Result { + let asset = self.append_ext(asset); + self.complete_windows_ext_to_asset(&asset, v, os) + } + /// Get the format for this package and version pub fn format(&self, v: &str, os: &str, arch: &str) -> Result<&str> { if self.r#type == AquaPackageType::GithubArchive { @@ -483,9 +561,9 @@ impl AquaPackage { } let format = if self.format.is_empty() { let asset = if !self.asset.is_empty() { - self.asset(v, os, arch)? + self.asset_without_appended_ext(v, &Default::default(), os, arch)? } else if !self.url.is_empty() { - self.url.to_string() + self.parse_aqua_str(&self.url, v, &Default::default(), os, arch)? } else { log::debug!("no asset or url for {}/{}", self.repo_owner, self.repo_name); String::new() @@ -504,58 +582,91 @@ impl AquaPackage { /// Get the asset name for this package and version pub fn asset(&self, v: &str, os: &str, arch: &str) -> Result { - if self.asset.is_empty() && self.url.split("/").count() > "//".len() { - let asset = self.url.rsplit("/").next().unwrap_or(""); - self.parse_aqua_str(asset, v, &Default::default(), os, arch) - } else { - self.parse_aqua_str(&self.asset, v, &Default::default(), os, arch) - } + let asset = self.asset_without_appended_ext(v, &Default::default(), os, arch)?; + self.finish_asset(asset, v, os) } /// Get all possible asset strings for this package, version and platform pub fn asset_strs(&self, v: &str, os: &str, arch: &str) -> Result> { - let mut strs = - IndexSet::from([self.parse_aqua_str(&self.asset, v, &Default::default(), os, arch)?]); + let mut strs = IndexSet::new(); + let asset = self.asset_without_appended_ext(v, &Default::default(), os, arch)?; + strs.insert(self.finish_asset(asset.clone(), v, os)?); + strs.insert(asset); if os == "darwin" { let mut ctx = HashMap::default(); ctx.insert("Arch".to_string(), "universal".to_string()); - strs.insert(self.parse_aqua_str(&self.asset, v, &ctx, os, arch)?); + let asset = self.asset_without_appended_ext(v, &ctx, os, arch)?; + strs.insert(self.finish_asset(asset.clone(), v, os)?); + strs.insert(asset); } else if os == "windows" { let mut ctx = HashMap::default(); - let asset = self.parse_aqua_str(&self.asset, v, &ctx, os, arch)?; - strs.insert(self.complete_windows_ext_to_asset(&asset, v, os, arch)?); if arch == "arm64" { ctx.insert("Arch".to_string(), "amd64".to_string()); - strs.insert(self.parse_aqua_str(&self.asset, v, &ctx, os, arch)?); - let asset = self.parse_aqua_str(&self.asset, v, &ctx, os, arch)?; - strs.insert(self.complete_windows_ext_to_asset(&asset, v, os, arch)?); + let asset = self.asset_without_appended_ext(v, &ctx, os, arch)?; + strs.insert(self.finish_asset(asset.clone(), v, os)?); + strs.insert(asset); } } Ok(strs) } - /// Apply Windows .exe extension to an asset or URL string if appropriate. + /// Apply Windows executable extension to an asset or URL string if appropriate. /// Mirrors upstream aqua's `completeWindowsExtToAsset` decision tree. - fn complete_windows_ext_to_asset( + fn complete_windows_ext_to_asset(&self, s: &str, v: &str, os: &str) -> Result { + if os != "windows" || s.ends_with(".exe") || s.ends_with(".jar") { + return Ok(s.to_string()); + } + if self.format == "raw" { + return Ok(self.complete_windows_ext(s)); + } + if !self.format.is_empty() { + return Ok(s.to_string()); + } + if self.os_file_ext_is_empty(s, v) { + return Ok(self.complete_windows_ext(s)); + } + Ok(s.to_string()) + } + + /// Apply Windows executable completion to install file source paths. + /// Mirrors upstream aqua's `completeWindowsExtToFileSrc`. + pub fn complete_windows_ext_to_file_src(&self, src: &str, v: &str, os: &str) -> String { + if os != "windows" || !self.complete_windows_ext_enabled() { + return src.to_string(); + } + if self.os_file_ext_is_empty(src, v) { + self.complete_windows_ext(src) + } else { + src.to_string() + } + } + + /// Apply Windows executable completion to link destinations, preserving an + /// explicit source extension when the destination omits one. + pub fn complete_windows_ext_to_file_dst( &self, - s: &str, + src: &str, + dst: &str, v: &str, os: &str, - arch: &str, - ) -> Result { - if os != "windows" || s.ends_with(".exe") { - return Ok(s.to_string()); + ) -> String { + if os != "windows" + || !self.complete_windows_ext_enabled() + || !self.os_file_ext_is_empty(dst, v) + { + return dst.to_string(); } - if self.complete_windows_ext && self.format(v, os, arch)? == "raw" { - return Ok(format!("{s}.exe")); + match self.os_file_ext(src, v) { + Some(ext) => append_str_ext(dst, &ext), + None => self.complete_windows_ext(dst), } - Ok(s.to_string()) } /// Get the URL for this package and version pub fn url(&self, v: &str, os: &str, arch: &str) -> Result { let url = self.parse_aqua_str(&self.url, v, &Default::default(), os, arch)?; - self.complete_windows_ext_to_asset(&url, v, os, arch) + let url = self.append_ext(url); + self.complete_windows_ext_to_asset(&url, v, os) } /// Parse an Aqua template string with variable substitution and platform info @@ -842,8 +953,14 @@ fn apply_override(mut orig: AquaPackage, avo: &AquaPackage) -> AquaPackage { if avo.windows_arm_emulation { orig.windows_arm_emulation = true; } - if !avo.complete_windows_ext { - orig.complete_windows_ext = false; + if avo.complete_windows_ext.is_some() { + orig.complete_windows_ext = avo.complete_windows_ext; + } + if !avo.windows_ext.is_empty() { + orig.windows_ext = avo.windows_ext.clone(); + } + if avo.append_ext.is_some() { + orig.append_ext = avo.append_ext; } if !avo.supported_envs.is_empty() { orig.supported_envs = avo.supported_envs.clone(); @@ -1340,7 +1457,7 @@ packages: let pkg = AquaPackage { url: "https://example.com/tool/{{.Version}}/tool.exe".to_string(), format: "raw".to_string(), - complete_windows_ext: true, + complete_windows_ext: Some(true), ..Default::default() }; @@ -1359,7 +1476,7 @@ packages: let pkg = AquaPackage { url: "https://example.com/tool/{{.Version}}/tool".to_string(), format: "raw".to_string(), - complete_windows_ext: true, + complete_windows_ext: Some(true), ..Default::default() }; @@ -1370,13 +1487,204 @@ packages: ); } + #[test] + fn test_url_preserves_jar_when_completing_windows_ext() { + let pkg = AquaPackage { + url: "https://example.com/tool/{{.Version}}/tool.jar".to_string(), + format: "raw".to_string(), + complete_windows_ext: Some(true), + ..Default::default() + }; + + let url = pkg.url("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(url, "https://example.com/tool/1.0.0/tool.jar"); + } + + #[test] + fn test_asset_appends_format_ext_by_default() { + let pkg = AquaPackage { + asset: "tool-{{.Version}}-{{.OS}}-{{.Arch}}".to_string(), + format: "tar.gz".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "linux", "amd64").unwrap(); + + assert_eq!(asset, "tool-1.0.0-linux-amd64.tar.gz"); + } + + #[test] + fn test_asset_respects_append_ext_false() { + let pkg = AquaPackage { + asset: "tool-{{.Version}}-{{.OS}}-{{.Arch}}".to_string(), + format: "tar.gz".to_string(), + append_ext: Some(false), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "linux", "amd64").unwrap(); + + assert_eq!(asset, "tool-1.0.0-linux-amd64"); + } + + #[test] + fn test_asset_uses_custom_windows_ext() { + let pkg = AquaPackage { + asset: "tool-{{.Version}}-{{.OS}}-{{.Arch}}".to_string(), + format: "raw".to_string(), + windows_ext: ".bat".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(asset, "tool-1.0.0-windows-amd64.bat"); + } + + #[test] + fn test_asset_normalizes_custom_windows_ext() { + let pkg = AquaPackage { + asset: "tool-{{.Version}}-{{.OS}}-{{.Arch}}".to_string(), + format: "raw".to_string(), + windows_ext: "bat".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(asset, "tool-1.0.0-windows-amd64.bat"); + } + + #[test] + fn test_asset_omitted_format_preserves_existing_extension() { + let pkg = AquaPackage { + asset: "tool-{{.Version}}.ps1".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(asset, "tool-1.0.0.ps1"); + } + + #[test] + fn test_asset_completion_strips_version_from_filename_only() { + let pkg = AquaPackage { + asset: "https://example.com/1.0.0/tool-{{.Version}}".to_string(), + format: "raw".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(asset, "https://example.com/1.0.0/tool-1.0.0.exe"); + } + + #[test] + fn test_asset_completion_preserves_prefix_before_non_boundary_version() { + let pkg = AquaPackage { + asset: "x1.8atool_{{.Version}}_win".to_string(), + format: "raw".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.8", "windows", "amd64").unwrap(); + + assert_eq!(asset, "x1.8atool_1.8_win.exe"); + } + + #[test] + fn test_asset_completion_treats_version_dot_as_empty_extension() { + let pkg = AquaPackage { + asset: "tool.{{.Version}}".to_string(), + format: "raw".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(asset, "tool.1.0.0.exe"); + } + + #[test] + fn test_asset_completion_does_not_corrupt_version_prefixes() { + let pkg = AquaPackage { + asset: "tool-1.1.1".to_string(), + format: "raw".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.1", "windows", "amd64").unwrap(); + + assert_eq!(asset, "tool-1.1.1.exe"); + } + + #[test] + fn test_github_content_does_not_complete_windows_ext_by_default() { + let pkg = AquaPackage { + r#type: AquaPackageType::GithubContent, + path: Some("install".to_string()), + asset: "install".to_string(), + format: "raw".to_string(), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(asset, "install"); + } + + #[test] + fn test_github_content_complete_windows_ext_defaults_to_sh() { + let pkg = AquaPackage { + r#type: AquaPackageType::GithubContent, + path: Some("install".to_string()), + asset: "install".to_string(), + format: "raw".to_string(), + complete_windows_ext: Some(true), + ..Default::default() + }; + + let asset = pkg.asset("1.0.0", "windows", "amd64").unwrap(); + + assert_eq!(asset, "install.sh"); + } + + #[test] + fn test_file_src_and_dst_complete_windows_ext() { + let pkg = AquaPackage::default(); + + assert_eq!( + pkg.complete_windows_ext_to_file_src("tool_1.0.0", "v1.0.0", "windows"), + "tool_1.0.0.exe" + ); + assert_eq!( + pkg.complete_windows_ext_to_file_src("tool_1.0.0.bat", "v1.0.0", "windows"), + "tool_1.0.0.bat" + ); + assert_eq!( + pkg.complete_windows_ext_to_file_dst( + "tool_1.0.0.bat", + "tool_1.0.0", + "v1.0.0", + "windows" + ), + "tool_1.0.0.bat" + ); + assert_eq!( + pkg.complete_windows_ext_to_file_dst("tool_1.0.0", "tool_1.0.0", "v1.0.0", "windows"), + "tool_1.0.0.exe" + ); + } + #[test] fn test_asset_strs_no_double_exe_extension() { // asset_strs should also not double .exe when asset already ends in .exe. let pkg = AquaPackage { asset: "tool.exe".to_string(), format: "raw".to_string(), - complete_windows_ext: true, + complete_windows_ext: Some(true), ..Default::default() }; diff --git a/src/backend/aqua.rs b/src/backend/aqua.rs index bb0abb37a1..f9151daffe 100644 --- a/src/backend/aqua.rs +++ b/src/backend/aqua.rs @@ -2275,7 +2275,7 @@ impl AquaBackend { let name_str: &str = name.as_ref(); install_path.join(name_str) }) - .map(|path| complete_windows_ext(path, pkg.complete_windows_ext, os())) + .map(|path| complete_windows_ext(path, pkg, os(), v)) .collect(); let first_bin_path = bin_paths .first() @@ -2388,7 +2388,7 @@ impl AquaBackend { .unwrap_or(&pkg.repo_name); let mut path = install_path.join(fallback_name); - path = complete_windows_ext(path, pkg.complete_windows_ext, os); + path = complete_windows_ext(path, pkg, os, version); return Ok(vec![AquaFileLink { src: path.clone(), @@ -2447,8 +2447,8 @@ impl AquaBackend { .parent() .wrap_err_with(|| format!("file source has no parent: {}", src.display()))? .join(link.as_deref().unwrap_or(f.name.as_str())); - src = complete_windows_ext(src, pkg.complete_windows_ext, os); - dst = complete_windows_dst_ext(&src, dst, pkg.complete_windows_ext, os); + src = complete_windows_ext(src, pkg, os, version); + dst = complete_windows_dst_ext(&src, dst, pkg, os, version); Ok(Some(AquaFileLink { src, @@ -2649,22 +2649,50 @@ fn ends_with_v(s: &str) -> bool { s.ends_with('v') || s.ends_with('V') } -fn complete_windows_ext(path: PathBuf, complete: bool, target_os: &str) -> PathBuf { - if target_os == "windows" && complete && path.extension().is_none() { - path.with_extension("exe") - } else { - path +fn complete_windows_ext( + mut path: PathBuf, + pkg: &AquaPackage, + target_os: &str, + version: &str, +) -> PathBuf { + let Some(file_name) = path + .file_name() + .map(|file_name| file_name.to_string_lossy().into_owned()) + else { + return path; + }; + let completed = pkg.complete_windows_ext_to_file_src(&file_name, version, target_os); + if completed != file_name { + path.set_file_name(completed); } + path } -fn complete_windows_dst_ext(src: &Path, dst: PathBuf, complete: bool, target_os: &str) -> PathBuf { - if target_os != "windows" || !complete || dst.extension().is_some() { +fn complete_windows_dst_ext( + src: &Path, + mut dst: PathBuf, + pkg: &AquaPackage, + target_os: &str, + version: &str, +) -> PathBuf { + let Some(src_file_name) = src + .file_name() + .map(|file_name| file_name.to_string_lossy().into_owned()) + else { return dst; + }; + let Some(dst_file_name) = dst + .file_name() + .map(|file_name| file_name.to_string_lossy().into_owned()) + else { + return dst; + }; + let completed = + pkg.complete_windows_ext_to_file_dst(&src_file_name, &dst_file_name, version, target_os); + if completed != dst_file_name { + dst.set_file_name(completed); } - match src.extension() { - Some(ext) => dst.with_extension(ext), - None => dst.with_extension("exe"), - } + dst } /// Returns install-time-only option keys for the Aqua backend. @@ -2735,24 +2763,75 @@ mod tests { #[test] fn test_complete_windows_ext_preserves_existing_extension() { + let pkg = AquaPackage::default(); assert_eq!( - complete_windows_ext(PathBuf::from("bat/arq.bat"), true, "windows"), + complete_windows_ext(PathBuf::from("bat/arq.bat"), &pkg, "windows", "1.0.0"), PathBuf::from("bat/arq.bat") ); assert_eq!( - complete_windows_ext(PathBuf::from("bin/tool"), true, "windows"), + complete_windows_ext(PathBuf::from("lib/tool.jar"), &pkg, "windows", "1.0.0"), + PathBuf::from("lib/tool.jar") + ); + assert_eq!( + complete_windows_ext(PathBuf::from("bin/tool"), &pkg, "windows", "1.0.0"), PathBuf::from("bin/tool.exe") ); + assert_eq!( + complete_windows_ext(PathBuf::from("bin/tool_1.0.0"), &pkg, "windows", "v1.0.0"), + PathBuf::from("bin/tool_1.0.0.exe") + ); + assert_eq!( + complete_windows_ext(PathBuf::from("bin/tool.1.0.0"), &pkg, "windows", "v1.0.0"), + PathBuf::from("bin/tool.1.0.0.exe") + ); + assert_eq!( + complete_windows_ext( + PathBuf::from("bin/x1.8atool_1.8_win"), + &pkg, + "windows", + "1.8" + ), + PathBuf::from("bin/x1.8atool_1.8_win.exe") + ); + assert_eq!( + complete_windows_ext(PathBuf::from("bin/tool-1.1.1"), &pkg, "windows", "1.1"), + PathBuf::from("bin/tool-1.1.1.exe") + ); + } + + #[test] + fn test_complete_windows_ext_uses_custom_windows_ext() { + let mut pkg = AquaPackage::default(); + pkg.windows_ext = ".bat".to_string(); + + assert_eq!( + complete_windows_ext(PathBuf::from("dart-sass/sass"), &pkg, "windows", "1.0.0"), + PathBuf::from("dart-sass/sass.bat") + ); + } + + #[test] + fn test_complete_windows_ext_can_default_to_sh() { + let mut pkg = AquaPackage::default(); + pkg.r#type = AquaPackageType::GithubContent; + pkg.complete_windows_ext = Some(true); + + assert_eq!( + complete_windows_ext(PathBuf::from("install"), &pkg, "windows", "1.0.0"), + PathBuf::from("install.sh") + ); } #[test] fn test_complete_windows_dst_ext_uses_source_extension() { + let pkg = AquaPackage::default(); assert_eq!( complete_windows_dst_ext( Path::new("bat/arq.bat"), PathBuf::from("bat/arq"), - true, + &pkg, "windows", + "1.0.0", ), PathBuf::from("bat/arq.bat") ); @@ -2760,11 +2839,32 @@ mod tests { complete_windows_dst_ext( Path::new("bin/tool"), PathBuf::from("bin/tool"), - true, - "windows" + &pkg, + "windows", + "1.0.0" ), PathBuf::from("bin/tool.exe") ); + assert_eq!( + complete_windows_dst_ext( + Path::new("bin/tool_1.0.0.bat"), + PathBuf::from("bin/tool_1.0.0"), + &pkg, + "windows", + "v1.0.0" + ), + PathBuf::from("bin/tool_1.0.0.bat") + ); + assert_eq!( + complete_windows_dst_ext( + Path::new("bin/tool_1.0.0"), + PathBuf::from("bin/tool_1.0.0"), + &pkg, + "windows", + "v1.0.0" + ), + PathBuf::from("bin/tool_1.0.0.exe") + ); } #[test] @@ -2920,7 +3020,7 @@ mod tests { link: Some("mc.exe".to_string()), ..Default::default() }]; - pkg.complete_windows_ext = false; + pkg.complete_windows_ext = Some(false); let links = AquaBackend::srcs_for_platform( &pkg, @@ -2942,6 +3042,31 @@ mod tests { ); } + #[test] + fn test_srcs_support_custom_windows_ext() { + let mut pkg = AquaPackage::default(); + pkg.windows_ext = ".bat".to_string(); + pkg.files = vec![AquaFile { + name: "sass".to_string(), + src: Some("dart-sass/sass".to_string()), + ..Default::default() + }]; + + let links = + AquaBackend::srcs_for_platform(&pkg, "1.0.0", Path::new("install"), "windows", "amd64") + .unwrap(); + + assert_eq!( + links, + vec![AquaFileLink { + src: PathBuf::from("install").join("dart-sass/sass.bat"), + dst: PathBuf::from("install").join("dart-sass/sass.bat"), + hard: false, + explicit_link: false, + }] + ); + } + #[test] fn test_srcs_support_hard_file_link() { let mut pkg = AquaPackage::default();