1use std::sync::Arc;
4
5use sim_kernel::{
6 CapabilityName, CapabilitySet, Cx, Dir, ReadPolicy, Result, Shape, Symbol, Value,
7};
8use sim_lib_core::{ReadEvalBroker, ReadEvalRequest, ReadEvalSource, RequestOrigin};
9use sim_lib_namespace::{
10 ModuleIdentity, ModuleInstance, ModuleLoader, ModuleRequest, ModuleResolutionReceipt,
11};
12use sim_shape::AnyShape;
13
14pub struct JavascriptModulePolicy {
19 loader: ModuleLoader,
20 codec: Symbol,
21}
22
23impl Default for JavascriptModulePolicy {
24 fn default() -> Self {
25 Self::with_codec(Symbol::qualified("codec", "javascript"))
26 }
27}
28
29impl JavascriptModulePolicy {
30 pub fn with_codec(codec: Symbol) -> Self {
32 Self {
33 loader: ModuleLoader::new(),
34 codec,
35 }
36 }
37
38 pub fn load(
40 &self,
41 cx: &mut Cx,
42 specifier: impl Into<String>,
43 admission: JavascriptModuleAdmission,
44 ) -> Result<ModuleInstance> {
45 self.load_from(cx, None, specifier, admission)
46 }
47
48 pub fn load_from(
50 &self,
51 cx: &mut Cx,
52 importer: Option<ModuleIdentity>,
53 specifier: impl Into<String>,
54 admission: JavascriptModuleAdmission,
55 ) -> Result<ModuleInstance> {
56 self.loader.load(
57 cx,
58 ModuleRequest {
59 root_id: admission.root_id,
60 root: admission.root,
61 importer,
62 specifier: specifier.into(),
63 codec: self.codec.clone(),
64 read_policy: admission.read_policy,
65 requires: admission.requires,
66 allow: admission.allow,
67 },
68 )
69 }
70
71 pub fn dynamic_import(
73 &self,
74 cx: &mut Cx,
75 importer: Option<ModuleIdentity>,
76 specifier: impl Into<String>,
77 admission: JavascriptModuleAdmission,
78 ) -> Result<ModuleInstance> {
79 self.load_from(cx, importer, specifier, admission)
80 }
81
82 pub fn receipts(&self) -> Result<Vec<ModuleResolutionReceipt>> {
84 self.loader.receipts()
85 }
86}
87
88pub struct JavascriptModuleAdmission {
90 pub root_id: Symbol,
92 pub root: Arc<dyn Dir>,
94 pub read_policy: ReadPolicy,
96 pub requires: Vec<CapabilityName>,
98 pub allow: CapabilitySet,
100}
101
102pub struct DynamicJavascript {
104 broker: ReadEvalBroker,
105 codec: Symbol,
106}
107
108impl Default for DynamicJavascript {
109 fn default() -> Self {
110 Self::with_codec(Symbol::qualified("codec", "javascript"))
111 }
112}
113
114impl DynamicJavascript {
115 pub fn with_codec(codec: Symbol) -> Self {
117 Self {
118 broker: ReadEvalBroker::new(),
119 codec,
120 }
121 }
122
123 pub fn evaluate(
125 &self,
126 cx: &mut Cx,
127 source: impl Into<String>,
128 admission: JavascriptDynamicAdmission,
129 ) -> Result<Value> {
130 self.broker.admit(
131 cx,
132 ReadEvalRequest {
133 origin: RequestOrigin::new(Symbol::qualified("javascript", "dynamic-source")),
134 codec: self.codec.clone(),
135 source: ReadEvalSource::Text(source.into()),
136 read_policy: admission.read_policy,
137 requires: admission.requires,
138 allow: admission.allow,
139 expected_shape: admission.expected_shape,
140 },
141 )
142 }
143}
144
145pub struct JavascriptDynamicAdmission {
147 pub read_policy: ReadPolicy,
149 pub requires: Vec<CapabilityName>,
151 pub allow: CapabilitySet,
153 pub expected_shape: Arc<dyn Shape>,
155}
156
157impl JavascriptDynamicAdmission {
158 pub fn new(read_policy: ReadPolicy) -> Self {
160 Self {
161 read_policy,
162 requires: Vec::new(),
163 allow: CapabilitySet::new(),
164 expected_shape: Arc::new(AnyShape),
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use std::{collections::BTreeMap, sync::RwLock};
172
173 use sim_codec_lisp::LispCodecLib;
174 use sim_kernel::{
175 ClassId, ClassRef, CodecId, DefaultFactory, EagerPolicy, Error, Object, ObjectCompat,
176 Table, TrustLevel, read_eval_capability,
177 };
178 use sim_lib_namespace::{ModuleResolutionOutcome, module_load_capability};
179
180 use super::*;
181
182 fn context() -> (Cx, sim_kernel::GrantSeat) {
183 let (mut cx, seat) = Cx::new_seated(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
184 cx.load_lib(&LispCodecLib::new(CodecId(71)).unwrap())
185 .unwrap();
186 (cx, seat)
187 }
188
189 fn trusted() -> ReadPolicy {
190 ReadPolicy {
191 trust: TrustLevel::TrustedSource,
192 capabilities: CapabilitySet::new().grant(read_eval_capability()),
193 }
194 }
195
196 #[derive(Default)]
197 struct MemoryDir(RwLock<BTreeMap<Symbol, Value>>);
198
199 impl MemoryDir {
200 fn source(&self, cx: &mut Cx, name: &str, source: &str) {
201 self.0.write().unwrap().insert(
202 Symbol::new(name),
203 cx.factory().string(source.to_owned()).unwrap(),
204 );
205 }
206 }
207
208 impl Object for MemoryDir {
209 fn display(&self, _cx: &mut Cx) -> Result<String> {
210 Ok("javascript-memory-root".to_owned())
211 }
212 fn as_any(&self) -> &dyn std::any::Any {
213 self
214 }
215 }
216 impl ObjectCompat for MemoryDir {
217 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
218 cx.factory()
219 .class_stub(ClassId(0), Symbol::qualified("test", "JavascriptRoot"))
220 }
221 fn as_table_impl(&self) -> Option<&dyn Table> {
222 Some(self)
223 }
224 fn as_dir(&self) -> Option<&dyn Dir> {
225 Some(self)
226 }
227 }
228 impl Table for MemoryDir {
229 fn backend_symbol(&self) -> Symbol {
230 Symbol::qualified("test", "javascript-root")
231 }
232 fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
233 self.0
234 .read()
235 .unwrap()
236 .get(&key)
237 .cloned()
238 .map_or_else(|| cx.factory().nil(), Ok)
239 }
240 fn set(&self, _cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
241 self.0.write().unwrap().insert(key, value);
242 Ok(())
243 }
244 fn has(&self, _cx: &mut Cx, key: Symbol) -> Result<bool> {
245 Ok(self.0.read().unwrap().contains_key(&key))
246 }
247 fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
248 self.0
249 .write()
250 .unwrap()
251 .remove(&key)
252 .map_or_else(|| cx.factory().nil(), Ok)
253 }
254 fn keys(&self, _cx: &mut Cx) -> Result<Vec<Symbol>> {
255 Ok(self.0.read().unwrap().keys().cloned().collect())
256 }
257 fn entries(&self, _cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
258 Ok(self
259 .0
260 .read()
261 .unwrap()
262 .iter()
263 .map(|(k, v)| (k.clone(), v.clone()))
264 .collect())
265 }
266 fn len(&self, _cx: &mut Cx) -> Result<usize> {
267 Ok(self.0.read().unwrap().len())
268 }
269 fn clear(&self, _cx: &mut Cx) -> Result<()> {
270 self.0.write().unwrap().clear();
271 Ok(())
272 }
273 }
274 impl Dir for MemoryDir {
275 fn mkdir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Value> {
276 Err(Error::Eval("nested dirs unsupported".into()))
277 }
278 fn opendir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Option<Value>> {
279 Ok(None)
280 }
281 fn rmdir(&self, cx: &mut Cx, _name: Symbol) -> Result<Value> {
282 cx.factory().nil()
283 }
284 fn is_dir(&self, _cx: &mut Cx, _name: Symbol) -> Result<bool> {
285 Ok(false)
286 }
287 }
288
289 fn admission(root: Arc<MemoryDir>) -> JavascriptModuleAdmission {
290 JavascriptModuleAdmission {
291 root_id: Symbol::new("modules"),
292 root,
293 read_policy: trusted(),
294 requires: vec![module_load_capability()],
295 allow: CapabilitySet::new(),
296 }
297 }
298
299 #[test]
300 fn esm_uses_shared_live_lifecycle_supplied_roots_and_cached_failures() {
301 let (mut cx, seat) = context();
302 seat.grant(&mut cx, read_eval_capability()).unwrap();
303 seat.grant(&mut cx, module_load_capability()).unwrap();
304 let root = Arc::new(MemoryDir::default());
305 root.source(&mut cx, "answer.mjs", "42");
306 root.source(&mut cx, "broken.mjs", "(");
307 let modules = JavascriptModulePolicy::with_codec(Symbol::qualified("codec", "lisp"));
308 let first = modules
309 .load(&mut cx, "answer.mjs", admission(root.clone()))
310 .unwrap();
311 let imported = modules
312 .dynamic_import(&mut cx, None, "answer.mjs", admission(root.clone()))
313 .unwrap();
314 assert_eq!(first.identity(), imported.identity());
315 assert_eq!(
316 first
317 .default_export()
318 .get()
319 .unwrap()
320 .object()
321 .display(&mut cx)
322 .unwrap(),
323 "42"
324 );
325 assert!(
326 modules
327 .load(&mut cx, "broken.mjs", admission(root.clone()))
328 .is_err()
329 );
330 root.source(&mut cx, "broken.mjs", "41");
331 assert!(
332 modules
333 .load(&mut cx, "broken.mjs", admission(root))
334 .is_err()
335 );
336 assert_eq!(
337 modules
338 .receipts()
339 .unwrap()
340 .iter()
341 .map(|r| r.outcome)
342 .collect::<Vec<_>>(),
343 vec![
344 ModuleResolutionOutcome::Linked,
345 ModuleResolutionOutcome::CacheHit,
346 ModuleResolutionOutcome::Failed,
347 ModuleResolutionOutcome::Failed,
348 ]
349 );
350 }
351
352 #[test]
353 fn dynamic_source_rejects_ambient_authority() {
354 let (mut cx, _seat) = context();
355 let dynamic = DynamicJavascript::with_codec(Symbol::qualified("codec", "lisp"));
356 let denied = dynamic
357 .evaluate(
358 &mut cx,
359 "42",
360 JavascriptDynamicAdmission::new(ReadPolicy {
361 trust: TrustLevel::Untrusted,
362 capabilities: CapabilitySet::new(),
363 }),
364 )
365 .unwrap_err();
366 assert!(matches!(
367 denied,
368 Error::TrustDenied { .. } | Error::CapabilityDenied { .. }
369 ));
370 }
371}