#![expect(
clippy::redundant_pub_crate,
reason = "explicit pub(crate) documents the crate-wide visibility intent at each item"
)]
use std::path::PathBuf;
use airsl::{GrantSet, InstructionLimit, MemoryLimit, Policy};
use clap::{Parser, Subcommand, ValueEnum};
const UNLIMITED: &str = "none";
#[derive(Debug, Parser)]
#[command(name = "airsl", version, about, long_about = None)]
pub(crate) struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Ceiling<T> {
Unlimited,
Of(T),
}
impl<T> Ceiling<T> {
fn into_limit(self) -> Option<T> {
match self {
Self::Unlimited => None,
Self::Of(limit) => Some(limit),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub(crate) enum PolicyName {
Trusted,
#[default]
Confined,
Pure,
}
impl From<PolicyName> for Policy {
fn from(value: PolicyName) -> Self {
match value {
PolicyName::Trusted => Self::trusted(),
PolicyName::Confined => Self::confined(),
PolicyName::Pure => Self::pure(),
}
}
}
#[derive(Debug, Default, clap::Args)]
pub(crate) struct Grants {
#[arg(long = "allow-read", value_name = "DIR")]
pub read: Vec<PathBuf>,
#[arg(long = "allow-write", value_name = "DIR")]
pub write: Vec<PathBuf>,
#[arg(long = "allow-env", value_name = "NAME")]
pub env: Vec<String>,
#[arg(long = "allow-exec", value_name = "PROGRAM")]
pub exec: Vec<String>,
}
impl Grants {
fn into_grant_set(self) -> GrantSet {
GrantSet::declared()
.with_fs(|fs| {
let fs = self.read.into_iter().fold(fs, airsl::FsGrant::read);
self.write.into_iter().fold(fs, airsl::FsGrant::write)
})
.with_env(|env| env.read(self.env))
.with_proc(|proc| proc.allow(self.exec))
}
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Run {
#[arg(long)]
fail_open: bool,
#[arg(long, value_enum, default_value_t = PolicyName::Confined)]
policy: PolicyName,
#[arg(long, value_name = "BYTES|none", value_parser = parse_memory_limit)]
memory_limit: Option<Ceiling<MemoryLimit>>,
#[arg(long, value_name = "COUNT|none", value_parser = parse_instruction_limit)]
instruction_limit: Option<Ceiling<InstructionLimit>>,
#[command(flatten)]
grants: Grants,
script: PathBuf,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Test {
#[arg(long, value_enum, default_value_t = PolicyName::Confined)]
policy: PolicyName,
#[command(flatten)]
grants: Grants,
#[arg(default_value = ".")]
path: PathBuf,
},
Check {
#[arg(default_value = ".")]
path: PathBuf,
},
Doctor {
#[arg(long, value_enum, default_value_t = PolicyName::Confined)]
policy: PolicyName,
},
}
pub(crate) fn resolve_policy(
preset: PolicyName,
memory: Option<Ceiling<MemoryLimit>>,
instructions: Option<Ceiling<InstructionLimit>>,
grants: Grants,
) -> Policy {
let policy = Policy::from(preset);
let mut limits = *policy.limits();
if let Some(memory) = memory {
limits = limits.with_memory(memory.into_limit());
}
if let Some(instructions) = instructions {
limits = limits.with_instructions(instructions.into_limit());
}
let policy = policy.with_limits(limits);
if policy.grants().is_unrestricted() {
policy
} else {
policy.with_grants(grants.into_grant_set())
}
}
fn parse_memory_limit(raw: &str) -> Result<Ceiling<MemoryLimit>, String> {
if raw == UNLIMITED {
return Ok(Ceiling::Unlimited);
}
raw.parse::<usize>()
.map(|bytes| Ceiling::Of(MemoryLimit::bytes(bytes)))
.map_err(|_| format!("expected a byte count or `{UNLIMITED}`, got `{raw}`"))
}
fn parse_instruction_limit(raw: &str) -> Result<Ceiling<InstructionLimit>, String> {
if raw == UNLIMITED {
return Ok(Ceiling::Unlimited);
}
raw.parse::<u64>()
.map(|count| Ceiling::Of(InstructionLimit::count(count)))
.map_err(|_| format!("expected an instruction count or `{UNLIMITED}`, got `{raw}`"))
}
#[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 = "tests panic to reject an unexpected parse shape; a panic is the intended failure signal"
)]
use super::{
Ceiling, Cli, Command, Grants, PolicyName, parse_instruction_limit, parse_memory_limit,
resolve_policy,
};
use airsl::{InstructionLimit, LanguageSurface, MemoryLimit, Policy};
use clap::Parser as _;
fn parse(args: &[&str]) -> Cli {
Cli::try_parse_from(args).unwrap()
}
fn run_command(args: &[&str]) -> Command {
parse(args).command
}
#[test]
fn run_defaults_to_reporting_errors_and_the_confined_preset() {
let Command::Run {
fail_open,
policy,
memory_limit,
instruction_limit,
script,
args,
..
} = run_command(&["airsl", "run", "hook.lua"])
else {
panic!("expected the run subcommand");
};
assert!(!fail_open);
assert_eq!(policy, PolicyName::Confined);
assert!(memory_limit.is_none());
assert!(instruction_limit.is_none());
assert_eq!(script, std::path::Path::new("hook.lua"));
assert!(args.is_empty());
}
#[test]
fn fail_open_is_opt_in() {
let Command::Run { fail_open, .. } = run_command(&["airsl", "run", "--fail-open", "h.lua"])
else {
panic!("expected the run subcommand");
};
assert!(fail_open);
}
#[test]
fn each_preset_name_selects_its_policy() {
for (name, expected) in [
(PolicyName::Trusted, LanguageSurface::Full),
(PolicyName::Confined, LanguageSurface::Restricted),
(PolicyName::Pure, LanguageSurface::Minimal),
] {
assert_eq!(Policy::from(name).language(), expected, "{name:?}");
}
}
#[test]
fn the_policy_flag_selects_a_preset() {
let Command::Run { policy, .. } =
run_command(&["airsl", "run", "--policy", "trusted", "h.lua"])
else {
panic!("expected the run subcommand");
};
assert_eq!(policy, PolicyName::Trusted);
}
#[test]
fn an_unknown_preset_is_a_usage_error() {
assert!(Cli::try_parse_from(["airsl", "run", "--policy", "wide-open", "h.lua"]).is_err());
}
#[test]
fn a_ceiling_can_be_tightened_or_lifted() {
let Command::Run { memory_limit, .. } =
run_command(&["airsl", "run", "--memory-limit", "4096", "h.lua"])
else {
panic!("expected the run subcommand");
};
assert_eq!(memory_limit, Some(Ceiling::Of(MemoryLimit::bytes(4096))));
let Command::Run { memory_limit, .. } =
run_command(&["airsl", "run", "--memory-limit", "none", "h.lua"])
else {
panic!("expected the run subcommand");
};
assert_eq!(memory_limit, Some(Ceiling::Unlimited));
}
#[test]
fn a_ceiling_that_is_neither_a_number_nor_none_is_a_usage_error() {
assert!(Cli::try_parse_from(["airsl", "run", "--memory-limit", "lots", "h.lua"]).is_err());
assert!(
Cli::try_parse_from(["airsl", "run", "--instruction-limit", "lots", "h.lua"]).is_err()
);
}
#[test]
fn the_ceiling_parsers_accept_a_count_or_the_unlimited_word() {
assert_eq!(parse_memory_limit("none").unwrap(), Ceiling::Unlimited);
assert!(matches!(parse_memory_limit("1").unwrap(), Ceiling::Of(_)));
assert!(parse_memory_limit("-1").is_err());
assert_eq!(parse_instruction_limit("none").unwrap(), Ceiling::Unlimited);
assert!(matches!(
parse_instruction_limit("1").unwrap(),
Ceiling::Of(_)
));
assert!(parse_instruction_limit("").is_err());
}
#[test]
fn trailing_arguments_reach_the_script_untouched() {
let Command::Run { args, .. } =
run_command(&["airsl", "run", "h.lua", "--verbose", "-x", "value"])
else {
panic!("expected the run subcommand");
};
assert_eq!(args, ["--verbose", "-x", "value"]);
}
#[test]
fn doctor_describes_the_confined_preset_by_default() {
let Command::Doctor { policy } = run_command(&["airsl", "doctor"]) else {
panic!("expected the doctor subcommand");
};
assert_eq!(policy, PolicyName::Confined);
}
#[test]
fn doctor_can_describe_another_preset() {
let Command::Doctor { policy } = run_command(&["airsl", "doctor", "--policy", "pure"])
else {
panic!("expected the doctor subcommand");
};
assert_eq!(policy, PolicyName::Pure);
}
#[test]
fn no_override_leaves_the_presets_ceilings_alone() {
let policy = resolve_policy(PolicyName::Confined, None, None, Grants::default());
assert!(policy.limits().memory().is_some());
assert!(policy.limits().instructions().is_some());
}
#[test]
fn an_override_replaces_one_ceiling_and_leaves_the_other() {
let policy = resolve_policy(
PolicyName::Confined,
Some(Ceiling::Of(MemoryLimit::bytes(512))),
None,
Grants::default(),
);
assert_eq!(policy.limits().memory().map(MemoryLimit::get), Some(512));
assert!(policy.limits().instructions().is_some());
}
#[test]
fn an_override_can_lift_a_ceiling_the_preset_imposed() {
let policy = resolve_policy(
PolicyName::Confined,
Some(Ceiling::Unlimited),
Some(Ceiling::Unlimited),
Grants::default(),
);
assert!(policy.limits().memory().is_none());
assert!(policy.limits().instructions().is_none());
}
#[test]
fn an_override_can_impose_a_ceiling_the_preset_lifted() {
let policy = resolve_policy(
PolicyName::Trusted,
None,
Some(Ceiling::Of(InstructionLimit::count(10))),
Grants::default(),
);
assert_eq!(
policy.limits().instructions().map(InstructionLimit::get),
Some(10)
);
assert!(policy.limits().memory().is_none());
}
#[test]
fn a_missing_script_path_is_a_usage_error() {
assert!(Cli::try_parse_from(["airsl", "run"]).is_err());
}
#[test]
fn an_unknown_subcommand_is_a_usage_error() {
assert!(Cli::try_parse_from(["airsl", "frobnicate"]).is_err());
}
}