#![expect(
clippy::redundant_pub_crate,
reason = "explicit pub(crate) documents the crate-wide visibility intent at each item"
)]
use std::path::Path;
use airsl::{Engine, FailurePolicy, Policy, Script};
const DEBUG_VAR: &str = "AIRSL_DEBUG";
pub(crate) fn run(script: &Path, args: &[String], failure: FailurePolicy, policy: &Policy) -> i32 {
match execute(script, args, policy) {
Ok(()) => 0,
Err(error) => {
let breached = error.exhausted_limit().is_some();
if !failure.swallows_errors() || breached || std::env::var_os(DEBUG_VAR).is_some() {
eprintln!("airsl: {error}");
}
failure.exit_code()
}
}
}
fn execute(script: &Path, args: &[String], policy: &Policy) -> airsl::Result<()> {
let engine = Engine::builder().policy(policy.clone()).build()?;
let script = Script::from_file(script)?.with_args(args.iter().map(String::as_str));
engine.eval(&script)
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests unwrap known-valid fixtures; a panic is the intended failure signal"
)]
use super::run;
use airsl::{FailurePolicy, InstructionLimit, Policy, ResourceLimits};
use std::io::Write as _;
fn script(body: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("s.lua");
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(body.as_bytes()).unwrap();
(dir, path)
}
#[test]
fn a_successful_script_exits_zero() {
let (_dir, path) = script("local x = 1");
assert_eq!(
run(&path, &[], FailurePolicy::Report, &Policy::confined()),
0
);
}
#[test]
fn a_failing_script_exits_nonzero_when_reporting() {
let (_dir, path) = script("error('boom')");
assert_ne!(
run(&path, &[], FailurePolicy::Report, &Policy::confined()),
0
);
}
#[test]
fn a_failing_script_exits_zero_when_failing_open() {
let (_dir, path) = script("error('boom')");
assert_eq!(
run(&path, &[], FailurePolicy::FailOpen, &Policy::confined()),
0
);
}
#[test]
fn a_syntax_error_also_fails_open() {
let (_dir, path) = script("this is not lua");
assert_eq!(
run(&path, &[], FailurePolicy::FailOpen, &Policy::confined()),
0
);
}
#[test]
fn a_missing_script_fails_open_rather_than_blocking() {
let missing = std::path::Path::new("/nonexistent/s.lua");
assert_eq!(
run(missing, &[], FailurePolicy::FailOpen, &Policy::confined()),
0
);
assert_ne!(
run(missing, &[], FailurePolicy::Report, &Policy::confined()),
0
);
}
#[test]
fn arguments_arrive_in_the_global_arg_table() {
let (_dir, path) = script("assert(arg[1] == 'one'); assert(arg[2] == 'two')");
let args = [String::from("one"), String::from("two")];
assert_eq!(
run(&path, &args, FailurePolicy::Report, &Policy::confined()),
0
);
}
#[test]
fn a_confined_run_cannot_reach_io() {
let (_dir, path) = script("assert(io == nil)");
assert_eq!(
run(&path, &[], FailurePolicy::Report, &Policy::confined()),
0
);
}
#[test]
fn a_runaway_script_fails_open_without_blocking_the_caller() {
let (_dir, path) = script("while true do end");
let policy = Policy::confined().with_limits(
ResourceLimits::none().with_instructions(Some(InstructionLimit::count(100_000))),
);
assert_eq!(run(&path, &[], FailurePolicy::FailOpen, &policy), 0);
assert_ne!(run(&path, &[], FailurePolicy::Report, &policy), 0);
}
#[test]
fn a_trusted_run_can_reach_io() {
let (_dir, path) = script("assert(type(io) == 'table')");
assert_eq!(
run(&path, &[], FailurePolicy::Report, &Policy::trusted()),
0
);
}
}