Skip to main content

sim_lib_lang_javascript/
modules.rs

1//! ECMAScript source modules and dynamic source under explicit authority.
2
3use std::sync::Arc;
4
5use sim_kernel::Symbol;
6use sim_lib_core::{DynamicSourcePolicy, RequestOrigin};
7use sim_lib_namespace::{IdentitySpecifierPolicy, SourceModulePolicy};
8
9/// JavaScript policy over the shared parse/link/evaluate module lifecycle.
10///
11/// The shared loader canonicalizes supplied-root identities, detects cycles,
12/// caches failures, and exposes its default export through a live binding cell.
13pub fn javascript_module_policy() -> SourceModulePolicy {
14    javascript_module_policy_with_codec(Symbol::qualified("codec", "javascript"))
15}
16
17/// Builds JavaScript's module entry for an installed compatible source codec.
18pub fn javascript_module_policy_with_codec(codec: Symbol) -> SourceModulePolicy {
19    SourceModulePolicy::new(codec, Arc::new(IdentitySpecifierPolicy))
20}
21
22/// Builds JavaScript's dynamic-source entry over the shared source policy.
23pub fn dynamic_javascript_policy() -> DynamicSourcePolicy {
24    dynamic_javascript_policy_with_codec(Symbol::qualified("codec", "javascript"))
25}
26
27/// Host-authored root and authority supplied for one ESM resolution.
28pub fn dynamic_javascript_policy_with_codec(codec: Symbol) -> DynamicSourcePolicy {
29    DynamicSourcePolicy::new(
30        codec,
31        RequestOrigin::new(Symbol::qualified("javascript", "dynamic-source")),
32    )
33}
34
35#[cfg(test)]
36mod tests {
37    use std::{collections::BTreeMap, sync::RwLock};
38
39    use sim_codec_lisp::LispCodecLib;
40    use sim_kernel::{
41        CapabilitySet, ClassId, ClassRef, CodecId, Cx, DefaultFactory, Dir, EagerPolicy, Error,
42        Expr, Object, ObjectCompat, ReadPolicy, Result, Table, TrustLevel, Value,
43        read_eval_capability,
44    };
45    use sim_lib_core::SourceAuthority;
46    use sim_lib_namespace::{ModuleResolutionOutcome, module_load_capability};
47    use sim_shape::{AnyShape, ExactExprShape};
48
49    use super::*;
50
51    fn context() -> (Cx, sim_kernel::GrantSeat) {
52        let (mut cx, seat) = Cx::new_seated(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
53        cx.load_lib(&LispCodecLib::new(CodecId(71)).unwrap())
54            .unwrap();
55        (cx, seat)
56    }
57
58    fn trusted() -> ReadPolicy {
59        ReadPolicy {
60            trust: TrustLevel::TrustedSource,
61            capabilities: CapabilitySet::new().grant(read_eval_capability()),
62        }
63    }
64
65    #[derive(Default)]
66    struct MemoryDir(RwLock<BTreeMap<Symbol, Value>>);
67
68    impl MemoryDir {
69        fn source(&self, cx: &mut Cx, name: &str, source: &str) {
70            self.0.write().unwrap().insert(
71                Symbol::new(name),
72                cx.factory().string(source.to_owned()).unwrap(),
73            );
74        }
75    }
76
77    impl Object for MemoryDir {
78        fn display(&self, _cx: &mut Cx) -> Result<String> {
79            Ok("javascript-memory-root".to_owned())
80        }
81        fn as_any(&self) -> &dyn std::any::Any {
82            self
83        }
84    }
85    impl ObjectCompat for MemoryDir {
86        fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
87            cx.factory()
88                .class_stub(ClassId(0), Symbol::qualified("test", "JavascriptRoot"))
89        }
90        fn as_table_impl(&self) -> Option<&dyn Table> {
91            Some(self)
92        }
93        fn as_dir(&self) -> Option<&dyn Dir> {
94            Some(self)
95        }
96    }
97    impl Table for MemoryDir {
98        fn backend_symbol(&self) -> Symbol {
99            Symbol::qualified("test", "javascript-root")
100        }
101        fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
102            self.0
103                .read()
104                .unwrap()
105                .get(&key)
106                .cloned()
107                .map_or_else(|| cx.factory().nil(), Ok)
108        }
109        fn set(&self, _cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
110            self.0.write().unwrap().insert(key, value);
111            Ok(())
112        }
113        fn has(&self, _cx: &mut Cx, key: Symbol) -> Result<bool> {
114            Ok(self.0.read().unwrap().contains_key(&key))
115        }
116        fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
117            self.0
118                .write()
119                .unwrap()
120                .remove(&key)
121                .map_or_else(|| cx.factory().nil(), Ok)
122        }
123        fn keys(&self, _cx: &mut Cx) -> Result<Vec<Symbol>> {
124            Ok(self.0.read().unwrap().keys().cloned().collect())
125        }
126        fn entries(&self, _cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
127            Ok(self
128                .0
129                .read()
130                .unwrap()
131                .iter()
132                .map(|(k, v)| (k.clone(), v.clone()))
133                .collect())
134        }
135        fn len(&self, _cx: &mut Cx) -> Result<usize> {
136            Ok(self.0.read().unwrap().len())
137        }
138        fn clear(&self, _cx: &mut Cx) -> Result<()> {
139            self.0.write().unwrap().clear();
140            Ok(())
141        }
142    }
143    impl Dir for MemoryDir {
144        fn mkdir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Value> {
145            Err(Error::Eval("nested dirs unsupported".into()))
146        }
147        fn opendir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Option<Value>> {
148            Ok(None)
149        }
150        fn rmdir(&self, cx: &mut Cx, _name: Symbol) -> Result<Value> {
151            cx.factory().nil()
152        }
153        fn is_dir(&self, _cx: &mut Cx, _name: Symbol) -> Result<bool> {
154            Ok(false)
155        }
156    }
157
158    fn authority(requires: Vec<sim_kernel::CapabilityName>) -> SourceAuthority {
159        SourceAuthority::new(trusted(), requires, CapabilitySet::new()).unwrap()
160    }
161
162    #[test]
163    fn esm_uses_shared_live_lifecycle_supplied_roots_and_cached_failures() {
164        let (mut cx, seat) = context();
165        seat.grant(&mut cx, read_eval_capability()).unwrap();
166        seat.grant(&mut cx, module_load_capability()).unwrap();
167        let root = Arc::new(MemoryDir::default());
168        root.source(&mut cx, "answer.mjs", "42");
169        root.source(&mut cx, "broken.mjs", "(");
170        let modules = javascript_module_policy_with_codec(Symbol::qualified("codec", "lisp"));
171        let first = modules
172            .load(
173                &mut cx,
174                Symbol::new("modules"),
175                root.clone(),
176                "answer.mjs",
177                authority(vec![module_load_capability()]),
178            )
179            .unwrap();
180        let imported = modules
181            .dynamic_import(
182                &mut cx,
183                Symbol::new("modules"),
184                root.clone(),
185                Some(first.identity().clone()),
186                "./answer.mjs",
187                authority(vec![module_load_capability()]),
188            )
189            .unwrap();
190        assert_eq!(first.identity(), imported.identity());
191        assert_eq!(
192            first
193                .default_export()
194                .get()
195                .unwrap()
196                .object()
197                .display(&mut cx)
198                .unwrap(),
199            "42"
200        );
201        assert!(
202            modules
203                .load(
204                    &mut cx,
205                    Symbol::new("modules"),
206                    root.clone(),
207                    "broken.mjs",
208                    authority(Vec::new())
209                )
210                .is_err()
211        );
212        root.source(&mut cx, "broken.mjs", "41");
213        assert!(
214            modules
215                .load(
216                    &mut cx,
217                    Symbol::new("modules"),
218                    root,
219                    "broken.mjs",
220                    authority(Vec::new())
221                )
222                .is_err()
223        );
224        assert_eq!(
225            modules
226                .receipts()
227                .unwrap()
228                .iter()
229                .map(|r| r.outcome)
230                .collect::<Vec<_>>(),
231            vec![
232                ModuleResolutionOutcome::Linked,
233                ModuleResolutionOutcome::CacheHit,
234                ModuleResolutionOutcome::DecodeFailed,
235                ModuleResolutionOutcome::DecodeFailed,
236            ]
237        );
238    }
239
240    #[test]
241    fn dynamic_source_rejects_ambient_authority() {
242        let (mut cx, seat) = context();
243        let dynamic = dynamic_javascript_policy_with_codec(Symbol::qualified("codec", "lisp"));
244        let denied = SourceAuthority::new(
245            ReadPolicy {
246                trust: TrustLevel::Untrusted,
247                capabilities: CapabilitySet::new(),
248            },
249            Vec::new(),
250            CapabilitySet::new(),
251        )
252        .unwrap_err();
253        assert!(matches!(
254            denied,
255            Error::TrustDenied { .. } | Error::CapabilityDenied { .. }
256        ));
257
258        seat.grant(&mut cx, read_eval_capability()).unwrap();
259        let denied = dynamic
260            .evaluate_text(
261                &mut cx,
262                "42",
263                authority(Vec::new()),
264                Arc::new(ExactExprShape::new(Expr::String("not-42".into()))),
265            )
266            .unwrap_err();
267        assert!(matches!(denied, Error::WrongShape { .. }));
268        let value = dynamic
269            .evaluate_text(&mut cx, "42", authority(Vec::new()), Arc::new(AnyShape))
270            .unwrap();
271        assert_eq!(value.object().display(&mut cx).unwrap(), "42");
272    }
273}