use std::path::Path;
use crate::extension::manifest::Manifest;
use crate::extension::negotiate::Negotiation;
#[derive(Debug, Clone, Copy)]
pub struct ApprovalRequest<'a> {
dir: &'a Path,
manifest: &'a Manifest,
negotiation: &'a Negotiation,
}
impl<'a> ApprovalRequest<'a> {
#[must_use]
pub const fn new(dir: &'a Path, manifest: &'a Manifest, negotiation: &'a Negotiation) -> Self {
Self {
dir,
manifest,
negotiation,
}
}
#[must_use]
pub const fn dir(&self) -> &'a Path {
self.dir
}
#[must_use]
pub const fn manifest(&self) -> &'a Manifest {
self.manifest
}
#[must_use]
pub const fn negotiation(&self) -> &'a Negotiation {
self.negotiation
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Approve,
Deny(String),
}
pub trait Approver: Send + Sync {
fn decide(&self, request: &ApprovalRequest<'_>) -> Decision;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ManifestApprover;
impl Approver for ManifestApprover {
fn decide(&self, request: &ApprovalRequest<'_>) -> Decision {
if request.negotiation().is_satisfied() {
Decision::Approve
} else {
let denials: Vec<String> = request
.negotiation()
.denied()
.iter()
.map(ToString::to_string)
.collect();
Decision::Deny(denials.join("; "))
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DenyAll;
impl Approver for DenyAll {
fn decide(&self, _request: &ApprovalRequest<'_>) -> Decision {
Decision::Deny(String::from("extensions are disabled"))
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests unwrap known-valid fixtures; a panic is the intended failure signal"
)]
#![expect(
clippy::panic,
reason = "a wrong Decision variant is the intended failure signal"
)]
use super::{ApprovalRequest, Approver, Decision, DenyAll, ManifestApprover};
use crate::Policy;
use crate::extension::ceiling::Ceiling;
use crate::extension::manifest::{MANIFEST_FILE, Manifest};
use crate::extension::negotiate::negotiate;
use crate::extension::variables::Variables;
use crate::modules::stdlib;
fn fixture(capabilities: &str) -> (tempfile::TempDir, Manifest) {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("main.lua"), "return 1").unwrap();
std::fs::write(
dir.path().join(MANIFEST_FILE),
format!(
"[extension]\nname='t'\nversion='1'\nentry='main.lua'\napi=1\n[capabilities]\n{capabilities}\n"
),
)
.unwrap();
let m = Manifest::from_dir(dir.path(), &Variables::none()).unwrap();
(dir, m)
}
#[test]
fn the_manifest_approver_approves_a_satisfied_negotiation() {
let (dir, m) = fixture("regex = true");
let n = negotiate(
&m,
&Ceiling::new(Policy::confined()).unwrap(),
&stdlib().unwrap(),
);
let req = ApprovalRequest::new(dir.path(), &m, &n);
assert_eq!(ManifestApprover.decide(&req), Decision::Approve);
}
#[test]
fn the_manifest_approver_denies_listing_every_denial() {
let (dir, m) = fixture("proc.run=['git']\nenv.read=['X']");
let n = negotiate(
&m,
&Ceiling::new(Policy::confined()).unwrap(),
&stdlib().unwrap(),
);
let req = ApprovalRequest::new(dir.path(), &m, &n);
let Decision::Deny(reason) = ManifestApprover.decide(&req) else {
panic!("expected Deny");
};
assert!(
reason.contains("proc.run `git`") && reason.contains("env.read `X`"),
"{reason}"
);
}
#[test]
fn deny_all_denies_even_a_satisfied_negotiation() {
let (dir, m) = fixture("");
let n = negotiate(
&m,
&Ceiling::new(Policy::confined()).unwrap(),
&stdlib().unwrap(),
);
let req = ApprovalRequest::new(dir.path(), &m, &n);
assert_eq!(
DenyAll.decide(&req),
Decision::Deny(String::from("extensions are disabled"))
);
}
#[test]
fn the_request_exposes_the_directory_for_an_interactive_approver() {
let (dir, m) = fixture("");
let n = negotiate(
&m,
&Ceiling::new(Policy::confined()).unwrap(),
&stdlib().unwrap(),
);
let req = ApprovalRequest::new(dir.path(), &m, &n);
assert_eq!(req.dir(), dir.path());
assert_eq!(req.manifest().name().as_str(), "t");
}
}