const MAX_VERSION_BYTES: usize = 128;
pub fn validate(value: &str) -> Result<(), String> {
if value.contains('@') {
parse_exact(value).map(|_| ()).map_err(str::to_owned)
} else if !value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'+'))
{
Ok(())
} else {
Err("require a declared tool name using letters, digits, - _ . +, or node@VERSION/bun@VERSION".to_owned())
}
}
#[must_use]
pub fn check_exact_runtime(
requirement: &str,
node_version: Option<&str>,
bun_version: Option<&str>,
) -> Option<Result<(), String>> {
if !requirement.contains('@') {
return None;
}
Some((|| {
let (runtime, expected) = parse_exact(requirement)
.map_err(|reason| format!("capability_invalid:runtime_requirement:{reason}"))?;
let observed = match runtime {
"node" => node_version.map(|value| {
let value = value.trim();
value.strip_prefix('v').unwrap_or(value)
}),
"bun" => bun_version.map(str::trim),
_ => unreachable!("parse_exact accepts only Node and Bun"),
};
let observed = match observed {
Some(value) if exact_version(value) => value,
Some(_) => "<invalid>",
None => "<unknown>",
};
if observed == expected {
Ok(())
} else {
Err(format!(
"capability_missing:runtime_version:{runtime}:required={expected}:observed={observed}"
))
}
})())
}
fn parse_exact(value: &str) -> Result<(&str, &str), &'static str> {
let (runtime, version) = value
.split_once('@')
.ok_or("exact runtime requirement must contain @")?;
if !matches!(runtime, "node" | "bun") {
return Err("supported exact runtime requirements are node@VERSION and bun@VERSION");
}
if !exact_version(version) {
return Err(
"VERSION must be a full MAJOR.MINOR.PATCH with optional prerelease/build suffix, not a range or alias",
);
}
Ok((runtime, version))
}
fn exact_version(value: &str) -> bool {
if value.is_empty() || value.len() > MAX_VERSION_BYTES || !value.is_ascii() {
return false;
}
let (version, build) = match value.split_once('+') {
Some((version, build)) => (version, Some(build)),
None => (value, None),
};
if build.is_some_and(|build| !identifiers(build, false)) {
return false;
}
let (core, prerelease) = match version.split_once('-') {
Some((core, prerelease)) => (core, Some(prerelease)),
None => (version, None),
};
if prerelease.is_some_and(|prerelease| !identifiers(prerelease, true)) {
return false;
}
let mut components = core.split('.');
(0..3).all(|_| components.next().is_some_and(numeric_component)) && components.next().is_none()
}
fn numeric_component(value: &str) -> bool {
!value.is_empty()
&& (value.len() == 1 || !value.starts_with('0'))
&& value.bytes().all(|byte| byte.is_ascii_digit())
}
fn identifiers(value: &str, reject_numeric_leading_zero: bool) -> bool {
value.split('.').all(|part| {
!part.is_empty()
&& part
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
&& (!reject_numeric_leading_zero
|| !part.bytes().all(|byte| byte.is_ascii_digit())
|| numeric_component(part))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn named_tools_keep_their_original_grammar_and_gate() {
for name in ["clang", "g++", "clang-20", "my_tool.1", "node", "bun"] {
validate(name).unwrap();
assert_eq!(
check_exact_runtime(name, Some("v26.7.0"), Some("1.3.0")),
None
);
}
for name in ["", "clang -v", "tool/path", "x=y", "x\n", "naïve"] {
assert!(validate(name).is_err(), "{name:?}");
}
}
#[test]
fn exact_node_and_bun_versions_match_only_their_own_capabilities() {
for node in ["v26.7.0", "26.7.0", "v26.7.0\n", " v26.7.0\r\n"] {
assert_eq!(
check_exact_runtime("node@26.7.0", Some(node), None),
Some(Ok(()))
);
}
assert_eq!(
check_exact_runtime("bun@1.3.0", None, Some("1.3.0\n")),
Some(Ok(()))
);
assert!(
check_exact_runtime("node@26.7.0", None, Some("26.7.0"))
.unwrap()
.is_err()
);
assert!(
check_exact_runtime("bun@1.3.0", Some("v1.3.0"), None)
.unwrap()
.is_err()
);
assert!(
check_exact_runtime("bun@1.3.0", None, Some("v1.3.0"))
.unwrap()
.is_err()
);
}
#[test]
fn full_versions_are_not_prefixes_or_ranges() {
for version in [
"26",
"26.7",
"26.7.0.1",
"26.7.00",
"026.7.0",
"26.07.0",
"v26.7.0",
"^26.7.0",
"~26.7.0",
">=26.7.0",
"26.*",
"*",
"latest",
"lts",
" 26.7.0",
"26.7.0 ",
"26.7.0\n",
"26.7.0 || 22.0.0",
"26.7.0@other",
"26.7.0-",
"26.7.0+",
"26.7.0-rc..1",
"26.7.0-01",
"26.7.0+one+two",
"26.7.0+one..two",
"26.7.0",
] {
let requirement = format!("node@{version}");
assert!(validate(&requirement).is_err(), "{requirement:?}");
assert!(
check_exact_runtime(&requirement, Some(version), None)
.unwrap()
.is_err()
);
}
for observed in [
"v26.7.1",
"v26.7.00",
"v26.7.01",
"v26.7.0.1",
"v26.7.0-rc.1",
] {
assert!(
check_exact_runtime("node@26.7.0", Some(observed), None)
.unwrap()
.is_err()
);
}
}
#[test]
fn prerelease_and_build_metadata_are_part_of_exact_identity() {
let versions = [
"26.7.0",
"26.7.0-rc.1",
"26.7.0-rc.2",
"26.7.0+one",
"26.7.0+two",
"26.7.0-rc.1+0001",
];
for expected in versions {
let requirement = format!("node@{expected}");
validate(&requirement).unwrap();
for observed in versions {
let actual = format!("v{observed}");
assert_eq!(
check_exact_runtime(&requirement, Some(&actual), None)
.unwrap()
.is_ok(),
expected == observed
);
}
}
}
#[test]
fn unsupported_runtimes_never_become_named_tool_fallbacks() {
for requirement in [
"rust@1.90.0",
"npm@11.0.0",
"Node@26.7.0",
"@26.7.0",
"clang@20.0.0",
] {
assert!(validate(requirement).is_err());
assert!(
check_exact_runtime(requirement, Some("v26.7.0"), Some("1.90.0"))
.unwrap()
.is_err()
);
}
}
#[test]
fn missing_malformed_and_multiline_observations_are_not_evidence() {
let unknown = check_exact_runtime("node@26.7.0", None, None)
.unwrap()
.unwrap_err();
assert_eq!(
unknown,
"capability_missing:runtime_version:node:required=26.7.0:observed=<unknown>"
);
for observed in [
"",
"node v26.7.0",
"vv26.7.0",
"v26.7.0\nother",
"v26.7.0\0",
"v26.7.0\x1b[0m",
] {
let detail = check_exact_runtime("node@26.7.0", Some(observed), None)
.unwrap()
.unwrap_err();
assert!(detail.ends_with("observed=<invalid>"), "{detail}");
assert!(!detail.chars().any(char::is_control));
}
}
#[test]
fn version_bounds_do_not_overflow_or_truncate_into_matches() {
let bounded = format!("1.2.3+{}", "a".repeat(MAX_VERSION_BYTES - 6));
assert_eq!(bounded.len(), MAX_VERSION_BYTES);
assert!(exact_version(&bounded));
assert!(!exact_version(&format!("{bounded}a")));
assert!(exact_version("184467440737095516160.2.3"));
assert!(!exact_version("1.2.3-000"));
assert!(exact_version("1.2.3+000"));
}
#[test]
fn conflicting_exact_requirements_cannot_both_match() {
for observed in ["v22.0.0", "v26.7.0", "v26.7.1"] {
assert!(!["node@26.7.0", "node@26.7.1"].iter().all(|required| {
check_exact_runtime(required, Some(observed), None)
.unwrap()
.is_ok()
}));
}
}
}