1use sim_kernel::{CapabilityName, Cx, Error, Expr, NumberLiteral, Result, Symbol};
2use std::{
3 collections::BTreeMap,
4 sync::{
5 Arc,
6 atomic::{AtomicBool, Ordering},
7 },
8};
9
10const MAX_BINDINGS: usize = 128;
11const MAX_BINDING_BYTES: usize = 64 * 1024;
12pub fn exec_capability() -> CapabilityName {
14 CapabilityName::new("exec")
15}
16pub fn proc_result_symbol() -> Symbol {
18 Symbol::new("ProcResult")
19}
20
21macro_rules! opaque_ref {
22 ($name:ident, $label:literal) => {
23 #[doc = concat!("Opaque, boot-trusted ", $label, ".")]
24 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
25 pub struct $name(String);
26 impl $name {
27 pub fn new(value: impl Into<String>) -> Result<Self> {
29 let value = value.into();
30 if value.is_empty() || value.contains('\0') {
31 return Err(Error::Eval(
32 concat!($label, " must be non-empty and NUL-free").into(),
33 ));
34 }
35 Ok(Self(value))
36 }
37 #[must_use]
38 pub fn as_str(&self) -> &str {
40 &self.0
41 }
42 }
43 };
44}
45opaque_ref!(ProgramRef, "program reference");
46opaque_ref!(ProjectRootRef, "project-root reference");
47opaque_ref!(PrivateArtifactRef, "private-artifact reference");
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct ArgAtom(String);
52impl ArgAtom {
53 pub fn new(value: impl Into<String>) -> Result<Self> {
55 let value = value.into();
56 if value.contains('\0') {
57 return Err(Error::Eval("argument contains NUL".into()));
58 }
59 Ok(Self(value))
60 }
61 #[must_use]
62 pub fn as_str(&self) -> &str {
64 &self.0
65 }
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum BindingValue {
71 Literal(String),
73 ProjectRoot(ProjectRootRef),
75 PrivateArtifact(PrivateArtifactRef),
77}
78#[derive(Clone, Debug, Default, PartialEq, Eq)]
79pub struct SealedBindings(BTreeMap<String, BindingValue>);
81impl SealedBindings {
82 #[must_use]
84 pub fn empty() -> Self {
85 Self::default()
86 }
87 pub fn try_from_entries(
89 entries: impl IntoIterator<Item = (String, BindingValue)>,
90 ) -> Result<Self> {
91 let mut values = BTreeMap::new();
92 let mut bytes = 0usize;
93 for (name, value) in entries {
94 if name.is_empty() || name.contains(['=', '\0']) {
95 return Err(Error::Eval("sealed binding has an invalid name".into()));
96 }
97 let value_bytes = match &value {
98 BindingValue::Literal(v) => {
99 if v.contains('\0') {
100 return Err(Error::Eval("sealed binding literal contains NUL".into()));
101 }
102 v.len()
103 }
104 BindingValue::ProjectRoot(v) => v.as_str().len(),
105 BindingValue::PrivateArtifact(v) => v.as_str().len(),
106 };
107 bytes = bytes.saturating_add(name.len()).saturating_add(value_bytes);
108 if values.insert(name, value).is_some() {
109 return Err(Error::Eval("duplicate sealed binding".into()));
110 }
111 if values.len() > MAX_BINDINGS || bytes > MAX_BINDING_BYTES {
112 return Err(Error::Eval("sealed bindings exceed bounded size".into()));
113 }
114 }
115 Ok(Self(values))
116 }
117 pub fn literals(entries: impl IntoIterator<Item = (String, String)>) -> Result<Self> {
119 Self::try_from_entries(
120 entries
121 .into_iter()
122 .map(|(k, v)| (k, BindingValue::Literal(v))),
123 )
124 }
125 pub fn iter(&self) -> impl Iterator<Item = (&str, &BindingValue)> {
127 self.0.iter().map(|(k, v)| (k.as_str(), v))
128 }
129}
130
131#[derive(Clone, Debug, PartialEq, Eq)]
132pub struct ProcessBudget {
134 pub timeout_ms: u64,
136 pub max_output_bytes: usize,
138 pub stdin: Option<Vec<u8>>,
140}
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct ExecOptions {
144 pub program: ProgramRef,
146 pub root: ProjectRootRef,
148 pub budget: ProcessBudget,
150 pub environment: SealedBindings,
152 pub private_artifacts: Vec<PrivateArtifactRef>,
154}
155impl ExecOptions {
156 pub fn new(
158 program: ProgramRef,
159 root: ProjectRootRef,
160 timeout_ms: u64,
161 max_output_bytes: usize,
162 ) -> Self {
163 Self {
164 program,
165 root,
166 budget: ProcessBudget {
167 timeout_ms,
168 max_output_bytes,
169 stdin: None,
170 },
171 environment: SealedBindings::empty(),
172 private_artifacts: Vec::new(),
173 }
174 }
175 #[must_use]
176 pub fn with_stdin(mut self, stdin: impl Into<Vec<u8>>) -> Self {
178 self.budget.stdin = Some(stdin.into());
179 self
180 }
181 #[must_use]
182 pub fn with_bindings(mut self, bindings: SealedBindings) -> Self {
184 self.environment = bindings;
185 self
186 }
187 #[must_use]
188 pub fn with_private_artifacts(mut self, artifacts: Vec<PrivateArtifactRef>) -> Self {
190 self.private_artifacts = artifacts;
191 self
192 }
193}
194#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct ProcessRequest {
197 pub program: ProgramRef,
199 pub argv: Vec<ArgAtom>,
201 pub root: ProjectRootRef,
203 pub environment: SealedBindings,
205 pub private_artifacts: Vec<PrivateArtifactRef>,
207 pub budget: ProcessBudget,
209}
210
211#[derive(Clone, Debug, Default)]
212pub struct ProcessCancellation(Arc<AtomicBool>);
214impl ProcessCancellation {
215 pub fn cancel(&self) {
217 self.0.store(true, Ordering::Release)
218 }
219 #[must_use]
220 pub fn is_cancelled(&self) -> bool {
222 self.0.load(Ordering::Acquire)
223 }
224}
225#[derive(Clone, Debug, PartialEq, Eq)]
226pub struct ProcResult {
228 pub stdout: String,
230 pub stderr: String,
232 pub exit_code: i32,
234 pub truncated: bool,
236}
237impl ProcResult {
238 #[must_use]
240 pub fn to_constructor_expr(&self) -> Expr {
241 Expr::Call {
242 operator: Box::new(Expr::Symbol(proc_result_symbol())),
243 args: vec![
244 Expr::String(self.stdout.clone()),
245 Expr::String(self.stderr.clone()),
246 Expr::Number(NumberLiteral {
247 domain: Symbol::qualified("numbers", "i64"),
248 canonical: self.exit_code.to_string(),
249 }),
250 Expr::Bool(self.truncated),
251 ],
252 }
253 }
254}
255#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct ProcessReceipt {
258 pub provider: String,
260 pub elapsed_mono_ns: u64,
262 pub result: ProcResult,
264}
265#[derive(Clone, Debug, PartialEq, Eq)]
266pub struct StopReceipt {
268 pub provider: String,
270 pub elapsed_mono_ns: u64,
272 pub cleanup: String,
274}
275#[derive(Clone, Debug, PartialEq, Eq)]
276pub struct DispatchEvidence {
278 pub provider: String,
280 pub stage: String,
282 pub detail: String,
284}
285#[derive(Clone, Debug, PartialEq, Eq)]
286pub enum ProcessRefusal {
288 Invalid(String),
290 Refused(String),
292 SpawnFailed(String),
294}
295#[derive(Clone, Debug, PartialEq, Eq)]
296pub enum ProcessAttempt {
298 NotDispatched {
300 refusal: ProcessRefusal,
302 },
303 Completed {
305 receipt: ProcessReceipt,
307 },
308 StoppedAfterTimeout {
310 receipt: StopReceipt,
312 },
313 StoppedAfterCancel {
315 receipt: StopReceipt,
317 },
318 UnknownAfterDispatch {
320 evidence: DispatchEvidence,
322 },
323}
324impl ProcessAttempt {
325 #[must_use]
327 pub fn automatically_retryable(&self) -> bool {
328 matches!(self, Self::NotDispatched { .. })
329 }
330}
331pub trait ProcessPort: Send + Sync {
333 fn run(&self, request: &ProcessRequest, cancellation: &ProcessCancellation) -> ProcessAttempt;
335}
336
337pub fn exec(
339 cx: &mut Cx,
340 port: &dyn ProcessPort,
341 argv: &[String],
342 options: &ExecOptions,
343 cancellation: &ProcessCancellation,
344) -> Result<ProcResult> {
345 cx.require(&exec_capability())?;
346 let request = checked_request(argv, options)?;
347 match port.run(&request, cancellation) {
348 ProcessAttempt::Completed { receipt } => Ok(receipt.result),
349 attempt => Err(Error::HostError(format!("exec attempt: {attempt:?}"))),
350 }
351}
352fn checked_request(argv: &[String], options: &ExecOptions) -> Result<ProcessRequest> {
353 if options.budget.timeout_ms == 0 {
354 return Err(Error::Eval("exec requires a non-zero timeout_ms".into()));
355 }
356 if options.budget.max_output_bytes == 0 {
357 return Err(Error::Eval("exec requires a non-zero output budget".into()));
358 }
359 let argv = argv
360 .iter()
361 .cloned()
362 .map(ArgAtom::new)
363 .collect::<Result<Vec<_>>>()?;
364 Ok(ProcessRequest {
365 program: options.program.clone(),
366 argv,
367 root: options.root.clone(),
368 environment: options.environment.clone(),
369 private_artifacts: options.private_artifacts.clone(),
370 budget: options.budget.clone(),
371 })
372}