Skip to main content

sim_run_core/
source.rs

1use std::{ffi::OsString, fmt, path::PathBuf, str::FromStr};
2
3use crate::{CliError, CratesIoSpec};
4use sim_kernel::{Datum, LibSourceSpec as KernelLibSourceSpec, Symbol};
5
6/// Library source syntax accepted by the command line.
7///
8/// Each variant maps to a `kind:value` spelling parsed via [`FromStr`].
9///
10/// # Examples
11///
12/// ```
13/// use sim_run_core::LibSourceSpec;
14///
15/// assert_eq!(
16///     "symbol:codec/lisp".parse::<LibSourceSpec>().unwrap(),
17///     LibSourceSpec::Symbol("codec/lisp".to_owned()),
18/// );
19/// assert!("codec/lisp".parse::<LibSourceSpec>().is_err());
20/// ```
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum LibSourceSpec {
23    /// A `symbol:NAME` source resolved through the kernel loader.
24    Symbol(String),
25    /// A `path:PATH` source read from the local filesystem.
26    Path(PathBuf),
27    /// A `url:URL` source fetched from a remote location.
28    Url(String),
29    /// A `bytes:TEXT` source holding inline library bytes.
30    Bytes(Vec<u8>),
31    /// An open loader-defined source carried as opaque kernel data.
32    Open {
33        /// Loader-defined source kind.
34        kind: Symbol,
35        /// Opaque payload interpreted by the loader that claims `kind`.
36        payload: Datum,
37    },
38    /// A `host:NAME` source provided by the host environment.
39    Host(String),
40    /// A `crates.io:NAME@REQ` source resolved outside the kernel.
41    CratesIo(CratesIoSpec),
42}
43
44impl LibSourceSpec {
45    pub(crate) fn to_kernel_data_source(&self) -> Option<KernelLibSourceSpec> {
46        match self {
47            Self::Symbol(symbol) => Some(KernelLibSourceSpec::Symbol(symbol_from_text(symbol))),
48            Self::Path(path) => Some(sim_run_loaders::path_source_spec(path.clone())),
49            Self::Url(url) => Some(sim_run_loaders::url_source_spec(url.clone())),
50            Self::Bytes(bytes) => Some(sim_run_loaders::bytes_source_spec(bytes.clone())),
51            Self::Open { kind, payload } => Some(KernelLibSourceSpec::Open {
52                kind: kind.clone(),
53                payload: payload.clone(),
54            }),
55            Self::Host(_) | Self::CratesIo(_) => None,
56        }
57    }
58
59    pub(crate) fn from_kernel_data_source(source: KernelLibSourceSpec) -> Self {
60        match source {
61            KernelLibSourceSpec::Symbol(symbol) => Self::Symbol(symbol.to_string()),
62            KernelLibSourceSpec::Open { kind, payload }
63                if kind == sim_run_loaders::path_source_kind() =>
64            {
65                sim_run_loaders::path_from_payload(&payload)
66                    .map(Self::Path)
67                    .unwrap_or(Self::Open { kind, payload })
68            }
69            KernelLibSourceSpec::Open { kind, payload }
70                if kind == sim_run_loaders::url_source_kind() =>
71            {
72                sim_run_loaders::url_from_payload(&payload)
73                    .map(Self::Url)
74                    .unwrap_or(Self::Open { kind, payload })
75            }
76            KernelLibSourceSpec::Open { kind, payload }
77                if kind == sim_run_loaders::bytes_source_kind() =>
78            {
79                sim_run_loaders::bytes_from_payload(&payload)
80                    .map(Self::Bytes)
81                    .unwrap_or(Self::Open { kind, payload })
82            }
83            KernelLibSourceSpec::Open { kind, payload } => Self::Open { kind, payload },
84        }
85    }
86}
87
88pub(crate) fn parse_source_os(source: OsString) -> Result<LibSourceSpec, CliError> {
89    #[cfg(unix)]
90    {
91        use std::os::unix::ffi::{OsStrExt, OsStringExt};
92
93        let bytes = source.as_os_str().as_bytes();
94        if let Some(rest) = bytes.strip_prefix(b"path:") {
95            if rest.is_empty() {
96                return Err(CliError::new("path: source value is empty"));
97            }
98            return Ok(LibSourceSpec::Path(PathBuf::from(OsString::from_vec(
99                rest.to_vec(),
100            ))));
101        }
102    }
103
104    let source = source
105        .into_string()
106        .map_err(|_| CliError::new("non-UTF-8 library source requires path:"))?;
107    source.parse()
108}
109
110impl fmt::Display for LibSourceSpec {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Self::Symbol(symbol) => write!(f, "symbol:{symbol}"),
114            Self::Path(path) => write!(f, "path:{}", path.display()),
115            Self::Url(url) => write!(f, "url:{url}"),
116            Self::Bytes(bytes) => write!(f, "bytes:{} bytes", bytes.len()),
117            Self::Open { kind, .. } => write!(f, "open:{kind}"),
118            Self::Host(name) => write!(f, "host:{name}"),
119            Self::CratesIo(spec) => write!(f, "crates.io:{spec}"),
120        }
121    }
122}
123
124pub(crate) fn symbol_from_text(text: &str) -> Symbol {
125    match text.split_once('/') {
126        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
127            Symbol::qualified(namespace.to_owned(), name.to_owned())
128        }
129        _ => Symbol::new(text.to_owned()),
130    }
131}
132
133impl FromStr for LibSourceSpec {
134    type Err = CliError;
135
136    fn from_str(source: &str) -> Result<Self, Self::Err> {
137        let Some((kind, rest)) = source.split_once(':') else {
138            return Err(CliError::new("library source must use kind:value syntax"));
139        };
140        if rest.is_empty() {
141            return Err(CliError::new(format!("{kind}: source value is empty")));
142        }
143        match kind {
144            "symbol" => Ok(Self::Symbol(rest.to_owned())),
145            "path" => Ok(Self::Path(PathBuf::from(rest))),
146            "url" => Ok(Self::Url(rest.to_owned())),
147            "bytes" => Ok(Self::Bytes(rest.as_bytes().to_vec())),
148            "host" => Ok(Self::Host(rest.to_owned())),
149            "crates.io" => Ok(Self::CratesIo(rest.parse::<CratesIoSpec>()?)),
150            _ => Err(CliError::new(format!(
151                "unsupported library source kind: {kind}"
152            ))),
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    #[cfg(unix)]
161    use std::os::unix::ffi::{OsStrExt, OsStringExt};
162
163    #[test]
164    fn parses_supported_source_specs() {
165        assert_eq!(
166            "symbol:codec/lisp".parse::<LibSourceSpec>().unwrap(),
167            LibSourceSpec::Symbol("codec/lisp".to_owned())
168        );
169        assert_eq!(
170            "path:./libs/demo.wasm".parse::<LibSourceSpec>().unwrap(),
171            LibSourceSpec::Path(PathBuf::from("./libs/demo.wasm"))
172        );
173        assert_eq!(
174            "url:https://example.invalid/demo.wasm"
175                .parse::<LibSourceSpec>()
176                .unwrap(),
177            LibSourceSpec::Url("https://example.invalid/demo.wasm".to_owned())
178        );
179        assert_eq!(
180            "bytes:abc".parse::<LibSourceSpec>().unwrap(),
181            LibSourceSpec::Bytes(b"abc".to_vec())
182        );
183        assert_eq!(
184            "host:test/demo".parse::<LibSourceSpec>().unwrap(),
185            LibSourceSpec::Host("test/demo".to_owned())
186        );
187        assert_eq!(
188            "crates.io:sim-codec-lisp@0.1.0"
189                .parse::<LibSourceSpec>()
190                .unwrap(),
191            LibSourceSpec::CratesIo("sim-codec-lisp@0.1.0".parse().unwrap())
192        );
193    }
194
195    #[test]
196    fn source_parse_errors_are_typed() {
197        assert_eq!(
198            "codec/lisp"
199                .parse::<LibSourceSpec>()
200                .unwrap_err()
201                .to_string(),
202            "library source must use kind:value syntax"
203        );
204        assert_eq!(
205            "symbol:".parse::<LibSourceSpec>().unwrap_err().to_string(),
206            "symbol: source value is empty"
207        );
208        assert_eq!(
209            "crate:sim"
210                .parse::<LibSourceSpec>()
211                .unwrap_err()
212                .to_string(),
213            "unsupported library source kind: crate"
214        );
215    }
216
217    #[cfg(unix)]
218    #[test]
219    fn source_path_os_bytes_survive_non_utf8() {
220        let parsed = parse_source_os(OsString::from_vec(
221            b"path:/tmp/sim-run-\xff-provider.so".to_vec(),
222        ))
223        .unwrap();
224
225        let LibSourceSpec::Path(path) = parsed else {
226            panic!("expected path source");
227        };
228        assert_eq!(
229            path.as_os_str().as_bytes(),
230            b"/tmp/sim-run-\xff-provider.so"
231        );
232    }
233
234    #[cfg(unix)]
235    #[test]
236    fn non_utf8_text_source_fails_closed() {
237        let err = parse_source_os(OsString::from_vec(b"symbol:codec/\xff".to_vec())).unwrap_err();
238
239        assert_eq!(err.to_string(), "non-UTF-8 library source requires path:");
240    }
241}