use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
const RUNTIME_PACKAGES: [&str; 4] = ["react", "react-dom", "preact", "svelte"];
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "level", rename_all = "lowercase")]
pub enum Trust {
#[default]
Strict,
Permissive {
#[serde(default)]
network: Vec<String>,
#[serde(default)]
host: bool,
},
Trusted,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Imports {
#[default]
Any,
Listed,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct Deny {
pub pack: String,
#[serde(default)]
pub version: Option<VersionReq>,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct Library {
pub name: String,
pub version: Version,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct Policy {
#[serde(default)]
pub packs: BTreeMap<String, Trust>,
#[serde(default)]
pub imports: Imports,
#[serde(default)]
pub deny: Vec<Deny>,
#[serde(default)]
pub refused: BTreeMap<String, String>,
#[serde(default)]
pub runtimes: Option<Vec<Library>>,
#[serde(default)]
pub libraries: Option<Vec<Library>>,
}
impl Policy {
#[must_use]
pub fn trust(&self, pack: &str) -> Trust {
self.packs.get(pack).cloned().unwrap_or_default()
}
pub(crate) fn refusal(&self, pack: &str, ambient: &[String]) -> Option<String> {
if let Some(reason) = self.refused.get(pack) {
return Some(format!("`{pack}` isn't available here: {reason}"));
}
let listed = self.packs.contains_key(pack) || ambient.iter().any(|known| known == pack);
(self.imports == Imports::Listed && !listed).then(|| {
format!(
"this host allows only the packs its configuration lists, and `{pack}` isn't one"
)
})
}
pub(crate) fn runs(&self, runtime: &str) -> bool {
let Some(runtimes) = &self.runtimes else {
return true;
};
runtime.rsplit_once('@').is_some_and(|(name, major)| {
runtimes
.iter()
.any(|known| known.name == name && known.version.major.to_string() == major)
})
}
pub(crate) fn provides(&self, name: &str) -> bool {
RUNTIME_PACKAGES.contains(&name)
|| !name.starts_with('@')
|| self
.libraries
.iter()
.flatten()
.any(|library| library.name == name)
}
pub(crate) fn unmet(&self, name: &str, range: &VersionReq) -> Option<String> {
let runtime = RUNTIME_PACKAGES.contains(&name);
let provided = if runtime {
self.runtimes.as_ref()
} else {
self.libraries.as_ref()
}?;
match provided.iter().find(|known| known.name == name) {
None if runtime => Some(format!("`{name}`, a runtime this host doesn't run")),
None => Some(format!("`{name}`, a library this host doesn't offer")),
Some(known) if !range.matches(&known.version) => Some(format!(
"`{name}` `{range}`, and this host has {}",
known.version
)),
Some(_) => None,
}
}
pub(crate) fn denial(&self, pack: &str, version: &Version) -> Option<String> {
self.deny
.iter()
.find(|deny| {
deny.pack == pack
&& deny
.version
.as_ref()
.is_none_or(|range| range.matches(version))
})
.map(|deny| format!("`{pack}@{version}` is denied here: {}", deny.reason))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn policy(value: serde_json::Value) -> Policy {
serde_json::from_value(value).expect("a policy")
}
#[test]
fn listed_packs_have_their_trust_and_others_are_strict() {
let policy = policy(json!({ "packs": {
"@acme/app": { "level": "trusted" },
"@acme/maps": { "level": "permissive", "network": ["https://tiles.example.com"] },
"@acme/charts": { "level": "permissive", "host": true }
} }));
assert_eq!(policy.trust("@acme/app"), Trust::Trusted);
assert_eq!(
policy.trust("@acme/maps"),
Trust::Permissive {
network: vec!["https://tiles.example.com".into()],
host: false,
}
);
assert_eq!(
policy.trust("@acme/charts"),
Trust::Permissive {
network: Vec::new(),
host: true,
}
);
assert_eq!(policy.trust("@other/kit"), Trust::Strict);
assert_eq!(
serde_json::to_value(Trust::Strict).expect("JSON"),
json!({ "level": "strict" })
);
}
#[test]
fn a_pack_is_refused_when_left_out_or_unlisted() {
let any = policy(json!({ "refused": { "@huge/pack": "larger than 5 MB" } }));
assert_eq!(
any.refusal("@huge/pack", &[]).as_deref(),
Some("`@huge/pack` isn't available here: larger than 5 MB")
);
assert_eq!(any.refusal("@acme/kit", &[]), None);
let listed = policy(
json!({ "imports": "listed", "packs": { "@acme/maps": { "level": "strict" } } }),
);
assert_eq!(listed.refusal("@acme/maps", &[]), None);
assert_eq!(
listed.refusal("@host/c", &["@host/c".into()]),
None,
"ambient"
);
assert!(listed.refusal("@acme/kit", &[]).is_some());
}
#[test]
fn the_host_runs_the_runtimes_it_lists_and_every_one_when_it_lists_none() {
assert!(Policy::default().runs("preact@10"));
let policy = policy(json!({ "runtimes": [
{ "name": "react", "version": "19.3.0" },
{ "name": "svelte", "version": "5.57.2" }
] }));
assert!(policy.runs("react@19"));
assert!(policy.runs("svelte@5"));
assert!(!policy.runs("svelte@4"));
assert!(!policy.runs("preact@10"));
assert!(!policy.runs("react"));
}
#[test]
fn a_packs_needs_are_met_by_the_hosts_runtimes_and_libraries() {
let range = |text: &str| VersionReq::parse(text).expect("a range");
let policy = policy(json!({
"runtimes": [{ "name": "svelte", "version": "5.40.0" }],
"libraries": [{ "name": "d3", "version": "7.9.0" }]
}));
assert_eq!(policy.unmet("svelte", &range("^5.20")), None);
assert_eq!(
policy.unmet("svelte", &range("^5.57")).as_deref(),
Some("`svelte` `^5.57`, and this host has 5.40.0")
);
assert_eq!(
policy.unmet("react", &range("^19")).as_deref(),
Some("`react`, a runtime this host doesn't run")
);
assert_eq!(policy.unmet("d3", &range("^7")), None);
assert_eq!(
policy.unmet("three", &range("^0.170")).as_deref(),
Some("`three`, a library this host doesn't offer")
);
assert_eq!(
Policy::default().unmet("d3", &range("^7")),
None,
"not said"
);
assert_eq!(Policy::default().unmet("react", &range("^19")), None);
}
#[test]
fn runtimes_offered_libraries_and_unscoped_names_are_provided_and_scoped_ones_are_library_packs()
{
let policy = policy(json!({ "libraries": [{ "name": "@acme/d3", "version": "7.9.0" }] }));
assert!(policy.provides("svelte"));
assert!(policy.provides("d3"), "not scoped");
assert!(policy.provides("@acme/d3"), "offered");
assert!(!policy.provides("@kit/three"));
assert!(!Policy::default().provides("@kit/three"));
}
#[test]
fn a_denial_covers_its_versions() {
let policy = policy(json!({ "deny": [
{ "pack": "@bad/pack", "reason": "misuse" },
{ "pack": "@acme/kit", "version": "<2", "reason": "a flaw found in 1.x" }
] }));
let version = |text: &str| Version::parse(text).expect("a version");
assert_eq!(
policy.denial("@bad/pack", &version("3.0.0")).as_deref(),
Some("`@bad/pack@3.0.0` is denied here: misuse")
);
assert!(policy.denial("@acme/kit", &version("1.4.2")).is_some());
assert_eq!(policy.denial("@acme/kit", &version("2.0.0")), None);
assert_eq!(policy.denial("@acme/charts", &version("1.0.0")), None);
}
}