use sim_kernel::{CapabilityName, Cx, Error, Expr, NumberLiteral, Result, Symbol};
use std::{
collections::BTreeMap,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
const MAX_BINDINGS: usize = 128;
const MAX_BINDING_BYTES: usize = 64 * 1024;
pub fn exec_capability() -> CapabilityName {
CapabilityName::new("exec")
}
pub fn proc_result_symbol() -> Symbol {
Symbol::new("ProcResult")
}
macro_rules! opaque_ref {
($name:ident, $label:literal) => {
#[doc = concat!("Opaque, boot-trusted ", $label, ".")]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self> {
let value = value.into();
if value.is_empty() || value.contains('\0') {
return Err(Error::Eval(
concat!($label, " must be non-empty and NUL-free").into(),
));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
};
}
opaque_ref!(ProgramRef, "program reference");
opaque_ref!(ProjectRootRef, "project-root reference");
opaque_ref!(PrivateArtifactRef, "private-artifact reference");
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArgAtom(String);
impl ArgAtom {
pub fn new(value: impl Into<String>) -> Result<Self> {
let value = value.into();
if value.contains('\0') {
return Err(Error::Eval("argument contains NUL".into()));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BindingValue {
Literal(String),
ProjectRoot(ProjectRootRef),
PrivateArtifact(PrivateArtifactRef),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SealedBindings(BTreeMap<String, BindingValue>);
impl SealedBindings {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
pub fn try_from_entries(
entries: impl IntoIterator<Item = (String, BindingValue)>,
) -> Result<Self> {
let mut values = BTreeMap::new();
let mut bytes = 0usize;
for (name, value) in entries {
if name.is_empty() || name.contains(['=', '\0']) {
return Err(Error::Eval("sealed binding has an invalid name".into()));
}
let value_bytes = match &value {
BindingValue::Literal(v) => {
if v.contains('\0') {
return Err(Error::Eval("sealed binding literal contains NUL".into()));
}
v.len()
}
BindingValue::ProjectRoot(v) => v.as_str().len(),
BindingValue::PrivateArtifact(v) => v.as_str().len(),
};
bytes = bytes.saturating_add(name.len()).saturating_add(value_bytes);
if values.insert(name, value).is_some() {
return Err(Error::Eval("duplicate sealed binding".into()));
}
if values.len() > MAX_BINDINGS || bytes > MAX_BINDING_BYTES {
return Err(Error::Eval("sealed bindings exceed bounded size".into()));
}
}
Ok(Self(values))
}
pub fn literals(entries: impl IntoIterator<Item = (String, String)>) -> Result<Self> {
Self::try_from_entries(
entries
.into_iter()
.map(|(k, v)| (k, BindingValue::Literal(v))),
)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &BindingValue)> {
self.0.iter().map(|(k, v)| (k.as_str(), v))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProcessBudget {
pub timeout_ms: u64,
pub max_output_bytes: usize,
pub stdin: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExecOptions {
pub program: ProgramRef,
pub root: ProjectRootRef,
pub budget: ProcessBudget,
pub environment: SealedBindings,
pub private_artifacts: Vec<PrivateArtifactRef>,
}
impl ExecOptions {
pub fn new(
program: ProgramRef,
root: ProjectRootRef,
timeout_ms: u64,
max_output_bytes: usize,
) -> Self {
Self {
program,
root,
budget: ProcessBudget {
timeout_ms,
max_output_bytes,
stdin: None,
},
environment: SealedBindings::empty(),
private_artifacts: Vec::new(),
}
}
#[must_use]
pub fn with_stdin(mut self, stdin: impl Into<Vec<u8>>) -> Self {
self.budget.stdin = Some(stdin.into());
self
}
#[must_use]
pub fn with_bindings(mut self, bindings: SealedBindings) -> Self {
self.environment = bindings;
self
}
#[must_use]
pub fn with_private_artifacts(mut self, artifacts: Vec<PrivateArtifactRef>) -> Self {
self.private_artifacts = artifacts;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProcessRequest {
pub program: ProgramRef,
pub argv: Vec<ArgAtom>,
pub root: ProjectRootRef,
pub environment: SealedBindings,
pub private_artifacts: Vec<PrivateArtifactRef>,
pub budget: ProcessBudget,
}
#[derive(Clone, Debug, Default)]
pub struct ProcessCancellation(Arc<AtomicBool>);
impl ProcessCancellation {
pub fn cancel(&self) {
self.0.store(true, Ordering::Release)
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProcResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub truncated: bool,
}
impl ProcResult {
#[must_use]
pub fn to_constructor_expr(&self) -> Expr {
Expr::Call {
operator: Box::new(Expr::Symbol(proc_result_symbol())),
args: vec![
Expr::String(self.stdout.clone()),
Expr::String(self.stderr.clone()),
Expr::Number(NumberLiteral {
domain: Symbol::qualified("numbers", "i64"),
canonical: self.exit_code.to_string(),
}),
Expr::Bool(self.truncated),
],
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProcessReceipt {
pub provider: String,
pub elapsed_mono_ns: u64,
pub result: ProcResult,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StopReceipt {
pub provider: String,
pub elapsed_mono_ns: u64,
pub cleanup: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DispatchEvidence {
pub provider: String,
pub stage: String,
pub detail: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProcessRefusal {
Invalid(String),
Refused(String),
SpawnFailed(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProcessAttempt {
NotDispatched {
refusal: ProcessRefusal,
},
Completed {
receipt: ProcessReceipt,
},
StoppedAfterTimeout {
receipt: StopReceipt,
},
StoppedAfterCancel {
receipt: StopReceipt,
},
UnknownAfterDispatch {
evidence: DispatchEvidence,
},
}
impl ProcessAttempt {
#[must_use]
pub fn automatically_retryable(&self) -> bool {
matches!(self, Self::NotDispatched { .. })
}
}
pub trait ProcessPort: Send + Sync {
fn run(&self, request: &ProcessRequest, cancellation: &ProcessCancellation) -> ProcessAttempt;
}
pub fn exec(
cx: &mut Cx,
port: &dyn ProcessPort,
argv: &[String],
options: &ExecOptions,
cancellation: &ProcessCancellation,
) -> Result<ProcResult> {
cx.require(&exec_capability())?;
let request = checked_request(argv, options)?;
match port.run(&request, cancellation) {
ProcessAttempt::Completed { receipt } => Ok(receipt.result),
attempt => Err(Error::HostError(format!("exec attempt: {attempt:?}"))),
}
}
fn checked_request(argv: &[String], options: &ExecOptions) -> Result<ProcessRequest> {
if options.budget.timeout_ms == 0 {
return Err(Error::Eval("exec requires a non-zero timeout_ms".into()));
}
if options.budget.max_output_bytes == 0 {
return Err(Error::Eval("exec requires a non-zero output budget".into()));
}
let argv = argv
.iter()
.cloned()
.map(ArgAtom::new)
.collect::<Result<Vec<_>>>()?;
Ok(ProcessRequest {
program: options.program.clone(),
argv,
root: options.root.clone(),
environment: options.environment.clone(),
private_artifacts: options.private_artifacts.clone(),
budget: options.budget.clone(),
})
}