#![forbid(unsafe_code)]
use serde_json::Value;
pub mod ast;
mod builtins;
mod dispatch;
mod interp;
pub mod lexer;
pub mod limits;
pub mod parser;
pub mod value;
pub use limits::{
DEFAULT_ALLOW_CLOCK, DEFAULT_MAX_CAPABILITY_CALLS, DEFAULT_MAX_OUTPUT_BYTES,
DEFAULT_MAX_OUTPUT_LINES, DEFAULT_MAX_RECURSION_DEPTH, DEFAULT_MAX_STEPS,
DEFAULT_MAX_VALUE_BYTES, DEFAULT_TIMEOUT, Limits,
};
pub use parser::ParseError;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapabilityDescription {
pub capability: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Clone, Debug, PartialEq)]
pub enum CapabilityCallResult {
Succeeded(Value),
Denied {
reason: String,
},
Failed {
error: String,
},
NotFound,
}
pub trait CapabilityInvoker {
fn granted(&self) -> Vec<String>;
fn is_granted(&self, capability: &str) -> bool {
self.granted().iter().any(|granted| granted == capability)
}
fn describe(&self, capability: &str) -> Option<CapabilityDescription> {
let _ = capability;
None
}
fn invoke(&self, capability: &str, input: Value) -> CapabilityCallResult;
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ExitCode(u8);
impl ExitCode {
pub const SUCCESS: Self = Self(0);
pub const FAILURE: Self = Self(1);
pub const SYNTAX: Self = Self(2);
pub const TIMEOUT: Self = Self(124);
pub const DENIED: Self = Self(126);
pub const NOT_FOUND: Self = Self(127);
#[must_use]
pub fn from_script_exit(status: i64) -> Self {
Self(u8::try_from(status.rem_euclid(256)).unwrap_or(0))
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
#[must_use]
pub const fn from_capability_result(result: &CapabilityCallResult) -> Self {
match result {
CapabilityCallResult::Succeeded(_) => Self::SUCCESS,
CapabilityCallResult::Failed { .. } => Self::FAILURE,
CapabilityCallResult::Denied { .. } => Self::DENIED,
CapabilityCallResult::NotFound => Self::NOT_FOUND,
}
}
}
impl From<ExitCode> for u8 {
fn from(code: ExitCode) -> Self {
code.0
}
}
impl From<u8> for ExitCode {
fn from(code: u8) -> Self {
Self(code)
}
}
impl std::fmt::Display for ExitCode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ScriptOutcome {
pub output: String,
pub exit_code: ExitCode,
pub truncated: bool,
pub capability_calls: u32,
pub steps: u64,
}
#[derive(Clone, Debug, Default)]
pub struct Interpreter {
limits: Limits,
curl_capability: Option<String>,
}
impl Interpreter {
#[must_use]
pub fn new(limits: Limits) -> Self {
Self {
limits,
curl_capability: None,
}
}
#[must_use]
pub fn with_curl_capability(mut self, capability: Option<String>) -> Self {
self.curl_capability = capability;
self
}
#[must_use]
pub fn limits(&self) -> Limits {
self.limits
}
pub fn run(&self, script: &str, invoker: &dyn CapabilityInvoker) -> ScriptOutcome {
interp::run(
script,
invoker,
self.limits,
self.curl_capability.as_deref(),
)
}
}
pub fn run(script: &str, invoker: &dyn CapabilityInvoker) -> ScriptOutcome {
Interpreter::new(Limits::default()).run(script, invoker)
}
#[cfg(test)]
mod tests {
use super::{CapabilityCallResult, ExitCode};
#[test]
fn exit_codes_follow_the_documented_mapping() {
assert_eq!(ExitCode::SUCCESS.get(), 0);
assert_eq!(ExitCode::FAILURE.get(), 1);
assert_eq!(ExitCode::SYNTAX.get(), 2);
assert_eq!(ExitCode::TIMEOUT.get(), 124);
assert_eq!(ExitCode::DENIED.get(), 126);
assert_eq!(ExitCode::NOT_FOUND.get(), 127);
}
#[test]
fn capability_results_map_onto_distinct_codes() {
assert_eq!(
ExitCode::from_capability_result(&CapabilityCallResult::Succeeded(
serde_json::Value::Null
)),
ExitCode::SUCCESS
);
assert_eq!(
ExitCode::from_capability_result(&CapabilityCallResult::Failed {
error: "boom".to_owned()
}),
ExitCode::FAILURE
);
assert_eq!(
ExitCode::from_capability_result(&CapabilityCallResult::Denied {
reason: "policy".to_owned()
}),
ExitCode::DENIED
);
assert_eq!(
ExitCode::from_capability_result(&CapabilityCallResult::NotFound),
ExitCode::NOT_FOUND
);
}
#[test]
fn script_exit_wraps_like_bash() {
assert_eq!(ExitCode::from_script_exit(0).get(), 0);
assert_eq!(ExitCode::from_script_exit(7).get(), 7);
assert_eq!(ExitCode::from_script_exit(256).get(), 0);
assert_eq!(ExitCode::from_script_exit(257).get(), 1);
assert_eq!(ExitCode::from_script_exit(-1).get(), 255);
}
}