1use std::{ffi::OsString, fmt, path::PathBuf, str::FromStr};
2
3use crate::{CliError, CratesIoSpec};
4use sim_kernel::{Datum, LibSourceSpec as KernelLibSourceSpec, Symbol};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum LibSourceSpec {
23 Symbol(String),
25 Path(PathBuf),
27 Url(String),
29 Bytes(Vec<u8>),
31 Open {
33 kind: Symbol,
35 payload: Datum,
37 },
38 Host(String),
40 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}