#![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 LimitOverride<T> {
Unlimited,
Of(T),
}
impl<T> LimitOverride<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<LimitOverride<MemoryLimit>>,
#[arg(long, value_name = "COUNT|none", value_parser = parse_instruction_limit)]
instruction_limit: Option<LimitOverride<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,
},
#[command(subcommand)]
Ext(ExtCommand),
}
#[derive(Debug, Default, clap::Args)]
pub(crate) struct ExtFlags {
#[command(flatten)]
pub grants: Grants,
#[arg(long, value_name = "BYTES|none", value_parser = parse_memory_limit)]
pub memory_limit: Option<LimitOverride<MemoryLimit>>,
#[arg(long, value_name = "COUNT|none", value_parser = parse_instruction_limit)]
pub instruction_limit: Option<LimitOverride<InstructionLimit>>,
#[arg(long = "event", value_name = "NAME")]
pub events: Vec<String>,
#[arg(long = "var", value_name = "NAME=VALUE", value_parser = parse_var)]
pub vars: Vec<(String, String)>,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ExtCommand {
Doctor {
dir: PathBuf,
#[command(flatten)]
flags: ExtFlags,
},
Fire {
dir: PathBuf,
event: String,
#[command(flatten)]
flags: ExtFlags,
},
}
fn parse_var(raw: &str) -> Result<(String, String), String> {
raw.split_once('=')
.map(|(name, value)| (name.to_owned(), value.to_owned()))
.ok_or_else(|| format!("expected NAME=VALUE, got `{raw}`"))
}
pub(crate) fn resolve_policy(
preset: PolicyName,
memory: Option<LimitOverride<MemoryLimit>>,
instructions: Option<LimitOverride<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<LimitOverride<MemoryLimit>, String> {
if raw == UNLIMITED {
return Ok(LimitOverride::Unlimited);
}
raw.parse::<usize>()
.map(|bytes| LimitOverride::Of(MemoryLimit::bytes(bytes)))
.map_err(|_| format!("expected a byte count or `{UNLIMITED}`, got `{raw}`"))
}
fn parse_instruction_limit(raw: &str) -> Result<LimitOverride<InstructionLimit>, String> {
if raw == UNLIMITED {
return Ok(LimitOverride::Unlimited);
}
raw.parse::<u64>()
.map(|count| LimitOverride::Of(InstructionLimit::count(count)))
.map_err(|_| format!("expected an instruction count or `{UNLIMITED}`, got `{raw}`"))
}
pub(crate) fn resolve_ceiling(flags: ExtFlags) -> airsl::Result<airsl::extension::Ceiling> {
let policy = resolve_policy(
PolicyName::Confined,
flags.memory_limit,
flags.instruction_limit,
flags.grants,
);
airsl::extension::Ceiling::new(policy)
}
#[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::{
Cli, Command, ExtCommand, ExtFlags, Grants, LimitOverride, PolicyName,
parse_instruction_limit, parse_memory_limit, parse_var, resolve_ceiling, 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(LimitOverride::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(LimitOverride::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(),
LimitOverride::Unlimited
);
assert!(matches!(
parse_memory_limit("1").unwrap(),
LimitOverride::Of(_)
));
assert!(parse_memory_limit("-1").is_err());
assert_eq!(
parse_instruction_limit("none").unwrap(),
LimitOverride::Unlimited
);
assert!(matches!(
parse_instruction_limit("1").unwrap(),
LimitOverride::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(LimitOverride::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(LimitOverride::Unlimited),
Some(LimitOverride::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(LimitOverride::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());
}
#[test]
fn ext_doctor_parses_dir_and_shared_flags() {
let cli = Cli::try_parse_from([
"airsl",
"ext",
"doctor",
"./x",
"--allow-read",
"/data",
"--allow-exec",
"git",
"--memory-limit",
"none",
"--instruction-limit",
"5000",
"--event",
"count",
"--event",
"stop",
"--var",
"APP_HOME=/srv/app",
])
.unwrap();
let Command::Ext(ExtCommand::Doctor { dir, flags }) = cli.command else {
panic!("expected the ext doctor subcommand");
};
assert_eq!(dir, std::path::PathBuf::from("./x"));
assert_eq!(flags.grants.read, [std::path::PathBuf::from("/data")]);
assert_eq!(flags.memory_limit, Some(LimitOverride::Unlimited));
assert_eq!(flags.events, ["count", "stop"]);
assert_eq!(flags.vars, [("APP_HOME".to_owned(), "/srv/app".to_owned())]);
}
#[test]
fn ext_fire_requires_an_event() {
assert!(Cli::try_parse_from(["airsl", "ext", "fire", "./x"]).is_err());
}
#[test]
fn var_rejects_a_missing_equals() {
let err = parse_var("NOEQ").unwrap_err();
assert!(err.contains("NAME=VALUE"), "{err}");
}
#[test]
fn var_splits_on_the_first_equals_only() {
assert_eq!(
parse_var("A=b=c").unwrap(),
("A".to_owned(), "b=c".to_owned())
);
}
#[test]
fn resolve_ceiling_is_confined_plus_flags_and_never_trusted() {
let flags = ExtFlags {
grants: Grants {
read: vec![std::path::PathBuf::from("/d")],
..Grants::default()
},
..ExtFlags::default()
};
let ceiling = resolve_ceiling(flags).unwrap();
assert_eq!(ceiling.policy().language(), LanguageSurface::Restricted);
assert!(
ceiling
.policy()
.grants()
.fs()
.allows_read(std::path::Path::new("/d/f"))
);
}
}