use aube_manifest::AllowBuildRaw;
use std::collections::{BTreeMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllowDecision {
Allow,
Deny,
Unspecified,
}
#[derive(Debug, Clone, Default)]
pub struct BuildPolicy {
allow_all: bool,
allowed: HashSet<String>,
denied: HashSet<String>,
allowed_sources: HashSet<String>,
denied_sources: HashSet<String>,
allowed_git_repositories: HashSet<String>,
denied_git_repositories: HashSet<String>,
allowed_wildcards: Vec<String>,
denied_wildcards: Vec<String>,
}
impl BuildPolicy {
pub fn deny_all() -> Self {
Self::default()
}
pub fn allow_all() -> Self {
Self {
allow_all: true,
..Self::default()
}
}
pub fn from_config(
allow_builds: &BTreeMap<String, AllowBuildRaw>,
only_built: &[String],
never_built: &[String],
dangerously_allow_all: bool,
) -> (Self, Vec<BuildPolicyError>) {
if dangerously_allow_all {
return (Self::allow_all(), Vec::new());
}
let mut allowed = HashSet::new();
let mut denied = HashSet::new();
let mut allowed_sources = HashSet::new();
let mut denied_sources = HashSet::new();
let mut allowed_git_repositories = HashSet::new();
let mut denied_git_repositories = HashSet::new();
let mut allowed_wildcards = Vec::new();
let mut denied_wildcards = Vec::new();
let mut warnings = Vec::new();
for (pattern, value) in allow_builds {
let bool_value = match value {
AllowBuildRaw::Bool(b) => *b,
AllowBuildRaw::Other(raw) => {
if raw == aube_manifest::workspace::ALLOW_BUILDS_REVIEW_PLACEHOLDER {
continue;
}
warnings.push(BuildPolicyError::UnsupportedValue {
pattern: pattern.clone(),
raw: raw.clone(),
});
continue;
}
};
match expand_spec(pattern) {
Ok(expanded) => {
let (exact, wild) = if bool_value {
(&mut allowed, &mut allowed_wildcards)
} else {
(&mut denied, &mut denied_wildcards)
};
let source = if bool_value {
&mut allowed_sources
} else {
&mut denied_sources
};
let git_repositories = if bool_value {
&mut allowed_git_repositories
} else {
&mut denied_git_repositories
};
sort_entries(expanded, exact, source, git_repositories, wild);
}
Err(e) => warnings.push(e),
}
}
for pattern in only_built {
match expand_spec(pattern) {
Ok(expanded) => sort_entries(
expanded,
&mut allowed,
&mut allowed_sources,
&mut allowed_git_repositories,
&mut allowed_wildcards,
),
Err(e) => warnings.push(e),
}
}
for pattern in never_built {
match expand_spec(pattern) {
Ok(expanded) => sort_entries(
expanded,
&mut denied,
&mut denied_sources,
&mut denied_git_repositories,
&mut denied_wildcards,
),
Err(e) => warnings.push(e),
}
}
(
Self {
allow_all: false,
allowed,
denied,
allowed_sources,
denied_sources,
allowed_git_repositories,
denied_git_repositories,
allowed_wildcards,
denied_wildcards,
},
warnings,
)
}
pub fn denylist(denied_patterns: &[String]) -> (Self, Vec<BuildPolicyError>) {
let mut denied = HashSet::new();
let mut denied_sources = HashSet::new();
let mut denied_git_repositories = HashSet::new();
let mut denied_wildcards = Vec::new();
let mut warnings = Vec::new();
for pattern in denied_patterns {
match expand_spec(pattern) {
Ok(expanded) => sort_entries(
expanded,
&mut denied,
&mut denied_sources,
&mut denied_git_repositories,
&mut denied_wildcards,
),
Err(e) => warnings.push(e),
}
}
(
Self {
allow_all: true,
allowed: HashSet::new(),
denied,
allowed_sources: HashSet::new(),
denied_sources,
allowed_git_repositories: HashSet::new(),
denied_git_repositories,
allowed_wildcards: Vec::new(),
denied_wildcards,
},
warnings,
)
}
pub fn decide(&self, name: &str, version: &str) -> AllowDecision {
self.decide_package(name, version, None)
}
pub fn decide_package(
&self,
name: &str,
version: &str,
source_key: Option<&str>,
) -> AllowDecision {
self.decide_package_with_git_repository(name, version, source_key, None)
}
pub fn decide_package_with_git_repository(
&self,
name: &str,
version: &str,
source_key: Option<&str>,
git_repository_key: Option<&str>,
) -> AllowDecision {
thread_local! {
static KEY_BUF: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}
if self.denied.contains(name) {
return AllowDecision::Deny;
}
if matches_any_wildcard(name, &self.denied_wildcards) {
return AllowDecision::Deny;
}
if let Some(source_key) = source_key
&& self.denied_sources.contains(source_key)
{
return AllowDecision::Deny;
}
if let Some(git_repository_key) = git_repository_key
&& self.denied_git_repositories.contains(git_repository_key)
{
return AllowDecision::Deny;
}
let (denied_versioned, allowed_versioned) = KEY_BUF.with(|buf| {
let mut b = buf.borrow_mut();
b.clear();
use std::fmt::Write as _;
let _ = write!(b, "{name}@{version}");
let key = b.as_str();
(self.denied.contains(key), self.allowed.contains(key))
});
if denied_versioned {
return AllowDecision::Deny;
}
if self.allow_all {
return AllowDecision::Allow;
}
if let Some(git_repository_key) = git_repository_key
&& self.allowed_git_repositories.contains(git_repository_key)
{
return AllowDecision::Allow;
}
if let Some(source_key) = source_key {
return if self.allowed_sources.contains(source_key) {
AllowDecision::Allow
} else {
AllowDecision::Unspecified
};
}
if self.allowed.contains(name) || allowed_versioned {
return AllowDecision::Allow;
}
if matches_any_wildcard(name, &self.allowed_wildcards) {
return AllowDecision::Allow;
}
AllowDecision::Unspecified
}
pub fn has_any_allow_rule(&self) -> bool {
self.allow_all
|| !self.allowed.is_empty()
|| !self.allowed_sources.is_empty()
|| !self.allowed_git_repositories.is_empty()
|| !self.allowed_wildcards.is_empty()
}
pub fn merge(&mut self, other: &Self) {
self.allow_all |= other.allow_all;
self.allowed.extend(other.allowed.iter().cloned());
self.denied.extend(other.denied.iter().cloned());
self.allowed_sources
.extend(other.allowed_sources.iter().cloned());
self.denied_sources
.extend(other.denied_sources.iter().cloned());
self.allowed_git_repositories
.extend(other.allowed_git_repositories.iter().cloned());
self.denied_git_repositories
.extend(other.denied_git_repositories.iter().cloned());
merge_unique(&mut self.allowed_wildcards, &other.allowed_wildcards);
merge_unique(&mut self.denied_wildcards, &other.denied_wildcards);
}
}
fn merge_unique(target: &mut Vec<String>, source: &[String]) {
for value in source {
if !target.iter().any(|existing| existing == value) {
target.push(value.clone());
}
}
}
pub fn pattern_matches(pattern: &str, name: &str, version: &str) -> Result<bool, BuildPolicyError> {
let with_version = format!("{name}@{version}");
for expanded in expand_spec(pattern)? {
if expanded.contains('*') {
if matches_wildcard(name, &expanded) {
return Ok(true);
}
} else if expanded == name || expanded == with_version {
return Ok(true);
}
}
Ok(false)
}
fn sort_entries(
entries: Vec<String>,
exact: &mut HashSet<String>,
sources: &mut HashSet<String>,
git_repositories: &mut HashSet<String>,
wildcards: &mut Vec<String>,
) {
for entry in entries {
if entry.contains('*') {
if !wildcards.iter().any(|p| p == &entry) {
wildcards.push(entry);
}
} else if is_git_repository_key(&entry) {
git_repositories.insert(entry);
} else if is_source_key(&entry) {
sources.insert(entry);
} else {
exact.insert(entry);
}
}
}
fn matches_any_wildcard(name: &str, patterns: &[String]) -> bool {
patterns.iter().any(|p| matches_wildcard(name, p))
}
fn matches_wildcard(name: &str, pattern: &str) -> bool {
let parts: Vec<&str> = pattern.split('*').collect();
let (first, rest) = match parts.split_first() {
Some(pair) => pair,
None => return false,
};
let Some(after_prefix) = name.strip_prefix(first) else {
return false;
};
let (last, middle) = match rest.split_last() {
Some(pair) => pair,
None => {
debug_assert!(false, "matches_wildcard called with no-wildcard pattern");
return false;
}
};
let mut remaining = after_prefix;
for mid in middle {
match remaining.find(mid) {
Some(idx) => remaining = &remaining[idx + mid.len()..],
None => return false,
}
}
remaining.len() >= last.len() && remaining.ends_with(last)
}
#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
pub enum BuildPolicyError {
#[error("build policy entry {pattern:?} has unsupported value {raw:?}: expected true/false")]
#[diagnostic(code(ERR_AUBE_BUILD_POLICY_UNSUPPORTED_VALUE))]
UnsupportedValue { pattern: String, raw: String },
#[error("build policy pattern {0:?} contains an invalid version union")]
#[diagnostic(code(ERR_AUBE_BUILD_POLICY_INVALID_VERSION_UNION))]
InvalidVersionUnion(String),
#[error("build policy pattern {0:?} mixes a wildcard name with a version union")]
#[diagnostic(code(ERR_AUBE_BUILD_POLICY_WILDCARD_WITH_VERSION))]
WildcardWithVersion(String),
}
fn expand_spec(pattern: &str) -> Result<Vec<String>, BuildPolicyError> {
let (name, versions_part) = split_name_and_versions(pattern);
if versions_part.is_empty() {
return Ok(vec![name.to_string()]);
}
if name.contains('*') {
return Err(BuildPolicyError::WildcardWithVersion(pattern.to_string()));
}
let mut out = Vec::new();
for raw in versions_part.split("||") {
let trimmed = raw.trim();
if is_source_version(trimmed) && !versions_part.contains("||") {
out.push(format!("{name}@{trimmed}"));
return Ok(out);
}
if trimmed.is_empty() || !is_exact_semver(trimmed) {
return Err(BuildPolicyError::InvalidVersionUnion(pattern.to_string()));
}
out.push(format!("{name}@{trimmed}"));
}
Ok(out)
}
fn is_source_key(key: &str) -> bool {
let (_, version) = split_name_and_versions(key);
is_source_version(version)
}
fn is_git_repository_key(key: &str) -> bool {
let (_, source) = split_name_and_versions(key);
!source.contains('#')
&& [
"git+https://",
"git+http://",
"git+ssh://",
"git+file://",
"git+git://",
]
.iter()
.any(|prefix| source.starts_with(prefix))
}
fn is_source_version(version: &str) -> bool {
[
"file+",
"link+",
"portal+",
"exec+",
"git+",
"url+",
"file:",
"link:",
"portal:",
"exec:",
"git:",
"http://",
"https://",
"github:",
"workspace:",
]
.iter()
.any(|prefix| version.starts_with(prefix))
}
fn split_name_and_versions(pattern: &str) -> (&str, &str) {
let scoped = pattern.starts_with('@');
let search_from = if scoped { 1 } else { 0 };
match pattern[search_from..].find('@') {
Some(rel) => {
let at = search_from + rel;
(&pattern[..at], &pattern[at + 1..])
}
None => (pattern, ""),
}
}
fn is_exact_semver(s: &str) -> bool {
let core = s.split('+').next().unwrap_or(s);
let main = core.split('-').next().unwrap_or(core);
let parts: Vec<&str> = main.split('.').collect();
if parts.len() != 3 {
return false;
}
parts
.iter()
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
}
#[cfg(test)]
mod tests {
use super::*;
fn policy(pairs: &[(&str, bool)]) -> BuildPolicy {
let map: BTreeMap<String, AllowBuildRaw> = pairs
.iter()
.map(|(k, v)| ((*k).to_string(), AllowBuildRaw::Bool(*v)))
.collect();
let (p, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert!(errs.is_empty(), "unexpected warnings: {errs:?}");
p
}
#[test]
fn bare_name_allows_any_version() {
let p = policy(&[("esbuild", true)]);
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Allow);
assert_eq!(p.decide("esbuild", "0.25.0"), AllowDecision::Allow);
assert_eq!(p.decide("rollup", "4.0.0"), AllowDecision::Unspecified);
}
#[test]
fn bare_name_does_not_allow_source_backed_package() {
let p = policy(&[("esbuild", true)]);
assert_eq!(
p.decide_package("esbuild", "0.25.0", Some("esbuild@file+abc123")),
AllowDecision::Unspecified
);
}
#[test]
fn exact_source_key_allows_source_backed_package() {
let p = policy(&[("esbuild@file+abc123", true)]);
assert_eq!(
p.decide_package("esbuild", "0.25.0", Some("esbuild@file+abc123")),
AllowDecision::Allow
);
assert_eq!(
p.decide_package("esbuild", "0.25.0", Some("esbuild@file+def456")),
AllowDecision::Unspecified
);
assert_eq!(p.decide("esbuild", "0.25.0"), AllowDecision::Unspecified);
}
#[test]
fn source_keys_accept_url_and_git_tails() {
let p = policy(&[
("native@url+abc123", true),
("gitdep@git+def456", true),
("raw-url@https://example.com/pkg.tgz", true),
("raw-git@github:owner/repo", true),
]);
assert_eq!(
p.decide_package("native", "1.0.0", Some("native@url+abc123")),
AllowDecision::Allow
);
assert_eq!(
p.decide_package("gitdep", "1.0.0", Some("gitdep@git+def456")),
AllowDecision::Allow
);
assert_eq!(
p.decide_package(
"raw-url",
"1.0.0",
Some("raw-url@https://example.com/pkg.tgz")
),
AllowDecision::Allow
);
assert_eq!(
p.decide_package("raw-git", "1.0.0", Some("raw-git@github:owner/repo")),
AllowDecision::Allow
);
}
#[test]
fn git_repository_rule_allows_every_resolved_commit() {
let p = policy(&[("gitdep@git+https://github.com/acme/gitdep.git", true)]);
for source_key in [
"gitdep@https://github.com/acme/gitdep.git#0123456789012345678901234567890123456789",
"gitdep@https://github.com/acme/gitdep.git#abcdefabcdefabcdefabcdefabcdefabcdefabcd",
] {
assert_eq!(
p.decide_package_with_git_repository(
"gitdep",
"1.0.0",
Some(source_key),
Some("gitdep@git+https://github.com/acme/gitdep.git"),
),
AllowDecision::Allow
);
}
assert_eq!(
p.decide_package_with_git_repository(
"gitdep",
"1.0.0",
Some("gitdep@https://github.com/acme/other.git#0123456789012345678901234567890123456789"),
Some("gitdep@git+https://github.com/acme/other.git"),
),
AllowDecision::Unspecified
);
}
#[test]
fn git_repository_rule_accepts_native_git_transport() {
let p = policy(&[("gitdep@git+git://github.com/acme/gitdep.git", true)]);
assert_eq!(
p.decide_package_with_git_repository(
"gitdep",
"1.0.0",
Some("gitdep@git://github.com/acme/gitdep.git#0123456789012345678901234567890123456789"),
Some("gitdep@git+git://github.com/acme/gitdep.git"),
),
AllowDecision::Allow
);
}
#[test]
fn git_repository_deny_and_package_deny_override_repository_allow() {
let p = policy(&[
("gitdep@git+https://github.com/acme/gitdep.git", true),
("other@git+https://github.com/acme/other.git", false),
("blocked", false),
("blocked@git+https://github.com/acme/blocked.git", true),
]);
assert_eq!(
p.decide_package_with_git_repository(
"other",
"1.0.0",
Some("other@https://github.com/acme/other.git#0123456789012345678901234567890123456789"),
Some("other@git+https://github.com/acme/other.git"),
),
AllowDecision::Deny
);
assert_eq!(
p.decide_package_with_git_repository(
"blocked",
"1.0.0",
Some("blocked@https://github.com/acme/blocked.git#0123456789012345678901234567890123456789"),
Some("blocked@git+https://github.com/acme/blocked.git"),
),
AllowDecision::Deny
);
}
#[test]
fn source_backed_package_name_deny_still_wins() {
let p = policy(&[("esbuild", false), ("esbuild@file+abc123", true)]);
assert_eq!(
p.decide_package("esbuild", "0.25.0", Some("esbuild@file+abc123")),
AllowDecision::Deny
);
}
#[test]
fn exact_version_is_strict() {
let p = policy(&[("esbuild@0.19.0", true)]);
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Allow);
assert_eq!(p.decide("esbuild", "0.19.1"), AllowDecision::Unspecified);
}
#[test]
fn version_union_splits() {
let p = policy(&[("esbuild@0.19.0 || 0.20.1", true)]);
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Allow);
assert_eq!(p.decide("esbuild", "0.20.1"), AllowDecision::Allow);
assert_eq!(p.decide("esbuild", "0.20.0"), AllowDecision::Unspecified);
}
#[test]
fn scoped_package_parses() {
let p = policy(&[("@swc/core@1.3.0", true)]);
assert_eq!(p.decide("@swc/core", "1.3.0"), AllowDecision::Allow);
assert_eq!(p.decide("@swc/core", "1.4.0"), AllowDecision::Unspecified);
}
#[test]
fn scoped_bare_name() {
let p = policy(&[("@swc/core", true)]);
assert_eq!(p.decide("@swc/core", "1.3.0"), AllowDecision::Allow);
}
#[test]
fn pattern_matches_scoped_names_and_versions() {
assert!(pattern_matches("@swc/core", "@swc/core", "1.3.0").unwrap());
assert!(pattern_matches("@swc/core@1.3.0", "@swc/core", "1.3.0").unwrap());
assert!(!pattern_matches("@swc/core@1.3.0", "@swc/core", "1.3.1").unwrap());
assert!(pattern_matches("@swc/*", "@swc/core", "1.3.0").unwrap());
assert!(pattern_matches("aube-test-*", "aube-test-native", "1.0.0").unwrap());
}
#[test]
fn dangerously_allow_all_bypasses_deny_list() {
let mut map = BTreeMap::new();
map.insert("esbuild".into(), AllowBuildRaw::Bool(false));
let (p, errs) = BuildPolicy::from_config(&map, &[], &[], true);
assert!(errs.is_empty());
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Allow);
}
#[test]
fn deny_wins_over_allow_when_both_listed() {
let map: BTreeMap<String, AllowBuildRaw> = [
("esbuild".to_string(), AllowBuildRaw::Bool(true)),
("esbuild@0.19.0".to_string(), AllowBuildRaw::Bool(false)),
]
.into_iter()
.collect();
let (p, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert!(errs.is_empty());
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Deny);
assert_eq!(p.decide("esbuild", "0.19.1"), AllowDecision::Allow);
}
#[test]
fn deny_all_is_default() {
let p = BuildPolicy::deny_all();
assert_eq!(p.decide("anything", "1.0.0"), AllowDecision::Unspecified);
assert!(!p.has_any_allow_rule());
}
#[test]
fn allow_all_flag() {
let p = BuildPolicy::allow_all();
assert_eq!(p.decide("anything", "1.0.0"), AllowDecision::Allow);
assert!(p.has_any_allow_rule());
}
#[test]
fn invalid_version_union_reports_warning() {
let map: BTreeMap<String, AllowBuildRaw> = [(
"esbuild@not-a-version".to_string(),
AllowBuildRaw::Bool(true),
)]
.into_iter()
.collect();
let (p, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert_eq!(errs.len(), 1);
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Unspecified);
}
#[test]
fn source_specs_cannot_be_union_members() {
let map: BTreeMap<String, AllowBuildRaw> = [(
"dependency@https://example.com/dep.tgz || 1.0.0".to_string(),
AllowBuildRaw::Bool(true),
)]
.into_iter()
.collect();
let (_, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert_eq!(errs.len(), 1);
assert!(matches!(errs[0], BuildPolicyError::InvalidVersionUnion(_)));
}
#[test]
fn semver_then_source_spec_union_is_also_rejected() {
let map: BTreeMap<String, AllowBuildRaw> = [(
"dependency@1.0.0 || https://example.com/dep.tgz".to_string(),
AllowBuildRaw::Bool(true),
)]
.into_iter()
.collect();
let (_, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert_eq!(errs.len(), 1);
assert!(matches!(errs[0], BuildPolicyError::InvalidVersionUnion(_)));
}
#[test]
fn non_bool_value_reports_warning() {
let map: BTreeMap<String, AllowBuildRaw> =
[("esbuild".to_string(), AllowBuildRaw::Other("maybe".into()))]
.into_iter()
.collect();
let (_, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert_eq!(errs.len(), 1);
}
#[test]
fn only_built_dependencies_allowlist_coexists_with_allow_builds() {
let map = BTreeMap::new();
let only_built = vec!["esbuild".to_string(), "@swc/core@1.3.0".to_string()];
let (p, errs) = BuildPolicy::from_config(&map, &only_built, &[], false);
assert!(errs.is_empty());
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Allow);
assert_eq!(p.decide("@swc/core", "1.3.0"), AllowDecision::Allow);
assert_eq!(p.decide("@swc/core", "1.4.0"), AllowDecision::Unspecified);
assert!(p.has_any_allow_rule());
}
#[test]
fn never_built_dependencies_denies() {
let map = BTreeMap::new();
let only_built = vec!["esbuild".to_string()];
let never_built = vec!["esbuild@0.19.0".to_string()];
let (p, errs) = BuildPolicy::from_config(&map, &only_built, &never_built, false);
assert!(errs.is_empty());
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Deny);
assert_eq!(p.decide("esbuild", "0.20.0"), AllowDecision::Allow);
}
#[test]
fn never_built_beats_allow_builds_map() {
let map: BTreeMap<String, AllowBuildRaw> =
[("esbuild".to_string(), AllowBuildRaw::Bool(true))]
.into_iter()
.collect();
let never_built = vec!["esbuild".to_string()];
let (p, errs) = BuildPolicy::from_config(&map, &[], &never_built, false);
assert!(errs.is_empty());
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Deny);
}
#[test]
fn merge_deduplicates_wildcards() {
let mut p = policy(&[("@babel/*", true), ("*-internal", false)]);
let other = policy(&[
("@babel/*", true),
("@types/*", true),
("*-internal", false),
]);
p.merge(&other);
p.merge(&other);
assert_eq!(p.allowed_wildcards, vec!["@babel/*", "@types/*"]);
assert_eq!(p.denied_wildcards, vec!["*-internal"]);
assert_eq!(p.decide("@types/node", "1.0.0"), AllowDecision::Allow);
assert_eq!(p.decide("pkg-internal", "1.0.0"), AllowDecision::Deny);
}
#[test]
fn splits_scoped_correctly() {
assert_eq!(
split_name_and_versions("@swc/core@1.3.0"),
("@swc/core", "1.3.0")
);
assert_eq!(split_name_and_versions("@swc/core"), ("@swc/core", ""));
assert_eq!(
split_name_and_versions("esbuild@0.19.0"),
("esbuild", "0.19.0")
);
assert_eq!(split_name_and_versions("esbuild"), ("esbuild", ""));
}
#[test]
fn wildcard_scope_allows_every_scope_member() {
let p = policy(&[("@babel/*", true)]);
assert_eq!(p.decide("@babel/core", "7.0.0"), AllowDecision::Allow);
assert_eq!(
p.decide("@babel/preset-env", "7.22.0"),
AllowDecision::Allow
);
assert_eq!(p.decide("@swc/core", "1.3.0"), AllowDecision::Unspecified);
assert_eq!(
p.decide("babel-loader", "9.0.0"),
AllowDecision::Unspecified
);
assert!(p.has_any_allow_rule());
}
#[test]
fn wildcard_suffix_matches_any_prefix() {
let p = policy(&[("*-loader", true)]);
assert_eq!(p.decide("css-loader", "6.0.0"), AllowDecision::Allow);
assert_eq!(p.decide("babel-loader", "9.0.0"), AllowDecision::Allow);
assert_eq!(
p.decide("loader-utils", "3.0.0"),
AllowDecision::Unspecified
);
}
#[test]
fn bare_star_matches_everything_and_is_distinct_from_allow_all() {
let map: BTreeMap<String, AllowBuildRaw> = [
("*".to_string(), AllowBuildRaw::Bool(true)),
("sketchy-pkg".to_string(), AllowBuildRaw::Bool(false)),
]
.into_iter()
.collect();
let (p, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert!(errs.is_empty());
assert_eq!(p.decide("esbuild", "0.19.0"), AllowDecision::Allow);
assert_eq!(p.decide("sketchy-pkg", "1.0.0"), AllowDecision::Deny);
}
#[test]
fn denied_wildcard_blocks_allowed_exact() {
let map: BTreeMap<String, AllowBuildRaw> = [
("@babel/core".to_string(), AllowBuildRaw::Bool(true)),
("@babel/*".to_string(), AllowBuildRaw::Bool(false)),
]
.into_iter()
.collect();
let (p, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert!(errs.is_empty());
assert_eq!(p.decide("@babel/core", "7.0.0"), AllowDecision::Deny);
assert_eq!(p.decide("@babel/traverse", "7.0.0"), AllowDecision::Deny);
}
#[test]
fn wildcard_with_version_is_rejected() {
let map: BTreeMap<String, AllowBuildRaw> =
[("@babel/*@7.0.0".to_string(), AllowBuildRaw::Bool(true))]
.into_iter()
.collect();
let (p, errs) = BuildPolicy::from_config(&map, &[], &[], false);
assert_eq!(errs.len(), 1);
assert!(matches!(errs[0], BuildPolicyError::WildcardWithVersion(_)));
assert_eq!(p.decide("@babel/core", "7.0.0"), AllowDecision::Unspecified);
}
#[test]
fn wildcards_flow_through_flat_lists_too() {
let only_built = vec!["@types/*".to_string()];
let never_built = vec!["*-internal".to_string()];
let (p, errs) =
BuildPolicy::from_config(&BTreeMap::new(), &only_built, &never_built, false);
assert!(errs.is_empty());
assert_eq!(p.decide("@types/node", "20.0.0"), AllowDecision::Allow);
assert_eq!(p.decide("@types/react", "18.0.0"), AllowDecision::Allow);
assert_eq!(p.decide("acme-internal", "1.0.0"), AllowDecision::Deny);
}
#[test]
fn matches_wildcard_handles_all_positions() {
assert!(matches_wildcard("@babel/core", "@babel/*"));
assert!(matches_wildcard("@babel/", "@babel/*"));
assert!(!matches_wildcard("@babe/core", "@babel/*"));
assert!(matches_wildcard("css-loader", "*-loader"));
assert!(matches_wildcard("-loader", "*-loader"));
assert!(!matches_wildcard("loader-x", "*-loader"));
assert!(matches_wildcard("foobar", "foo*bar"));
assert!(matches_wildcard("foo-x-bar", "foo*bar"));
assert!(!matches_wildcard("foobaz", "foo*bar"));
assert!(matches_wildcard("@x/anything", "*"));
assert!(matches_wildcard("", "*"));
assert!(matches_wildcard("anything", "**"));
}
#[test]
fn matches_wildcard_multi_segment_greedy_is_correct() {
assert!(matches_wildcard("abca", "*a*bc*a"));
assert!(matches_wildcard("xabcaYa", "*a*bc*a"));
assert!(matches_wildcard("abcaXa", "*a*bc*a"));
assert!(matches_wildcard("ababab", "*ab*ab*"));
assert!(matches_wildcard("abcd", "a*b*c*d"));
assert!(matches_wildcard("a1b2c3d", "a*b*c*d"));
assert!(!matches_wildcard("aab", "*ab*ab"));
assert!(!matches_wildcard("abab", "*abc*abc"));
assert!(matches_wildcard(
"@acme/core-loader-plugin",
"@acme/*-*-plugin"
));
assert!(!matches_wildcard(
"@acme/core-plugin-extra",
"@acme/*-*-plugin"
));
}
#[test]
fn semver_shape() {
assert!(is_exact_semver("1.2.3"));
assert!(is_exact_semver("0.19.0"));
assert!(is_exact_semver("1.0.0-alpha"));
assert!(is_exact_semver("1.0.0+build.42"));
assert!(!is_exact_semver("1.2"));
assert!(!is_exact_semver("^1.2.3"));
assert!(!is_exact_semver("1.x.0"));
}
}