use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::install::Compat;
pub const SCHEMA: u32 = 1;
pub const BUNDLED: &str = include_str!("../../plugin-index/index.toml");
pub const OVERRIDE_ENV: &str = "AUTUMN_PLUGIN_INDEX";
pub const FULL_TRUST_LABEL: &str = "full trust: native code";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PluginIndex {
pub schema: u32,
#[serde(default, rename = "plugin")]
pub plugins: Vec<Listing>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ListingOrigin {
FirstParty,
Community,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Tier {
Stable,
Experimental,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Trust {
Native,
Sandboxed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Status {
Listed,
Incompatible,
Delisted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CheckOutcome {
Pass,
Fail,
Exempt,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Conformance {
pub result: CheckOutcome,
pub autumn_web: String,
pub checked: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub reason: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Grants {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hosts: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tables: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub job_types: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub slots: Vec<String>,
}
impl Grants {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.hosts.is_empty()
&& self.tables.is_empty()
&& self.job_types.is_empty()
&& self.slots.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Listing {
pub name: String,
pub description: String,
pub origin: ListingOrigin,
pub repository: String,
pub version: String,
pub autumn_web: String,
pub tier: Tier,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub experimental_surfaces: Vec<String>,
pub trust: Trust,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routes: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub artifact_sha256: String,
#[serde(default, skip_serializing_if = "Grants::is_empty")]
pub grants: Grants,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub quotas: BTreeMap<String, u32>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub limits: BTreeMap<String, u64>,
pub status: Status,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub note: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub prefix: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub no_routes: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sensitive_routes: Vec<String>,
pub conformance: Conformance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IndexError {
Io(String),
Toml(String),
Schema(u32),
}
impl std::fmt::Display for IndexError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(msg) => write!(f, "cannot read the plugin index: {msg}"),
Self::Toml(msg) => write!(f, "the plugin index is not valid: {msg}"),
Self::Schema(found) => write!(
f,
"the plugin index uses schema {found}; this CLI reads schema {SCHEMA}"
),
}
}
}
impl std::error::Error for IndexError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Finding {
pub plugin: String,
pub message: String,
}
pub fn parse(src: &str) -> Result<PluginIndex, IndexError> {
#[derive(Deserialize)]
struct Header {
schema: u32,
}
let header: Header = toml::from_str(src).map_err(|e| IndexError::Toml(e.to_string()))?;
if header.schema != SCHEMA {
return Err(IndexError::Schema(header.schema));
}
toml::from_str(src).map_err(|e| IndexError::Toml(e.to_string()))
}
pub const MAX_INDEX_BYTES: u64 = 1024 * 1024;
#[must_use]
pub fn sanitize(text: &str) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(text.len());
for c in text.chars() {
if is_unsafe_char(c) {
let _ = write!(out, "\\u{{{:x}}}", u32::from(c));
} else {
out.push(c);
}
}
out
}
#[must_use]
pub fn is_unsafe_char(c: char) -> bool {
c.is_control()
|| matches!(
c,
'\u{0600}'..='\u{0605}'
| '\u{06DD}'
| '\u{070F}'
| '\u{0890}'..='\u{0891}'
| '\u{08E2}'
| '\u{110BD}'
| '\u{110CD}'
| '\u{13430}'..='\u{1343F}'
| '\u{FFF9}'..='\u{FFFB}'
| '\u{00AD}'
| '\u{034F}'
| '\u{061C}'
| '\u{115F}'..='\u{1160}'
| '\u{17B4}'..='\u{17B5}'
| '\u{180B}'..='\u{180F}'
| '\u{200B}'..='\u{200F}'
| '\u{2028}'..='\u{202E}'
| '\u{2060}'..='\u{206F}'
| '\u{3164}'
| '\u{FE00}'..='\u{FE0F}'
| '\u{FEFF}'
| '\u{FFA0}'
| '\u{FFF0}'..='\u{FFF8}'
| '\u{1BCA0}'..='\u{1BCA3}'
| '\u{1D173}'..='\u{1D17A}'
| '\u{E0000}'..='\u{E0FFF}'
)
}
#[must_use]
pub fn canonical(name: &str) -> String {
name.to_ascii_lowercase().replace('_', "-")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
Bundled,
Override(PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Loaded {
pub index: PluginIndex,
pub source: Source,
}
pub fn load(path: Option<&Path>) -> Result<Loaded, IndexError> {
let Some(path) = path else {
return Ok(Loaded {
index: parse(BUNDLED)?,
source: Source::Bundled,
});
};
let src = read_bounded(path)?;
Ok(Loaded {
index: parse(&src)?,
source: Source::Override(path.to_path_buf()),
})
}
fn read_bounded(path: &Path) -> Result<String, IndexError> {
use std::io::Read as _;
let io = |e: &dyn std::fmt::Display| IndexError::Io(format!("{}: {e}", path.display()));
let file = std::fs::File::open(path).map_err(|e| io(&e))?;
let meta = file.metadata().map_err(|e| io(&e))?;
if !meta.is_file() {
return Err(io(&"not a regular file"));
}
let mut bytes = Vec::new();
file.take(MAX_INDEX_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|e| io(&e))?;
if bytes.len() as u64 > MAX_INDEX_BYTES {
return Err(io(&format!("larger than {MAX_INDEX_BYTES} bytes")));
}
String::from_utf8(bytes).map_err(|_| io(&"not UTF-8"))
}
pub fn load_from_env() -> Result<Loaded, IndexError> {
let path = std::env::var_os(OVERRIDE_ENV)
.filter(|value| !value.is_empty())
.map(PathBuf::from);
load(path.as_deref())
}
impl PluginIndex {
#[must_use]
pub fn get(&self, name: &str) -> Option<&Listing> {
let name = canonical(name);
self.plugins
.iter()
.find(|listing| canonical(&listing.name) == name)
}
pub fn visible(&self) -> impl Iterator<Item = &Listing> {
self.plugins
.iter()
.filter(|listing| listing.status != Status::Delisted)
}
}
impl Listing {
#[must_use]
pub fn trust_label(&self) -> String {
match self.trust {
Trust::Native => FULL_TRUST_LABEL.to_owned(),
Trust::Sandboxed => {
let mut label = format!(
"sandboxed: capability manifest grants {}",
self.capabilities.join(", ")
);
let scopes: Vec<String> = [
("hosts", &self.grants.hosts),
("tables", &self.grants.tables),
("job types", &self.grants.job_types),
("render slots", &self.grants.slots),
]
.into_iter()
.filter(|(_, list)| !list.is_empty())
.map(|(what, list)| format!("{what} {}", list.join(", ")))
.collect();
if !scopes.is_empty() {
label.push_str("; scoped to ");
label.push_str(&scopes.join("; "));
}
if !self.routes.is_empty() {
label.push_str("; serves ");
label.push_str(&self.routes.join(", "));
}
let defaults = autumn_web::plugin_sandbox::CapabilityQuotas::default();
let changed: Vec<String> = defaults
.fields()
.into_iter()
.filter_map(|(key, default)| {
self.quotas
.get(key)
.filter(|v| **v != default)
.map(|v| format!("{key}={v}"))
})
.collect();
if !changed.is_empty() {
label.push_str("; quotas ");
label.push_str(&changed.join(", "));
}
let defaults = autumn_web::plugin_sandbox::ResourceLimits::default();
let changed: Vec<String> = defaults
.fields()
.into_iter()
.filter_map(|(key, default)| {
self.limits
.get(key)
.filter(|v| u128::from(**v) != default)
.map(|v| format!("{key}={v}"))
})
.collect();
if !changed.is_empty() {
label.push_str("; limits ");
label.push_str(&changed.join(", "));
}
label
}
}
}
#[must_use]
pub fn compat(&self, app: &str) -> Compat {
let Some(low) = concrete(app) else {
return if self.status != Status::Incompatible && range_within(app, &self.autumn_web) {
Compat::Compatible
} else {
Compat::Unknown
};
};
if self.flag_applies(&low.to_string()) {
return Compat::Incompatible;
}
let Ok(req) = semver::VersionReq::parse(&self.autumn_web) else {
return Compat::Unknown;
};
if !req.matches(&low) {
return Compat::Incompatible;
}
if let Some(top) = requirement_top(app, &low)
&& !req.matches(&top)
{
return Compat::Unknown;
}
Compat::Compatible
}
#[must_use]
pub fn flag_applies(&self, app: &str) -> bool {
if self.status == Status::Incompatible && self.trust == Trust::Sandboxed {
return true;
}
self.status == Status::Incompatible
&& concrete(app)
.zip(concrete(&self.conformance.autumn_web))
.is_some_and(|(app, failed)| {
let series = if failed.major == 0 {
semver::Version::new(0, failed.minor, 0)
} else {
semver::Version::new(failed.major, 0, 0)
};
app >= series
})
}
#[must_use]
pub fn verified_for(&self, app: &str) -> bool {
match (concrete(app), concrete(&self.conformance.autumn_web)) {
(Some(app), Some(checked)) => same_series(&app, &checked),
_ => false,
}
}
}
#[must_use]
pub fn validate(index: &PluginIndex) -> Vec<Finding> {
let mut findings = Vec::new();
let mut seen = std::collections::HashSet::new();
for listing in &index.plugins {
let mut add = |message: String| {
findings.push(Finding {
plugin: listing.name.clone(),
message,
});
};
if !seen.insert(canonical(&listing.name)) {
add("listed more than once".to_owned());
}
for message in admission_problems(listing) {
add(message);
}
}
findings
}
fn admission_problems(listing: &Listing) -> Vec<String> {
let mut out = Vec::new();
check_text(listing, &mut out);
check_identity(listing, &mut out);
check_versions(listing, &mut out);
check_conformance(listing, &mut out);
check_tier(listing, &mut out);
check_trust(listing, &mut out);
check_routes(listing, &mut out);
out
}
fn check_text(listing: &Listing, out: &mut Vec<String>) {
let fields = [
("name", listing.name.as_str()),
("description", listing.description.as_str()),
("repository", listing.repository.as_str()),
("note", listing.note.as_str()),
("prefix", listing.prefix.as_str()),
("conformance.reason", listing.conformance.reason.as_str()),
];
let lists = listing
.sensitive_routes
.iter()
.map(|route| ("sensitive_routes", route.as_str()))
.chain(
listing
.experimental_surfaces
.iter()
.map(|s| ("experimental_surfaces", s.as_str())),
)
.chain(
listing
.capabilities
.iter()
.map(|c| ("capabilities", c.as_str())),
)
.chain(listing.routes.iter().map(|r| ("routes", r.as_str())))
.chain(
[
&listing.grants.hosts,
&listing.grants.tables,
&listing.grants.job_types,
&listing.grants.slots,
]
.into_iter()
.flatten()
.map(|g| ("grants", g.as_str())),
);
for (field, value) in fields.into_iter().chain(lists) {
if value.chars().any(is_unsafe_char) {
out.push(format!("`{field}` holds a control character"));
}
}
if listing.description.trim().is_empty() {
out.push("`description` is empty".to_owned());
}
if !listing.repository.starts_with("https://") {
out.push("`repository` must be an https:// URL".to_owned());
}
}
fn check_identity(listing: &Listing, out: &mut Vec<String>) {
match listing.origin {
ListingOrigin::FirstParty if super::catalog::lookup(&listing.name).is_none() => {
out.push("origin is first-party, but the CLI catalog has no such crate".to_owned());
}
ListingOrigin::Community if !super::catalog::is_community_name(&listing.name) => {
out.push(format!(
"a community name must be `{}<name>`",
super::catalog::COMMUNITY_PREFIX
));
}
ListingOrigin::FirstParty | ListingOrigin::Community => {}
}
}
fn check_versions(listing: &Listing, out: &mut Vec<String>) {
if semver::Version::parse(&listing.version).is_err() {
out.push(format!(
"`version` {:?} is not a semver version",
listing.version
));
}
match semver::VersionReq::parse(&listing.autumn_web) {
Err(_) => out.push(format!(
"`autumn_web` {:?} is not a Cargo version requirement; declare the supported range",
listing.autumn_web
)),
Ok(req) if req.matches(&semver::Version::new(u64::from(u32::MAX), 0, 0)) => {
out.push(format!(
"`autumn_web` {:?} has no upper bound; name the series you verified",
listing.autumn_web
));
}
Ok(_) => {}
}
if semver::Version::parse(&listing.conformance.autumn_web).is_err() {
out.push(format!(
"`conformance.autumn_web` {:?} is not a semver version",
listing.conformance.autumn_web
));
}
if chrono::NaiveDate::parse_from_str(&listing.conformance.checked, "%Y-%m-%d").is_err() {
out.push(format!(
"`conformance.checked` {:?} is not a YYYY-MM-DD date",
listing.conformance.checked
));
}
}
fn check_conformance(listing: &Listing, out: &mut Vec<String>) {
let result = listing.conformance.result;
match (listing.status, result) {
(Status::Listed, CheckOutcome::Fail) => {
out.push("status is listed, but the last `autumn plugin-check` run failed".to_owned());
}
(Status::Incompatible, CheckOutcome::Pass | CheckOutcome::Exempt) => {
out.push(
"status is incompatible, but the last conformance run did not fail".to_owned(),
);
}
(Status::Delisted, CheckOutcome::Pass | CheckOutcome::Exempt) => {
out.push("status is delisted, but the last conformance run did not fail".to_owned());
}
_ => {}
}
if listing.status != Status::Listed && listing.note.trim().is_empty() {
out.push("a flagged or delisted listing needs a `note` that says why".to_owned());
}
if result == CheckOutcome::Exempt {
if listing.origin != ListingOrigin::FirstParty {
out.push("only a first-party listing can be exempt from plugin-check".to_owned());
}
if listing.conformance.reason.trim().is_empty() {
out.push("an exempt result needs a `conformance.reason`".to_owned());
}
}
let exempt_class =
result == CheckOutcome::Exempt || !listing.conformance.reason.trim().is_empty();
if exempt_class && !is_non_plugin(listing) {
out.push(
"only a first-party crate that is not a `Plugin` (the catalog mounts it with \
something other than `.plugin(`) can be exempt from plugin-check"
.to_owned(),
);
}
}
fn is_non_plugin(listing: &Listing) -> bool {
listing.origin == ListingOrigin::FirstParty
&& super::catalog::lookup(&listing.name).is_some_and(|entry| entry.mount_call != ".plugin(")
}
fn check_tier(listing: &Listing, out: &mut Vec<String>) {
use autumn_web::plugin_contract::{SurfaceTier, surface};
match (listing.tier, listing.experimental_surfaces.is_empty()) {
(Tier::Experimental, true) => {
out.push("tier is experimental, but `experimental_surfaces` is empty".to_owned());
}
(Tier::Stable, false) => out.push(
"`experimental_surfaces` is not empty, so the tier must be experimental".to_owned(),
),
_ => {}
}
for name in &listing.experimental_surfaces {
match surface(name) {
Some(found) if found.tier == SurfaceTier::Experimental => {}
Some(_) => out.push(format!("`{name}` is a stable surface, not experimental")),
None => out.push(format!("`{name}` is not a known plugin surface")),
}
}
}
fn check_routes(listing: &Listing, out: &mut Vec<String>) {
if listing.no_routes && !listing.prefix.is_empty() {
out.push("`no_routes` and a `prefix` contradict each other".to_owned());
}
if listing.trust == Trust::Native
&& !is_non_plugin(listing)
&& listing.prefix.is_empty()
&& !listing.no_routes
{
out.push(
"a native plugin listing needs a `prefix`, or `no_routes = true` for one that mounts \
no routes"
.to_owned(),
);
}
}
fn check_trust(listing: &Listing, out: &mut Vec<String>) {
use autumn_web::plugin_sandbox::SandboxCapability;
match listing.trust {
Trust::Native if !listing.capabilities.is_empty() => out.push(
"a native plugin has full trust; remove `capabilities` or set trust to sandboxed"
.to_owned(),
),
Trust::Sandboxed if listing.capabilities.is_empty() => {
out.push("a sandboxed listing must copy `capabilities` from its manifest".to_owned());
}
Trust::Native | Trust::Sandboxed => {}
}
if listing.trust == Trust::Native && !listing.routes.is_empty() {
out.push(
"`routes` records a sandboxed manifest; a native plugin's routes are checked by \
`plugin-check`"
.to_owned(),
);
}
let digest = &listing.artifact_sha256;
match listing.trust {
Trust::Sandboxed
if digest.len() != 64 || !digest.chars().all(|c| c.is_ascii_hexdigit()) =>
{
out.push(
"a sandboxed listing must record `artifact_sha256` from `autumn plugin inspect`"
.to_owned(),
);
}
Trust::Native if !digest.is_empty() => {
out.push("`artifact_sha256` is for a sandboxed listing only".to_owned());
}
Trust::Native if !listing.grants.is_empty() => {
out.push("`grants` is for a sandboxed listing only".to_owned());
}
Trust::Native if !listing.quotas.is_empty() => {
out.push("`quotas` is for a sandboxed listing only".to_owned());
}
Trust::Native if !listing.limits.is_empty() => {
out.push("`limits` is for a sandboxed listing only".to_owned());
}
Trust::Native | Trust::Sandboxed => {}
}
let known = autumn_web::plugin_sandbox::CapabilityQuotas::default().fields();
for key in listing.quotas.keys() {
if !known.iter().any(|(k, _)| k == key) {
out.push(format!("`{key}` is not a sandbox quota"));
}
}
let sandboxed = listing.trust == Trust::Sandboxed;
let missing: Vec<&str> = known
.iter()
.filter(|(k, _)| sandboxed && !listing.quotas.contains_key(*k))
.map(|(k, _)| *k)
.collect();
if !missing.is_empty() {
out.push(format!("`quotas` is missing {}", missing.join(", ")));
}
let known = autumn_web::plugin_sandbox::ResourceLimits::default().fields();
for key in listing.limits.keys() {
if !known.iter().any(|(k, _)| k == key) {
out.push(format!("`{key}` is not a sandbox resource limit"));
}
}
let missing: Vec<&str> = known
.iter()
.filter(|(k, _)| sandboxed && !listing.limits.contains_key(*k))
.map(|(k, _)| *k)
.collect();
if !missing.is_empty() {
out.push(format!("`limits` is missing {}", missing.join(", ")));
}
for name in &listing.capabilities {
if !SandboxCapability::ALL.iter().any(|c| c.as_str() == name) {
out.push(format!("`{name}` is not a sandbox capability"));
}
}
}
#[must_use]
pub fn staleness(index: &PluginIndex, against: &str) -> Vec<Finding> {
let mut findings = Vec::new();
for listing in &index.plugins {
let mut add = |message: String| {
findings.push(Finding {
plugin: listing.name.clone(),
message,
});
};
let checked = &listing.conformance.autumn_web;
match listing.status {
Status::Delisted => continue,
Status::Incompatible => {
if checked != against {
add(format!(
"flagged since autumn-web {checked}; re-verify against {against} to \
relist it, or delist it"
));
}
continue;
}
Status::Listed => {}
}
if checked != against {
add(format!(
"last verified against autumn-web {checked}; re-verify against {against}"
));
}
let excluded = semver::VersionReq::parse(&listing.autumn_web)
.ok()
.zip(semver::Version::parse(against).ok())
.is_some_and(|(req, version)| {
!req.matches(&semver::Version::new(
version.major,
version.minor,
version.patch,
))
});
if excluded {
add(format!(
"its range `{}` excludes autumn-web {against}; flag it incompatible or widen \
the range",
listing.autumn_web
));
}
if listing.origin == ListingOrigin::FirstParty && listing.version != against {
add(format!(
"first-party version {} is not the release {against}",
listing.version
));
}
}
findings
}
fn concrete(version: &str) -> Option<semver::Version> {
let full = version.trim().trim_start_matches(['=', '^', '~', ' ']);
semver::Version::parse(full)
.ok()
.or_else(|| {
super::install::parse_version(version)
.map(|(major, minor, patch)| semver::Version::new(major, minor, patch))
})
.or_else(|| {
full.parse()
.ok()
.map(|major| semver::Version::new(major, 0, 0))
})
}
fn req_interval(req: &str) -> Option<(semver::Version, Option<semver::Version>)> {
use semver::{Op, Version};
let req = semver::VersionReq::parse(req).ok()?;
let mut low = Version::new(0, 0, 0);
let mut high: Option<Version> = None;
for c in &req.comparators {
if !c.pre.is_empty() {
return None;
}
let (major, minor, patch) = (c.major, c.minor, c.patch);
let base = Version::new(major, minor.unwrap_or(0), patch.unwrap_or(0));
let past = match (minor, patch) {
(Some(minor), Some(patch)) => Version::new(major, minor, patch + 1),
(Some(minor), None) => Version::new(major, minor + 1, 0),
(None, _) => Version::new(major + 1, 0, 0),
};
let caret_top = match (major, minor, patch) {
(0, None, _) => Version::new(1, 0, 0),
(0, Some(0), None) => Version::new(0, 1, 0),
(0, Some(0), Some(patch)) => Version::new(0, 0, patch + 1),
(0, Some(minor), _) => Version::new(0, minor + 1, 0),
_ => Version::new(major + 1, 0, 0),
};
let tilde_top = minor.map_or_else(
|| Version::new(major + 1, 0, 0),
|minor| Version::new(major, minor + 1, 0),
);
let (lo, hi) = match c.op {
Op::Exact | Op::Wildcard => (base, Some(past)),
Op::Greater => (past, None),
Op::GreaterEq => (base, None),
Op::Less => (Version::new(0, 0, 0), Some(base)),
Op::LessEq => (Version::new(0, 0, 0), Some(past)),
Op::Tilde => (base, Some(tilde_top)),
Op::Caret => (base, Some(caret_top)),
_ => return None,
};
low = low.max(lo);
high = match (high, hi) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
};
}
high.as_ref()
.is_none_or(|high| low < *high)
.then_some((low, high))
}
fn range_within(app: &str, range: &str) -> bool {
let (Some((app_low, app_high)), Some((low, high))) = (req_interval(app), req_interval(range))
else {
return false;
};
let high_ok = match (&app_high, &high) {
(_, None) => true,
(None, Some(_)) => false,
(Some(app_high), Some(high)) => app_high <= high,
};
app_low >= low && high_ok
}
fn requirement_top(app: &str, low: &semver::Version) -> Option<semver::Version> {
let app = app.trim_start();
if let Some(rest) = app.strip_prefix('~') {
return Some(if rest.trim().split('.').count() == 1 {
semver::Version::new(low.major, u64::MAX, u64::MAX)
} else {
semver::Version::new(low.major, low.minor, u64::MAX)
});
}
let rest = app.strip_prefix('^')?;
Some(if rest.trim().split('.').count() == 1 {
semver::Version::new(low.major, u64::MAX, u64::MAX)
} else {
series_top(low)
})
}
const fn series_top(v: &semver::Version) -> semver::Version {
if v.major == 0 {
semver::Version::new(0, v.minor, u64::MAX)
} else {
semver::Version::new(v.major, u64::MAX, u64::MAX)
}
}
const fn same_series(a: &semver::Version, b: &semver::Version) -> bool {
if a.major == 0 || b.major == 0 {
a.major == b.major && a.minor == b.minor
} else {
a.major == b.major
}
}
#[must_use]
pub fn check(index: &PluginIndex, against: &str) -> Vec<Finding> {
let mut findings = validate(index);
findings.extend(staleness(index, against));
findings
}
#[cfg(test)]
mod tests {
use super::*;
const RELEASE: &str = env!("CARGO_PKG_VERSION");
fn native(name: &str, origin: ListingOrigin) -> Listing {
Listing {
name: name.to_owned(),
description: "A plugin".to_owned(),
origin,
repository: "https://example.com/repo".to_owned(),
version: "0.7.0".to_owned(),
autumn_web: "0.7".to_owned(),
tier: Tier::Stable,
experimental_surfaces: vec![],
trust: Trust::Native,
capabilities: vec![],
routes: vec![],
artifact_sha256: String::new(),
grants: Grants::default(),
quotas: BTreeMap::new(),
limits: BTreeMap::new(),
status: Status::Listed,
note: String::new(),
prefix: "/audit".to_owned(),
no_routes: false,
sensitive_routes: vec![],
conformance: Conformance {
result: CheckOutcome::Pass,
autumn_web: "0.7.0".to_owned(),
checked: "2026-09-27".to_owned(),
reason: String::new(),
},
}
}
fn community() -> Listing {
native("autumn-plugin-audit", ListingOrigin::Community)
}
fn index_of(plugins: Vec<Listing>) -> PluginIndex {
PluginIndex {
schema: SCHEMA,
plugins,
}
}
fn messages(findings: &[Finding]) -> String {
findings
.iter()
.map(|f| format!("{}: {}", f.plugin, f.message))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn the_bundled_index_passes_the_gate_for_this_release() {
let index = parse(BUNDLED).expect("bundled index parses");
let findings = check(&index, RELEASE);
assert!(findings.is_empty(), "{}", messages(&findings));
}
#[test]
fn the_bundled_index_lists_every_first_party_plugin() {
let index = parse(BUNDLED).expect("bundled index parses");
for entry in super::super::catalog::FIRST_PARTY {
let listing = index
.get(entry.crate_name)
.unwrap_or_else(|| panic!("{} has no listing", entry.crate_name));
assert_eq!(listing.origin, ListingOrigin::FirstParty);
assert_eq!(
listing.description, entry.summary,
"{} description drifted from the catalog",
entry.crate_name
);
}
}
#[test]
fn the_bundled_index_seeds_the_three_named_plugins() {
let index = parse(BUNDLED).expect("bundled index parses");
for name in [
"autumn-admin-plugin",
"autumn-cache-redis",
"autumn-storage-s3",
] {
assert!(index.get(name).is_some(), "{name}");
}
}
#[test]
fn every_bundled_listing_has_a_trust_label() {
let index = parse(BUNDLED).expect("bundled index parses");
for listing in &index.plugins {
let label = listing.trust_label();
assert!(!label.is_empty(), "{}", listing.name);
if listing.trust == Trust::Native {
assert_eq!(label, FULL_TRUST_LABEL);
}
}
}
#[test]
fn parse_rejects_an_unknown_schema() {
let err = parse("schema = 2\n").unwrap_err();
assert_eq!(err, IndexError::Schema(2));
}
#[test]
fn parse_rejects_an_unknown_field_value() {
let src = BUNDLED.replacen("trust = \"native\"", "trust = \"root\"", 1);
assert!(matches!(parse(&src), Err(IndexError::Toml(_))));
}
#[test]
fn parse_rejects_an_unknown_key() {
let src = BUNDLED.replacen(
"tier = \"stable\"",
"tier = \"stable\"\nexperimental_surface = [\"x\"]",
1,
);
assert!(matches!(parse(&src), Err(IndexError::Toml(_))));
let src = BUNDLED.replacen(
"result = \"pass\"",
"result = \"pass\"\nverdict = \"ok\"",
1,
);
assert!(matches!(parse(&src), Err(IndexError::Toml(_))));
}
#[test]
fn parse_accepts_an_empty_index() {
let index = parse("schema = 1\n").expect("empty index");
assert!(index.plugins.is_empty());
}
#[test]
fn load_without_a_path_uses_the_bundled_copy() {
let loaded = load(None).expect("bundled");
assert_eq!(loaded.source, Source::Bundled);
assert!(!loaded.index.plugins.is_empty());
}
#[test]
fn load_reads_an_override_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("index.toml");
std::fs::write(&path, "schema = 1\n").expect("write");
let loaded = load(Some(&path)).expect("override");
assert_eq!(loaded.source, Source::Override(path));
assert!(loaded.index.plugins.is_empty());
}
#[test]
fn load_fails_on_a_missing_override() {
let err = load(Some(Path::new("/nonexistent/autumn-index.toml"))).unwrap_err();
assert!(matches!(err, IndexError::Io(_)), "{err:?}");
}
#[test]
fn visible_hides_delisted_listings() {
let mut gone = community();
gone.name = "autumn-plugin-gone".to_owned();
gone.status = Status::Delisted;
let index = index_of(vec![community(), gone]);
let names: Vec<&str> = index.visible().map(|l| l.name.as_str()).collect();
assert_eq!(names, ["autumn-plugin-audit"]);
assert!(index.get("autumn-plugin-gone").is_some());
}
#[test]
fn a_sandboxed_label_names_its_capabilities() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["http-request".to_owned(), "kv".to_owned()];
assert_eq!(
listing.trust_label(),
"sandboxed: capability manifest grants http-request, kv"
);
}
#[test]
fn a_range_inside_the_listing_is_compatible() {
let listing = community();
assert_eq!(listing.autumn_web, "0.7");
for app in [">=0.7, <0.8", "0.7.*", ">=0.7.2, <0.7.9", ">0.7.0, <=0.7.5"] {
assert_eq!(listing.compat(app), Compat::Compatible, "{app}");
}
for app in [">=0.7, <0.9", ">=0.6, <0.8", ">=0.7", "*", "0.*"] {
assert_eq!(listing.compat(app), Compat::Unknown, "{app}");
}
let mut flagged = community();
flagged.status = Status::Incompatible;
assert_eq!(flagged.compat(">=0.7, <0.8"), Compat::Unknown);
assert_eq!(
listing.compat(">=0.7.0-alpha.1, <=0.7.0-alpha.5"),
Compat::Unknown
);
}
#[test]
fn req_interval_follows_cargo() {
let v = |major, minor, patch| semver::Version::new(major, minor, patch);
for (req, low, high) in [
("0.7", v(0, 7, 0), Some(v(0, 8, 0))),
("^0.0.3", v(0, 0, 3), Some(v(0, 0, 4))),
("~1.2", v(1, 2, 0), Some(v(1, 3, 0))),
("=0.7", v(0, 7, 0), Some(v(0, 8, 0))),
("0.7.*", v(0, 7, 0), Some(v(0, 8, 0))),
(">0.7", v(0, 8, 0), None),
(">0.7.2, <=0.7.5", v(0, 7, 3), Some(v(0, 7, 6))),
("*", v(0, 0, 0), None),
] {
assert_eq!(req_interval(req), Some((low, high)), "{req}");
}
assert_eq!(req_interval(">=0.8, <0.7"), None);
}
#[test]
fn compat_follows_the_declared_range() {
let listing = community();
assert_eq!(listing.compat("0.7.0"), Compat::Compatible);
assert_eq!(listing.compat("^0.7.2"), Compat::Compatible);
assert_eq!(listing.compat("0.6.0"), Compat::Incompatible);
assert_eq!(listing.compat("0.8.0"), Compat::Incompatible);
assert_eq!(listing.compat(">=0.6"), Compat::Unknown);
let mut one_two = community();
one_two.autumn_web = "~1.2".to_owned();
assert_eq!(one_two.compat("~1.2"), Compat::Compatible);
assert_eq!(one_two.compat("^1.2"), Compat::Unknown);
let one = Listing {
autumn_web: "1".to_owned(),
..community()
};
assert_eq!(one.compat("^1"), Compat::Compatible);
assert_eq!(one.compat("~1"), Compat::Compatible);
let below = Listing {
autumn_web: ">=0, <0.8".to_owned(),
..community()
};
assert_eq!(below.compat("^0"), Compat::Unknown);
assert_eq!(one_two.compat("~1"), Compat::Incompatible);
assert_eq!(listing.compat("0.7.0-alpha.1"), Compat::Incompatible);
}
#[test]
fn a_flagged_listing_is_incompatible_from_the_failed_release_on() {
let mut listing = community();
listing.autumn_web = ">=0.6".to_owned();
listing.status = Status::Incompatible;
listing.conformance.result = CheckOutcome::Fail;
listing.conformance.autumn_web = "0.7.0".to_owned();
assert_eq!(listing.compat("0.7.1"), Compat::Incompatible);
assert_eq!(listing.compat("0.8.0"), Compat::Incompatible);
assert_ne!(listing.compat("0.6.0"), Compat::Incompatible);
}
#[test]
fn verified_for_compares_the_series() {
let listing = community();
assert!(listing.verified_for("0.7.3"));
assert!(!listing.verified_for("0.8.0"));
assert!(!listing.verified_for("not a version"));
}
#[test]
fn a_well_formed_index_has_no_findings() {
let index = index_of(vec![community()]);
assert!(
validate(&index).is_empty(),
"{}",
messages(&validate(&index))
);
}
#[test]
fn a_listed_plugin_must_pass_conformance() {
let mut listing = community();
listing.conformance.result = CheckOutcome::Fail;
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("plugin-check"), "{text}");
}
#[test]
fn a_listing_must_declare_a_parseable_range() {
let mut listing = community();
listing.autumn_web = "zero point seven".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("autumn_web"), "{text}");
}
#[test]
fn only_first_party_listings_can_be_exempt() {
let mut listing = community();
listing.conformance.result = CheckOutcome::Exempt;
listing.conformance.reason = "trust me".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("exempt"), "{text}");
}
#[test]
fn an_exemption_needs_a_reason() {
let mut listing = native("autumn-storage-s3", ListingOrigin::FirstParty);
listing.conformance.result = CheckOutcome::Exempt;
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("reason"), "{text}");
}
#[test]
fn a_community_name_must_follow_the_convention() {
let mut listing = community();
listing.name = "serde".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("autumn-plugin-"), "{text}");
}
#[test]
fn a_first_party_listing_must_name_a_catalog_crate() {
let listing = native("autumn-plugin-audit", ListingOrigin::FirstParty);
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("first-party"), "{text}");
}
#[test]
fn duplicate_listings_are_refused() {
let text = messages(&validate(&index_of(vec![community(), community()])));
assert!(text.contains("more than once"), "{text}");
}
#[test]
fn the_tier_must_match_the_declared_surfaces() {
let mut listing = community();
listing.tier = Tier::Experimental;
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("experimental_surfaces"), "{text}");
let mut listing = community();
listing.experimental_surfaces = vec!["no-such-surface".to_owned()];
listing.tier = Tier::Experimental;
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("no-such-surface"), "{text}");
let mut listing = community();
listing.experimental_surfaces = vec![first_experimental_surface()];
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("tier"), "{text}");
}
#[test]
fn an_experimental_listing_with_real_surfaces_is_valid() {
let mut listing = community();
listing.tier = Tier::Experimental;
listing.experimental_surfaces = vec![first_experimental_surface()];
assert!(validate(&index_of(vec![listing])).is_empty());
}
fn first_experimental_surface() -> String {
autumn_web::plugin_contract::experimental_surface_names()
.next()
.expect("at least one experimental surface")
.to_owned()
}
#[test]
fn a_sandboxed_listing_must_name_known_capabilities() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
let text = messages(&validate(&index_of(vec![listing.clone()])));
assert!(text.contains("capabilities"), "{text}");
listing.capabilities = vec!["root-shell".to_owned()];
let text = messages(&validate(&index_of(vec![listing.clone()])));
assert!(text.contains("root-shell"), "{text}");
listing.capabilities = vec!["http-request".to_owned()];
listing.artifact_sha256 = "ab".repeat(32);
let listing = with_full_sandbox_maps(listing);
assert!(validate(&index_of(vec![listing])).is_empty());
}
#[test]
fn grants_are_for_sandboxed_listings_only() {
let mut native = community();
native.grants.hosts = vec!["api.example.com".to_owned()];
let text = messages(&validate(&index_of(vec![native])));
assert!(text.contains("grants"), "{text}");
}
#[test]
fn a_sandboxed_label_names_its_scoped_grants() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["http-outbound".to_owned()];
listing.grants.hosts = vec!["api.example.com".to_owned()];
assert!(
listing.trust_label().contains("hosts api.example.com"),
"{}",
listing.trust_label()
);
}
fn with_full_sandbox_maps(mut listing: Listing) -> Listing {
listing.quotas = autumn_web::plugin_sandbox::CapabilityQuotas::default()
.fields()
.into_iter()
.map(|(k, v)| (k.to_owned(), v))
.collect();
listing.limits = autumn_web::plugin_sandbox::ResourceLimits::default()
.fields()
.into_iter()
.map(|(k, v)| (k.to_owned(), u64::try_from(v).unwrap()))
.collect();
listing
}
#[test]
fn a_sandboxed_listing_needs_complete_quotas_and_limits() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["kv".to_owned()];
listing.artifact_sha256 = "ab".repeat(32);
let text = messages(&validate(&index_of(vec![listing.clone()])));
assert!(text.contains("`quotas` is missing"), "{text}");
assert!(text.contains("`limits` is missing"), "{text}");
let mut full = with_full_sandbox_maps(listing);
assert!(validate(&index_of(vec![full.clone()])).is_empty());
full.limits.remove("fuel");
let text = messages(&validate(&index_of(vec![full])));
assert!(text.contains("fuel"), "{text}");
}
#[test]
fn quotas_are_validated() {
let mut native = community();
native.quotas.insert("kv_reads".to_owned(), 10);
let text = messages(&validate(&index_of(vec![native])));
assert!(text.contains("quotas"), "{text}");
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["kv".to_owned()];
listing.artifact_sha256 = "ab".repeat(32);
listing.quotas.insert("warp_drives".to_owned(), 1);
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("warp_drives"), "{text}");
}
#[test]
fn limits_are_validated_and_labelled() {
let mut native = community();
native.limits.insert("fuel".to_owned(), 1);
let text = messages(&validate(&index_of(vec![native])));
assert!(text.contains("limits"), "{text}");
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["kv".to_owned()];
listing.artifact_sha256 = "ab".repeat(32);
listing.limits.insert("warp".to_owned(), 1);
let text = messages(&validate(&index_of(vec![listing.clone()])));
assert!(text.contains("warp"), "{text}");
listing.limits.clear();
listing.limits.insert("fuel".to_owned(), 7);
assert!(
listing.trust_label().contains("fuel=7"),
"{}",
listing.trust_label()
);
}
#[test]
fn a_sandboxed_label_names_its_non_default_quotas() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["kv".to_owned()];
listing.quotas = autumn_web::plugin_sandbox::CapabilityQuotas::default()
.fields()
.into_iter()
.map(|(k, v)| (k.to_owned(), v))
.collect();
assert!(
!listing.trust_label().contains("quotas"),
"{}",
listing.trust_label()
);
listing.quotas.insert("kv_reads".to_owned(), 99_999);
assert!(
listing.trust_label().contains("kv_reads=99999"),
"{}",
listing.trust_label()
);
}
#[test]
fn a_sandboxed_listing_must_record_its_artifact_digest() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["http-request".to_owned()];
let text = messages(&validate(&index_of(vec![listing.clone()])));
assert!(text.contains("artifact_sha256"), "{text}");
listing.artifact_sha256 = "not hex".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("artifact_sha256"), "{text}");
let mut native = community();
native.artifact_sha256 = "ab".repeat(32);
let text = messages(&validate(&index_of(vec![native])));
assert!(text.contains("artifact_sha256"), "{text}");
}
#[test]
fn a_native_listing_cannot_claim_capabilities() {
let mut listing = community();
listing.capabilities = vec!["kv".to_owned()];
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("native"), "{text}");
}
#[test]
fn a_flagged_listing_needs_a_failed_run_and_a_note() {
let mut listing = community();
listing.status = Status::Incompatible;
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("fail"), "{text}");
assert!(text.contains("note"), "{text}");
}
#[test]
fn control_characters_are_refused() {
let mut listing = community();
listing.description = "safe\u{1b}[2Jevil".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("control"), "{text}");
}
#[test]
fn bidi_and_invisible_characters_are_refused() {
for bad in ["\u{202E}", "\u{2066}", "\u{200B}", "\u{2028}", "\u{FEFF}"] {
let mut listing = community();
listing.note = format!("safe{bad}evil");
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("control"), "{bad:?}: {text}");
}
}
#[test]
fn sanitize_escapes_unsafe_characters() {
assert_eq!(sanitize("a\u{1b}[2Jb\u{202E}c"), "a\\u{1b}[2Jb\\u{202e}c");
assert_eq!(sanitize("plain text"), "plain text");
}
#[test]
fn lookup_ignores_case_and_separator_variants() {
let index = index_of(vec![community()]);
assert!(index.get("autumn_plugin_AUDIT").is_some());
let mut twin = community();
twin.name = "autumn-plugin_audit".to_owned();
let text = messages(&validate(&index_of(vec![community(), twin])));
assert!(text.contains("more than once"), "{text}");
}
#[test]
fn a_range_without_an_upper_bound_is_refused() {
for open in ["*", ">=0.6"] {
let mut listing = community();
listing.autumn_web = open.to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("upper bound"), "{open}: {text}");
}
}
#[test]
fn load_refuses_an_override_that_is_not_a_file() {
let dir = tempfile::tempdir().expect("tempdir");
let err = load(Some(dir.path())).unwrap_err();
assert!(matches!(err, IndexError::Io(_)), "{err:?}");
}
#[test]
fn load_refuses_an_oversized_override() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("big.toml");
let mut text = String::from("schema = 1\n");
text.push_str(&"#".repeat(usize::try_from(MAX_INDEX_BYTES).unwrap() + 1));
std::fs::write(&path, text).expect("write");
let err = load(Some(&path)).unwrap_err();
assert!(matches!(err, IndexError::Io(_)), "{err:?}");
}
#[test]
fn the_repository_must_be_https() {
let mut listing = community();
listing.repository = "http://example.com".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("https"), "{text}");
}
#[test]
fn the_check_date_must_be_a_date() {
let mut listing = community();
listing.conformance.checked = "last tuesday".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("checked"), "{text}");
}
#[test]
fn a_listing_verified_on_this_release_is_fresh() {
let index = index_of(vec![community()]);
assert!(staleness(&index, "0.7.0").is_empty());
}
#[test]
fn a_listing_verified_on_an_older_release_is_stale() {
let index = index_of(vec![community()]);
let text = messages(&staleness(&index, "0.7.1"));
assert!(text.contains("re-verify"), "{text}");
}
#[test]
fn a_prerelease_release_is_covered_by_its_series() {
let mut listing = community();
listing.autumn_web = "0.8".to_owned();
listing.conformance.autumn_web = "0.8.0-rc.1".to_owned();
let text = messages(&staleness(&index_of(vec![listing]), "0.8.0-rc.1"));
assert!(text.is_empty(), "{text}");
}
#[test]
fn a_sandboxed_flag_applies_to_every_app() {
let mut listing = flagged();
listing.trust = Trust::Sandboxed;
listing.conformance.autumn_web = "0.9.0".to_owned();
listing.autumn_web = ">=0.1, <1".to_owned();
assert!(listing.flag_applies("0.2.0"));
assert_eq!(listing.compat("0.2.0"), Compat::Incompatible);
}
#[test]
fn a_flag_applies_from_the_failed_series() {
let mut listing = flagged();
listing.conformance.autumn_web = "0.8.0".to_owned();
assert!(listing.flag_applies("0.8.2"));
assert!(listing.flag_applies("0.9.0"));
assert!(!listing.flag_applies("0.7.0"));
assert!(!community().flag_applies("0.8.0"));
listing.conformance.autumn_web = "1.2.0".to_owned();
assert!(listing.flag_applies("1.1.0"));
assert!(listing.flag_applies("2.0.0"));
assert!(!listing.flag_applies("0.9.0"));
}
#[test]
fn an_unresolved_app_requirement_needs_the_whole_series() {
let mut exact = community();
exact.autumn_web = "=0.7.0".to_owned();
assert_eq!(exact.compat("0.7.0"), Compat::Compatible);
assert_eq!(exact.compat("^0.7.0"), Compat::Unknown);
let caret = community();
assert_eq!(caret.compat("^0.7.0"), Compat::Compatible);
assert_eq!(caret.compat("^0.6.0"), Compat::Incompatible);
}
#[test]
fn a_sandboxed_label_names_its_routes() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["kv".to_owned()];
listing.routes = vec!["GET /hello".to_owned(), "HEAD /hello".to_owned()];
let label = listing.trust_label();
assert!(label.contains("serves GET /hello, HEAD /hello"), "{label}");
}
#[test]
fn a_plugin_listing_cannot_be_exempt() {
let mut admin = native("autumn-admin-plugin", ListingOrigin::FirstParty);
admin.conformance.result = CheckOutcome::Exempt;
admin.conformance.reason = "trust me".to_owned();
let text = messages(&validate(&index_of(vec![admin])));
assert!(text.contains("not a `Plugin`"), "{text}");
let mut s3 = native("autumn-storage-s3", ListingOrigin::FirstParty);
s3.prefix.clear();
s3.conformance.result = CheckOutcome::Exempt;
s3.conformance.reason = "a BlobStore, not a Plugin".to_owned();
assert!(
validate(&index_of(vec![s3])).is_empty(),
"{:?}",
validate(&index_of(vec![]))
);
}
#[test]
fn a_native_listing_needs_a_route_mode() {
let mut listing = community();
listing.prefix.clear();
let text = messages(&validate(&index_of(vec![listing.clone()])));
assert!(text.contains("needs a `prefix`"), "{text}");
listing.no_routes = true;
assert!(validate(&index_of(vec![listing])).is_empty());
}
#[test]
fn a_route_with_a_terminal_control_is_refused() {
let mut listing = community();
listing.trust = Trust::Sandboxed;
listing.capabilities = vec!["kv".to_owned()];
listing.routes = vec!["GET /safe\u{1b}[2J".to_owned()];
let text = messages(&validate(&index_of(vec![listing])));
assert!(
text.contains("`routes` holds a control character"),
"{text}"
);
}
#[test]
fn no_routes_and_a_prefix_contradict() {
let mut listing = community();
listing.no_routes = true;
listing.prefix = "/x".to_owned();
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("no_routes"), "{text}");
}
#[test]
fn a_native_listing_cannot_record_routes() {
let mut listing = community();
listing.routes = vec!["GET /x".to_owned()];
let text = messages(&validate(&index_of(vec![listing])));
assert!(text.contains("`routes`"), "{text}");
}
#[test]
fn a_range_that_excludes_the_release_must_be_flagged() {
let mut listing = community();
listing.conformance.autumn_web = "0.8.0".to_owned();
let text = messages(&staleness(&index_of(vec![listing]), "0.8.0"));
assert!(text.contains("incompatible"), "{text}");
}
#[test]
fn a_first_party_listing_must_carry_the_release_version() {
let mut listing = native("autumn-admin-plugin", ListingOrigin::FirstParty);
listing.version = "0.6.0".to_owned();
let text = messages(&staleness(&index_of(vec![listing]), "0.7.0"));
assert!(text.contains("0.6.0"), "{text}");
}
fn flagged() -> Listing {
let mut listing = community();
listing.status = Status::Incompatible;
listing.conformance.result = CheckOutcome::Fail;
listing.note = "failed on 0.7.0".to_owned();
listing
}
#[test]
fn a_flag_on_this_release_is_fresh() {
assert!(staleness(&index_of(vec![flagged()]), "0.7.0").is_empty());
}
#[test]
fn a_flag_from_an_older_release_must_be_resolved() {
let text = messages(&staleness(&index_of(vec![flagged()]), "0.8.0"));
assert!(text.contains("delist"), "{text}");
}
#[test]
fn bidi_format_marks_are_unsafe() {
for c in [
'\u{061C}', '\u{200E}', '\u{202E}', '\u{2066}', '\u{2069}', '\u{206A}', '\u{206F}',
] {
assert!(is_unsafe_char(c), "U+{:04X}", u32::from(c));
assert!(!sanitize(&format!("a{c}b")).contains(c));
}
assert!(!is_unsafe_char('é'));
for c in [
'\u{00AD}',
'\u{034F}',
'\u{115F}',
'\u{3164}',
'\u{FE0F}',
'\u{E0041}',
] {
assert!(is_unsafe_char(c), "U+{:04X}", u32::from(c));
}
}
#[test]
fn a_delisted_listing_must_have_failed() {
let mut gone = community();
gone.status = Status::Delisted;
gone.note = "removed".to_owned();
let text = messages(&validate(&index_of(vec![gone.clone()])));
assert!(
text.contains("delisted, but the last conformance run did not fail"),
"{text}"
);
gone.conformance.result = CheckOutcome::Fail;
assert!(validate(&index_of(vec![gone])).is_empty());
}
#[test]
fn delisted_listings_are_never_stale() {
let mut gone = flagged();
gone.status = Status::Delisted;
assert!(staleness(&index_of(vec![gone]), "0.9.0").is_empty());
}
}