use std::sync::LazyLock;
use std::time::Duration;
use regex::Regex;
use crate::adapter::overlay::{Platform, ProbeOutcome};
use crate::models::ToolVariant;
pub const VERSION_PROBE_ARGS: &[&str] = &["--version"];
use super::OPTION_REJECTION_MARKERS;
const BSD_BANNER_TOKEN_PREFIXES: &[&str] = &["bsd", "freebsd", "openbsd", "netbsd"];
const BSD_FAMILY_PLATFORMS: &[&str] = &["macos", "freebsd", "openbsd", "netbsd", "dragonfly"];
const GNU_BOILERPLATE_WORDS: &[&str] = &[
"general", "public", "license", "licenses", "gpl", "lgpl", "agpl", "lesser", "affero", "org",
"project",
];
static APPLE_TARGET_TRIPLE_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[a-z0-9_.]+-apple-[a-z0-9_.]+").expect("valid static regex"));
pub fn current_platform() -> Platform {
Platform::new(std::env::consts::OS)
}
pub fn run_probes(
binary_path: &str,
arg_sets: &[Vec<String>],
timeout: Duration,
) -> Vec<ProbeOutcome> {
arg_sets
.iter()
.map(|args| run_probe(binary_path, args, timeout))
.collect()
}
fn run_probe(binary_path: &str, args: &[String], timeout: Duration) -> ProbeOutcome {
let borrowed: Vec<&str> = args.iter().map(String::as_str).collect();
match super::exec::run_with_timeout(binary_path, &borrowed, timeout) {
Ok(output) => {
let mut combined = String::from_utf8_lossy(&output.stdout).into_owned();
combined.push_str(&String::from_utf8_lossy(&output.stderr));
ProbeOutcome {
args: args.to_vec(),
succeeded: output.status.success(),
output: combined,
}
}
Err(_) => ProbeOutcome {
args: args.to_vec(),
succeeded: false,
output: String::new(),
},
}
}
pub fn classify_variant(
outcome: Option<&ProbeOutcome>,
platform: Option<&Platform>,
) -> ToolVariant {
let Some(outcome) = outcome else {
return ToolVariant::Unknown;
};
let output = outcome.output.to_lowercase();
if output.contains("busybox") {
return ToolVariant::Busybox;
}
if outcome.succeeded && mentions_bsd(&output) {
return ToolVariant::Bsd;
}
if outcome.succeeded && mentions_gnu_package(&output) {
return ToolVariant::Gnu;
}
if outcome.succeeded && mentions_apple(&output) {
return ToolVariant::Apple;
}
let rejected = !outcome.succeeded
&& OPTION_REJECTION_MARKERS
.iter()
.any(|marker| output.contains(marker));
if rejected && is_bsd_family_platform(platform) {
return ToolVariant::Bsd;
}
ToolVariant::Unknown
}
fn is_bsd_family_platform(platform: Option<&Platform>) -> bool {
platform.is_some_and(|current| BSD_FAMILY_PLATFORMS.contains(¤t.as_str()))
}
fn tokens(output: &str) -> impl Iterator<Item = &str> {
output
.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|token| !token.is_empty())
}
fn mentions_bsd(output: &str) -> bool {
tokens(output).any(|token| {
BSD_BANNER_TOKEN_PREFIXES
.iter()
.any(|marker| token.starts_with(marker))
})
}
fn mentions_gnu_package(output: &str) -> bool {
let mut tokens = tokens(output);
while let Some(token) = tokens.next() {
if token != "gnu" {
continue;
}
match tokens.next() {
Some(next) if !GNU_BOILERPLATE_WORDS.contains(&next) => return true,
_ => continue,
}
}
false
}
fn mentions_apple(output: &str) -> bool {
APPLE_TARGET_TRIPLE_RE
.replace_all(output, " ")
.split(|c: char| !c.is_ascii_alphanumeric())
.any(|token| token == "apple")
}
pub fn version_outcome(outcomes: &[ProbeOutcome]) -> Option<&ProbeOutcome> {
outcomes.iter().find(|outcome| {
outcome.args.len() == VERSION_PROBE_ARGS.len()
&& outcome
.args
.iter()
.zip(VERSION_PROBE_ARGS)
.all(|(actual, expected)| actual == expected)
})
}
pub fn version_probe_args() -> Vec<String> {
VERSION_PROBE_ARGS
.iter()
.map(|a| (*a).to_string())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn outcome(succeeded: bool, output: &str) -> ProbeOutcome {
ProbeOutcome {
args: version_probe_args(),
succeeded,
output: output.to_string(),
}
}
#[test]
fn test_classify_variant_matches_real_banners() {
let cases: &[(&str, ToolVariant)] = &[
("2.3-Apple (197)", ToolVariant::Apple),
("git version 2.50.1 (Apple Git-155)", ToolVariant::Apple),
("Apple diff (based on FreeBSD diff)", ToolVariant::Bsd),
(
"grep (BSD grep, GNU compatible) 2.6.0-FreeBSD",
ToolVariant::Bsd,
),
(
"bsdtar 3.5.3 - libarchive 3.7.4 zlib/1.2.12",
ToolVariant::Bsd,
),
("ls (GNU coreutils) 9.7", ToolVariant::Gnu),
("diff (GNU diffutils) 3.10", ToolVariant::Gnu),
("tar (GNU tar) 1.35", ToolVariant::Gnu),
(
"GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)",
ToolVariant::Gnu,
),
(
"curl 8.7.1 (x86_64-apple-darwin25.0) libcurl/8.7.1",
ToolVariant::Unknown,
),
(
"BusyBox v1.36.1 (2023-11-07) multi-call binary",
ToolVariant::Busybox,
),
("Python 3.12.10", ToolVariant::Unknown),
("OpenSSL 3.5.4 30 Sep 2025", ToolVariant::Unknown),
];
for (banner, expected) in cases {
let probe = outcome(true, banner);
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
*expected,
"banner: {banner}"
);
}
}
#[test]
fn test_classify_variant_gnu_coreutils_banner() {
let probe = outcome(true, "ls (GNU coreutils) 9.4\n");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("linux"))),
ToolVariant::Gnu
);
}
#[test]
fn test_classify_variant_gnu_coreutils_on_macos() {
let probe = outcome(true, "ls (GNU coreutils) 9.5\n");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
ToolVariant::Gnu
);
}
#[test]
fn test_classify_variant_gnu_beyond_coreutils() {
for banner in [
"diff (GNU diffutils) 3.10",
"sed (GNU sed) 4.9",
"tar (GNU tar) 1.35",
"GNU Awk 5.3.1, API 4.0",
] {
let probe = outcome(true, banner);
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("linux"))),
ToolVariant::Gnu,
"banner: {banner}"
);
}
}
#[test]
fn test_classify_variant_gpl_boilerplate_is_not_a_gnu_banner() {
let probe = outcome(
true,
"widget 1.2.3\nLicense GPLv3+: GNU GPL version 3 or later \
<https://gnu.org/licenses/gpl.html>",
);
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("linux"))),
ToolVariant::Unknown
);
}
#[test]
fn test_classify_variant_apple_port_from_banner() {
for banner in ["2.3-Apple (197)", "git version 2.50.1 (Apple Git-155)"] {
let probe = outcome(true, banner);
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
ToolVariant::Apple,
"banner: {banner}"
);
}
}
#[test]
fn test_classify_variant_apple_target_triple_is_not_an_apple_port() {
let curl = outcome(true, "curl 8.7.1 (x86_64-apple-darwin25.0) libcurl/8.7.1");
assert_eq!(
classify_variant(Some(&curl), Some(&Platform::new("macos"))),
ToolVariant::Unknown
);
let bash = outcome(
true,
"GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)",
);
assert_eq!(
classify_variant(Some(&bash), Some(&Platform::new("macos"))),
ToolVariant::Gnu
);
}
#[test]
fn test_classify_variant_bsd_marker_outranks_apple_marker() {
let probe = outcome(true, "Apple diff (based on FreeBSD diff)");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
ToolVariant::Bsd
);
}
#[test]
fn test_classify_variant_busybox_banner() {
let probe = outcome(true, "BusyBox v1.36.1 (2023-11-07) multi-call binary.");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("linux"))),
ToolVariant::Busybox
);
}
#[test]
fn test_classify_variant_bsd_needs_platform_corroboration() {
let probe = outcome(
false,
"ls: unrecognized option `--version'\nusage: ls [-@ABC...]",
);
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
ToolVariant::Bsd
);
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("linux"))),
ToolVariant::Unknown,
"rejecting --version is not by itself evidence of BSD"
);
}
#[test]
fn test_classify_variant_bsd_on_openbsd_from_rejected_version() {
let probe = outcome(
false,
"ls: unknown option -- -\nusage: ls [-1AaCcdFfgHhikLlmnopqRrSsTtux]",
);
for platform in ["openbsd", "netbsd", "dragonfly"] {
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new(platform))),
ToolVariant::Bsd,
"{platform} must corroborate a rejected --version"
);
}
}
#[test]
fn test_classify_variant_bsd_platform_match_is_case_insensitive() {
let probe = outcome(false, "ls: illegal option -- -");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("OpenBSD"))),
ToolVariant::Bsd
);
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("MACOS"))),
ToolVariant::Bsd
);
}
#[test]
fn test_classify_variant_illegal_option_wording() {
let probe = outcome(false, "ls: illegal option -- -");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("freebsd"))),
ToolVariant::Bsd
);
}
#[test]
fn test_classify_variant_unknown_without_probe() {
assert_eq!(
classify_variant(None, Some(&Platform::new("macos"))),
ToolVariant::Unknown
);
}
#[test]
fn test_classify_variant_bsd_from_successful_banner() {
let probe = outcome(true, "grep (BSD grep, GNU compatible) 2.6.0-FreeBSD");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
ToolVariant::Bsd
);
}
#[test]
fn test_classify_variant_bsd_banner_outranks_gnu_banner() {
let probe = outcome(true, "grep (BSD grep, GNU compatible) 2.6.0-FreeBSD");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
ToolVariant::Bsd
);
}
#[test]
fn test_mentions_bsd_matches_token_prefixes() {
assert!(mentions_bsd(
"grep (bsd grep, gnu compatible) 2.6.0-freebsd"
));
assert!(mentions_bsd("2.6.0-freebsd"));
assert!(
mentions_bsd("bsdtar 3.5.3 - libarchive 3.7.4"),
"libarchive fuses the marker into the program name"
);
assert!(
!mentions_bsd("tool 1.0 built from /opt/notbsdish/src"),
"an embedded substring must not claim the whole tool"
);
assert!(!mentions_bsd("openssl 3.5.4 30 sep 2025"));
}
#[test]
fn test_mentions_apple_ignores_target_triples() {
assert!(mentions_apple("2.3-apple (197)"));
assert!(mentions_apple("git version 2.50.1 (apple git-155)"));
assert!(!mentions_apple(
"curl 8.7.1 (x86_64-apple-darwin25.0) libcurl/8.7.1"
));
assert!(!mentions_apple(
"gnu bash, version 3.2.57(1)-release (arm64-apple-darwin25)"
));
}
#[test]
fn test_classify_variant_unknown_for_ordinary_tool() {
let probe = outcome(true, "git version 2.43.0");
assert_eq!(
classify_variant(Some(&probe), Some(&Platform::new("macos"))),
ToolVariant::Unknown
);
}
#[test]
fn test_run_probes_records_failure_for_missing_binary() {
let probes = run_probes(
"zzz_no_such_binary_xyz",
&[version_probe_args()],
Duration::from_secs(2),
);
assert_eq!(probes.len(), 1);
assert!(!probes[0].succeeded);
assert!(probes[0].output.is_empty());
}
#[test]
fn test_run_probes_captures_output_of_real_binary() {
let probes = run_probes("echo", &[vec!["hello".to_string()]], Duration::from_secs(5));
assert!(probes[0].succeeded);
assert!(probes[0].output.contains("hello"));
}
#[test]
fn test_version_outcome_finds_the_version_probe() {
let outcomes = vec![
ProbeOutcome {
args: vec!["-V".to_string()],
succeeded: true,
output: "x".to_string(),
},
outcome(false, "nope"),
];
assert!(version_outcome(&outcomes).is_some());
assert_eq!(version_outcome(&outcomes).unwrap().output, "nope");
}
#[test]
fn test_current_platform_reports_rust_os_verbatim() {
assert_eq!(
current_platform().as_str(),
std::env::consts::OS.to_ascii_lowercase()
);
assert!(!current_platform().as_str().is_empty());
}
#[test]
fn test_is_bsd_family_platform_covers_the_named_list() {
for name in ["macos", "freebsd", "openbsd", "netbsd", "dragonfly"] {
assert!(
is_bsd_family_platform(Some(&Platform::new(name))),
"{name} is a BSD-family platform"
);
}
for name in ["linux", "windows", "solaris", "illumos"] {
assert!(
!is_bsd_family_platform(Some(&Platform::new(name))),
"{name} is not a BSD-family platform"
);
}
assert!(
!is_bsd_family_platform(None),
"an unstated platform corroborates nothing"
);
}
}