use crate::downloader::events::{DownloaderError, UnsupportedReason};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CandidateUrl {
pub url: String,
pub archive_fmt: ArchiveFmt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveFmt {
TarGz,
Zip,
Bin,
}
const FILENAME_TEMPLATES: &[&str] = &[
"{name}-{target}-v{version}.{ext}",
"{name}-{target}-{version}.{ext}",
"{name}-{version}-{target}.{ext}",
"{name}-v{version}-{target}.{ext}",
"{name}_{target}_v{version}.{ext}",
"{name}_{target}_{version}.{ext}",
"{name}_{version}_{target}.{ext}",
"{name}_v{version}_{target}.{ext}",
"{name}-{target}.{ext}",
"{name}_{target}.{ext}",
];
const ARCHIVE_EXTS: &[(ArchiveFmt, &str)] = &[(ArchiveFmt::TarGz, "tar.gz"), (ArchiveFmt::Zip, "zip")];
pub fn candidate_urls(
name_candidates: &[String],
version: &str,
repo_url: &str,
targets: &[String],
) -> Result<Vec<CandidateUrl>, DownloaderError> {
let repo = repo_url.trim_end_matches('/');
if !repo.starts_with("https://github.com/") && !repo.starts_with("http://github.com/") {
return Err(DownloaderError::Unsupported(
UnsupportedReason::NoMetadataAndNoConvention,
));
}
if targets.is_empty() || name_candidates.is_empty() {
return Err(DownloaderError::Unsupported(
UnsupportedReason::NoMetadataAndNoConvention,
));
}
let pkg = &name_candidates[0];
let tag_paths: Vec<String> = vec![
format!("v{version}"),
format!("{version}"),
format!("{pkg}-v{version}"),
format!("{pkg}-{version}"),
format!("{pkg}/v{version}"),
format!("{pkg}/{version}"),
];
let mut out = Vec::with_capacity(40 * targets.len() * name_candidates.len() * tag_paths.len() / 2);
let mut seen = std::collections::HashSet::new();
for name in name_candidates {
for target in targets {
for tag_path in &tag_paths {
let base = format!("{repo}/releases/download/{tag_path}");
for tmpl in FILENAME_TEMPLATES {
for (fmt, ext) in ARCHIVE_EXTS {
let filename = tmpl
.replace("{name}", name)
.replace("{version}", version)
.replace("{target}", target)
.replace("{ext}", ext);
let url = format!("{base}/{filename}");
if seen.insert(url.clone()) {
out.push(CandidateUrl { url, archive_fmt: *fmt });
}
}
}
}
}
}
Ok(out)
}
pub fn current_targets() -> Vec<String> {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
match (arch, os) {
("aarch64", "macos") => vec![
"aarch64-apple-darwin".into(),
"arm64-apple-darwin".into(),
"darwin-arm64".into(),
],
("x86_64", "macos") => vec![
"x86_64-apple-darwin".into(),
"x64-apple-darwin".into(),
"darwin-amd64".into(),
"darwin-x64".into(),
],
("aarch64", "linux") => vec![
"aarch64-unknown-linux-gnu".into(),
"aarch64-unknown-linux-musl".into(),
"arm64-unknown-linux-gnu".into(),
"linux-arm64".into(),
],
("x86_64", "linux") => vec![
"x86_64-unknown-linux-gnu".into(),
"x86_64-unknown-linux-musl".into(),
"linux-amd64".into(),
"linux-x64".into(),
],
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn one(target: &str) -> Vec<String> {
vec![target.into()]
}
#[test]
fn non_github_repo_is_unsupported() {
let err = candidate_urls(
&["ripgrep".into()],
"14.1.2",
"https://gitlab.com/x/y",
&one("x86_64-apple-darwin"),
)
.unwrap_err();
assert!(matches!(
err,
DownloaderError::Unsupported(UnsupportedReason::NoMetadataAndNoConvention)
));
}
#[test]
fn empty_targets_is_unsupported() {
let err = candidate_urls(
&["ripgrep".into()],
"14.1.2",
"https://github.com/BurntSushi/ripgrep",
&[],
)
.unwrap_err();
assert!(matches!(
err,
DownloaderError::Unsupported(UnsupportedReason::NoMetadataAndNoConvention)
));
}
#[test]
fn single_target_yields_expected_candidate_count() {
let cands = candidate_urls(
&["ripgrep".into()],
"14.1.2",
"https://github.com/BurntSushi/ripgrep",
&one("x86_64-apple-darwin"),
)
.unwrap();
assert_eq!(cands.len(), 120);
}
#[test]
fn multiple_targets_yield_proportional_candidates() {
let cands = candidate_urls(
&["ripgrep".into()],
"14.1.2",
"https://github.com/BurntSushi/ripgrep",
&[
"aarch64-apple-darwin".into(),
"arm64-apple-darwin".into(),
"darwin-arm64".into(),
],
)
.unwrap();
assert_eq!(cands.len(), 360);
}
#[test]
fn includes_tauri_style_subcrate_prefix_tag() {
let cands = candidate_urls(
&["tauri-cli".into(), "cargo-tauri".into()],
"2.11.2",
"https://github.com/tauri-apps/tauri",
&one("aarch64-apple-darwin"),
)
.unwrap();
let urls: Vec<&str> = cands.iter().map(|c| c.url.as_str()).collect();
assert!(urls.iter().any(|u| u == &"https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.11.2/cargo-tauri-aarch64-apple-darwin.zip"));
}
#[test]
fn includes_ripgrep_style_v_prefix_name_version_target() {
let cands = candidate_urls(
&["ripgrep".into()],
"14.1.2",
"https://github.com/BurntSushi/ripgrep",
&one("x86_64-apple-darwin"),
)
.unwrap();
let urls: Vec<&str> = cands.iter().map(|c| c.url.as_str()).collect();
assert!(urls.iter().any(|u| u == &"https://github.com/BurntSushi/ripgrep/releases/download/v14.1.2/ripgrep-14.1.2-x86_64-apple-darwin.tar.gz"));
}
#[test]
fn includes_mdbook_style_v_in_filename() {
let cands = candidate_urls(
&["mdbook".into()],
"0.5.3",
"https://github.com/rust-lang/mdBook",
&one("x86_64-apple-darwin"),
)
.unwrap();
let urls: Vec<&str> = cands.iter().map(|c| c.url.as_str()).collect();
assert!(urls.iter().any(|u| u.contains("mdbook-v0.5.3-x86_64-apple-darwin.tar.gz")));
}
#[test]
fn includes_cargo_deny_style_bare_version_tag() {
let cands = candidate_urls(
&["cargo-deny".into()],
"0.19.7",
"https://github.com/EmbarkStudios/cargo-deny",
&one("x86_64-apple-darwin"),
)
.unwrap();
let urls: Vec<&str> = cands.iter().map(|c| c.url.as_str()).collect();
assert!(urls.iter().any(|u| u == &"https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.7/cargo-deny-0.19.7-x86_64-apple-darwin.tar.gz"));
}
#[test]
fn includes_underscore_separated_variant() {
let cands = candidate_urls(
&["somepkg".into()],
"1.0.0",
"https://github.com/x/y",
&one("x86_64-apple-darwin"),
)
.unwrap();
let urls: Vec<&str> = cands.iter().map(|c| c.url.as_str()).collect();
assert!(urls.iter().any(|u| u.contains("somepkg_x86_64-apple-darwin_1.0.0")));
}
#[test]
fn trailing_slash_in_repo_url_is_tolerated() {
let a = candidate_urls(
&["ripgrep".into()],
"14.1.2",
"https://github.com/BurntSushi/ripgrep/",
&one("x86_64-apple-darwin"),
)
.unwrap();
let b = candidate_urls(
&["ripgrep".into()],
"14.1.2",
"https://github.com/BurntSushi/ripgrep",
&one("x86_64-apple-darwin"),
)
.unwrap();
assert_eq!(a, b);
}
#[test]
fn current_targets_returns_nonempty_on_supported_unix() {
if std::env::consts::OS == "macos" || std::env::consts::OS == "linux" {
let ts = current_targets();
assert!(!ts.is_empty());
assert!(ts[0].contains('-'));
}
}
}