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
7pub 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
43pub 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}