Skip to main content

sim_run_core/
envelope.rs

1use std::ffi::OsStr;
2
3use sim_kernel::{Cx, Error, Expr, Result as KernelResult, Symbol, Value};
4
5use crate::{CliEnvelope, CliError, source::symbol_from_text};
6
7/// Projects the command envelope into a kernel table value.
8pub fn cli_envelope_value(cx: &mut Cx, envelope: &CliEnvelope) -> Result<Value, CliError> {
9    let codec = cx
10        .factory()
11        .symbol(symbol_from_text(&envelope.codec))
12        .map_err(envelope_error)?;
13    let verb = option_string(cx, envelope.verb.as_deref())?;
14    let args = envelope
15        .args
16        .iter()
17        .map(|arg| {
18            let arg = os_str_text(arg.as_os_str(), "CLI argument")?;
19            cx.factory().string(arg.to_owned()).map_err(envelope_error)
20        })
21        .collect::<Result<Vec<_>, _>>()?;
22    let args = cx.factory().list(args).map_err(envelope_error)?;
23    let eval = option_string(cx, envelope.eval.as_deref())?;
24    let script = option_os_string(
25        cx,
26        envelope.script.as_ref().map(|path| path.as_os_str()),
27        "script path",
28    )?;
29    let stdin = option_string(cx, envelope.stdin.as_deref())?;
30
31    cx.factory()
32        .table(vec![
33            (Symbol::new("codec"), codec),
34            (Symbol::new("verb"), verb),
35            (Symbol::new("args"), args),
36            (Symbol::new("eval"), eval),
37            (Symbol::new("script"), script),
38            (Symbol::new("stdin"), stdin),
39        ])
40        .map_err(envelope_error)
41}
42
43/// Extracts the UTF-8 payload arguments from a generic CLI envelope value.
44///
45/// Loaded command libraries use this instead of defining product-specific
46/// projections of the bootloader-owned envelope table.
47pub fn cli_envelope_args(cx: &mut Cx, envelope: &Value) -> KernelResult<Vec<String>> {
48    let Some(table) = envelope.object().as_table_impl() else {
49        return Err(Error::Eval("CLI envelope is not a table".to_owned()));
50    };
51    let value = table.get(cx, Symbol::new("args"))?;
52    let Expr::List(items) = value.object().as_expr(cx)? else {
53        return Err(Error::TypeMismatch {
54            expected: "argument list",
55            found: "non-list",
56        });
57    };
58    items
59        .into_iter()
60        .map(|item| match item {
61            Expr::String(value) => Ok(value),
62            _ => Err(Error::TypeMismatch {
63                expected: "string argument",
64                found: "non-string",
65            }),
66        })
67        .collect()
68}
69
70fn option_string(cx: &mut Cx, value: Option<&str>) -> Result<Value, CliError> {
71    match value {
72        Some(value) => cx
73            .factory()
74            .string(value.to_owned())
75            .map_err(envelope_error),
76        None => cx.factory().nil().map_err(envelope_error),
77    }
78}
79
80fn option_os_string(cx: &mut Cx, value: Option<&OsStr>, context: &str) -> Result<Value, CliError> {
81    match value {
82        Some(value) => cx
83            .factory()
84            .string(os_str_text(value, context)?.to_owned())
85            .map_err(envelope_error),
86        None => cx.factory().nil().map_err(envelope_error),
87    }
88}
89
90fn os_str_text<'a>(value: &'a OsStr, context: &str) -> Result<&'a str, CliError> {
91    value
92        .to_str()
93        .ok_or_else(|| CliError::new(format!("{context} requires UTF-8 text")))
94}
95
96fn envelope_error(err: sim_kernel::Error) -> CliError {
97    CliError::new(format!("build CLI envelope value: {err}"))
98}
99
100#[cfg(test)]
101mod tests {
102    use sim_kernel::testing::bare_cx as cx;
103
104    use super::*;
105    use crate::CliEnvelope;
106
107    #[cfg(unix)]
108    #[test]
109    fn envelope_rejects_non_utf8_script_path() {
110        use std::{ffi::OsString, os::unix::ffi::OsStringExt, path::PathBuf};
111
112        let mut cx = cx();
113        let envelope = CliEnvelope {
114            codec: "codec/lisp".to_owned(),
115            verb: None,
116            args: Vec::new(),
117            eval: None,
118            script: Some(PathBuf::from(OsString::from_vec(
119                b"/tmp/sim-run-\xff-script.sim".to_vec(),
120            ))),
121            stdin: None,
122        };
123
124        let err = cli_envelope_value(&mut cx, &envelope).unwrap_err();
125
126        assert_eq!(err.to_string(), "script path requires UTF-8 text");
127    }
128}