char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: prepare typed ops and static/live bootstrap711e4d5

main
17.5 KiB520 linesraw
1use std::cell::Cell;
2use std::collections::HashMap;
3use std::io;
4use std::rc::Rc;
5
6use avril_core::{Error, Extension, ModuleLoader, ModuleSource, Op, Runtime, RuntimeOptions};
7
8#[derive(Default)]
9struct Loader {
10    sources: HashMap<&'static str, ModuleSource>,
11    resolves: Cell<usize>,
12    loads: Cell<usize>,
13}
14impl ModuleLoader for Loader {
15    fn resolve(&self, specifier: &str, _: &str) -> Result<String, Error> {
16        self.resolves.set(self.resolves.get() + 1);
17        Ok(if specifier == "private-alias" {
18            "ext:test/util.js"
19        } else {
20            specifier
21        }
22        .into())
23    }
24    fn load(&self, name: &str) -> Result<ModuleSource, Error> {
25        self.loads.set(self.loads.get() + 1);
26        self.sources
27            .get(name)
28            .cloned()
29            .ok_or_else(|| Error::Module(format!("Missing {name}")))
30    }
31}
32
33fn extension() -> Extension {
34    Extension {
35        name: "ext:test",
36        dependencies: &[],
37        ops: vec![Op::asynchronous("echo", |value: u32| async move {
38            Ok::<_, io::Error>(value)
39        })],
40        modules: &[
41            (
42                "ext:test/main.js",
43                r#"
44                import {value} from './nested/../util.js';
45                import {primordials,ops} from 'ext:core';
46                import {echo} from 'ext:core/ops';
47                import {ArrayPrototypePush} from 'ext:core/primordials';
48                if(!Object.isFrozen(primordials)||!Object.isFrozen(ops)||echo!==ops.echo)throw Error('Captured exports');
49                globalThis.boot=[await echo(value)];ArrayPrototypePush(boot,import.meta.url,import.meta.main);
50                globalThis.extensionDynamic=()=>import('./util.js');
51            "#,
52            ),
53            ("ext:test/util.js", "export const value=42;"),
54        ],
55        entry_point: Some("ext:test/main.js"),
56    }
57}
58
59#[test]
60fn extension_modules_have_relative_imports_captured_exports_and_top_level_await() {
61    let mut runtime = Runtime::new(RuntimeOptions {
62        extensions: vec![extension()],
63        ..Default::default()
64    })
65    .unwrap();
66    let boot: serde_json::Value = runtime.execute_script("check", "boot").unwrap();
67    assert_eq!(boot, serde_json::json!([42, "ext:test/main.js", false]));
68    runtime.execute_script::<()>("guest",r#"extensionDynamic().then(()=>{throw Error('Allowed extension dynamic import');},e=>{if(!e.message.includes('not yet implemented'))throw e;});"#).unwrap();
69    runtime.run_event_loop().unwrap();
70}
71
72#[test]
73fn extension_module_can_be_registered_as_guest_builtin() {
74    let loader = Rc::new(Loader {
75        sources: HashMap::from([(
76            "guest",
77            "import {greeting, greet} from 'avril:greeter'; globalThis.result = `${greeting}|${greet('Avril')}`;".into(),
78        )]),
79        ..Default::default()
80    });
81    let mut runtime = Runtime::new(RuntimeOptions {
82        extensions: vec![Extension {
83            name: "ext:greeter",
84            dependencies: &[],
85            ops: vec![],
86            modules: &[(
87                "ext:greeter/main.js",
88                "globalThis.greeterRuns = (globalThis.greeterRuns ?? 0) + 1; export const greeting = 'hello'; export function greet(name) { return `${greeting}, ${name}!`; }",
89            )],
90            entry_point: None,
91        }],
92        module_loader: Some(loader),
93    })
94    .unwrap();
95    assert!(
96        runtime
97            .execute_script::<bool>("check", "globalThis.greeterRuns === undefined")
98            .unwrap()
99    );
100    runtime
101        .register_extension_module("avril:greeter", "ext:greeter/main.js")
102        .unwrap();
103    runtime.execute_module("guest").unwrap();
104    assert_eq!(
105        runtime.execute_script::<String>("check", "result").unwrap(),
106        "hello|hello, Avril!"
107    );
108    assert_eq!(
109        runtime
110            .execute_script::<u32>("check", "greeterRuns")
111            .unwrap(),
112        1
113    );
114    assert!(
115        runtime
116            .register_extension_module("avril:greeter", "ext:greeter/main.js")
117            .is_err()
118    );
119    assert!(
120        runtime
121            .register_extension_module("ext:greeter", "ext:greeter/main.js")
122            .is_err()
123    );
124    assert!(
125        runtime
126            .register_extension_module("avril:missing", "ext:missing/main.js")
127            .is_err()
128    );
129    assert_eq!(
130        runtime
131            .execute_script::<u32>("check", "greeterRuns")
132            .unwrap(),
133        1
134    );
135}
136
137#[test]
138fn static_guest_imports_cannot_access_cached_internal_modules_or_bootstrap() {
139    for specifier in [
140        "ext:test/util.js",
141        "ext:test/main.js",
142        "ext:core",
143        "ext:core/ops",
144        "ext:core/event_hooks",
145        "ext:core/primordials",
146        "ext:core/promises",
147        "ext:core/bootstrap",
148        "ext:core/stack_trace.js",
149        "ext:core/source_map",
150        "ext:unknown",
151    ] {
152        let source = format!("import {{value}} from '{specifier}';");
153        let loader = Rc::new(Loader {
154            sources: HashMap::from([("guest", source.into())]),
155            ..Default::default()
156        });
157        let mut runtime = Runtime::new(RuntimeOptions {
158            extensions: vec![extension()],
159            module_loader: Some(loader.clone()),
160        })
161        .unwrap();
162        assert!(
163            runtime
164                .execute_module("guest")
165                .unwrap_err()
166                .to_string()
167                .contains("private")
168        );
169        assert_eq!(loader.resolves.get(), 0);
170        assert_eq!(loader.loads.get(), 1);
171    }
172}
173
174#[test]
175fn dynamic_imports_and_guest_loader_aliases_cannot_reach_internal_modules() {
176    let loader = Rc::new(Loader {
177        sources: HashMap::from([("static-alias", "import 'private-alias';".into())]),
178        ..Default::default()
179    });
180    let mut runtime = Runtime::new(RuntimeOptions {
181        extensions: vec![extension()],
182        module_loader: Some(loader.clone()),
183    })
184    .unwrap();
185    runtime.execute_script::<()>("ext:test/main.js",r#"
186        const opsSymbol = Symbol.for('__avril.ops');
187        if ('mapSourceLocation' in globalThis || opsSymbol in globalThis || Reflect.ownKeys(globalThis).includes(opsSymbol)) throw Error('Public native registry');
188        globalThis.denied=0;
189        for(const name of ['ext:test/util.js','ext:core','ext:core/stack_trace.js','ext:core/source_map','ext:unknown','private-alias']) {
190            import(name).then(()=>{throw Error('Leaked internal module');},e=>{if(!e.message.includes('private')||e.message.includes('not yet implemented'))throw e;denied++;});
191        }
192    "#).unwrap();
193    runtime.run_event_loop().unwrap();
194    assert_eq!(
195        runtime.execute_script::<usize>("check", "denied").unwrap(),
196        6
197    );
198    assert!(
199        runtime
200            .execute_module("static-alias")
201            .unwrap_err()
202            .to_string()
203            .contains("private")
204    );
205    assert_eq!(loader.loads.get(), 1);
206}
207
208#[test]
209fn private_entry_names_and_public_builtin_registration_are_rejected() {
210    let loader = Rc::new(Loader {
211        sources: HashMap::from([("guest", "globalThis.isMain=import.meta.main;".into())]),
212        ..Default::default()
213    });
214    let mut runtime = Runtime::new(RuntimeOptions {
215        extensions: vec![extension()],
216        module_loader: Some(loader.clone()),
217    })
218    .unwrap();
219    assert!(
220        runtime
221            .execute_module("ext:test/main.js")
222            .unwrap_err()
223            .to_string()
224            .contains("private")
225    );
226    assert!(
227        runtime
228            .register_module("ext:core", |scope| Some(avril_core::v8::Object::new(scope)))
229            .unwrap_err()
230            .to_string()
231            .contains("private")
232    );
233    runtime.execute_module("guest").unwrap();
234    assert!(runtime.execute_script::<bool>("check", "isMain").unwrap());
235    assert_eq!(loader.loads.get(), 1);
236}
237
238#[test]
239fn extension_cycles_share_module_instances_and_initialization_runs_once() {
240    let extensions = vec![
241        Extension {
242            name: "ext:cycle",
243            dependencies: &[],
244            ops: vec![],
245            modules: &[
246                (
247                    "ext:cycle/main.js",
248                    "import {sum} from './a.js';globalThis.total=sum();",
249                ),
250                (
251                    "ext:cycle/a.js",
252                    "import {b} from './b.js';export const a=20;export function sum(){return a+b;}globalThis.aRuns=(globalThis.aRuns??0)+1;",
253                ),
254                (
255                    "ext:cycle/b.js",
256                    "import {a} from './a.js';export const b=22;export function readA(){return a;}",
257                ),
258            ],
259            entry_point: Some("ext:cycle/main.js"),
260        },
261        Extension {
262            name: "ext:consumer",
263            dependencies: &[],
264            ops: vec![],
265            modules: &[(
266                "ext:consumer/main.js",
267                "import {a} from 'ext:cycle/a.js';globalThis.again=a;",
268            )],
269            entry_point: Some("ext:consumer/main.js"),
270        },
271    ];
272    let mut runtime = Runtime::new(RuntimeOptions { extensions, ..Default::default() }).unwrap();
273    assert_eq!(
274        runtime
275            .execute_script::<Vec<u32>>("check", "[total,again,aRuns]")
276            .unwrap(),
277        [42, 20, 1]
278    );
279}
280
281#[test]
282fn extension_entry_points_run_after_dependencies() {
283    let mut runtime = Runtime::new(RuntimeOptions {
284        extensions: vec![
285            Extension {
286                name: "ext:dependent",
287                dependencies: &["ext:dependency"],
288                ops: vec![],
289                modules: &[(
290                    "ext:dependent/main.js",
291                    "globalThis.order.push('dependent');",
292                )],
293                entry_point: Some("ext:dependent/main.js"),
294            },
295            Extension {
296                name: "ext:dependency",
297                dependencies: &[],
298                ops: vec![],
299                modules: &[(
300                    "ext:dependency/main.js",
301                    "globalThis.order = ['dependency'];",
302                )],
303                entry_point: Some("ext:dependency/main.js"),
304            },
305        ],
306        ..Default::default()
307    })
308    .unwrap();
309    assert_eq!(
310        runtime
311            .execute_script::<Vec<String>>("check", "order")
312            .unwrap(),
313        ["dependency", "dependent"]
314    );
315}
316
317#[test]
318fn missing_cyclic_and_invalid_extension_dependencies_fail_before_bootstrap() {
319    for (dependencies, expected) in [
320        (&["ext:missing"][..], "depends on missing extension"),
321        (&["ext:test"][..], "Extension dependency cycle"),
322        (&["dependency"][..], "Invalid extension dependency"),
323    ] {
324        let result = Runtime::new(RuntimeOptions {
325            extensions: vec![Extension {
326                name: "ext:test",
327                dependencies,
328                ops: vec![],
329                modules: &[("ext:test/main.js", "globalThis.booted = true;")],
330                entry_point: Some("ext:test/main.js"),
331            }],
332            ..Default::default()
333        });
334        let Err(error) = result else {
335            panic!("extension dependency was accepted");
336        };
337        assert!(error.to_string().contains(expected));
338    }
339
340    let result = Runtime::new(RuntimeOptions {
341        extensions: vec![
342            Extension {
343                name: "ext:first",
344                dependencies: &["ext:second"],
345                ops: vec![],
346                modules: &[("ext:first/main.js", "")],
347                entry_point: Some("ext:first/main.js"),
348            },
349            Extension {
350                name: "ext:second",
351                dependencies: &["ext:first"],
352                ops: vec![],
353                modules: &[("ext:second/main.js", "")],
354                entry_point: Some("ext:second/main.js"),
355            },
356        ],
357        ..Default::default()
358    });
359    let Err(error) = result else {
360        panic!("extension dependency cycle was accepted");
361    };
362    assert!(
363        error
364            .to_string()
365            .contains("ext:first -> ext:second -> ext:first")
366    );
367}
368
369#[test]
370fn extensions_never_fall_back_to_the_guest_loader() {
371    for modules in [
372        &[("ext:test/main.js", "import './missing.js';")][..],
373        &[("ext:test/main.js", "import '../../escape.js';")][..],
374        &[("ext:test/main.js", "import 'guest';")][..],
375        &[("ext:test/main.js", "import 'avril:std';")][..],
376    ] {
377        let loader = Rc::new(Loader::default());
378        let result = Runtime::new(RuntimeOptions {
379            extensions: vec![Extension {
380                name: "ext:test",
381                dependencies: &[],
382                ops: vec![],
383                modules,
384                entry_point: Some("ext:test/main.js"),
385            }],
386            module_loader: Some(loader.clone()),
387        });
388        assert!(result.is_err());
389        assert_eq!(loader.loads.get(), 0);
390        assert_eq!(loader.resolves.get(), 0);
391    }
392}
393
394#[test]
395fn duplicate_sources_and_invalid_extension_registration_fail_before_bootstrap() {
396    for modules in [
397        &[("ext:test/main.js", ""), ("ext:test/main.js", "")][..],
398        &[("ext:other/main.js", "")][..],
399        &[("ext:test/../main.js", "")][..],
400    ] {
401        let result = Runtime::new(RuntimeOptions {
402            extensions: vec![Extension {
403                name: "ext:test",
404                dependencies: &[],
405                ops: vec![],
406                modules,
407                entry_point: None,
408            }],
409            ..Default::default()
410        });
411        assert!(matches!(result, Err(Error::Module(_))));
412    }
413}
414
415#[test]
416fn inherited_eval_origins_never_authorize_internal_imports() {
417    let mut runtime=Runtime::new(RuntimeOptions{extensions:vec![Extension{name:"ext:eval",dependencies:&[],ops:vec![],modules:&[("ext:eval/main.js","globalThis.invokeEval=(callback,source)=>callback(source);globalThis.makeFunction=source=>new Function(source);")],entry_point:Some("ext:eval/main.js")}],..Default::default()}).unwrap();
418    runtime.execute_script::<()>("guest",r#"
419        globalThis.evalDenied=0;
420        invokeEval(eval,"globalThis.fromEval=import('ext:core')");
421        const fromFunction=makeFunction("return import('ext:core/primordials')")();
422        for(const promise of [fromEval,fromFunction]) promise.then(()=>{throw Error('Eval gained extension authority');},e=>{if(!e.message.includes('private'))throw e;evalDenied++;});
423    "#).unwrap();
424    runtime.run_event_loop().unwrap();
425    assert_eq!(
426        runtime
427            .execute_script::<u32>("check", "evalDenied")
428            .unwrap(),
429        2
430    );
431}
432
433#[test]
434fn bootstrap_waits_for_its_entry_not_unrelated_host_work() {
435    let mut runtime=Runtime::new(RuntimeOptions{extensions:vec![Extension{name:"ext:background",dependencies:&[],ops:vec![Op::asynchronous("pending",|():()|futures::future::pending::<Result<(),io::Error>>()),Op::asynchronous("ready",|():()|async {Ok::<_,io::Error>(42)})],modules:&[("ext:background/main.js","import {pending,ready} from 'ext:core/ops';void pending();globalThis.ready=await ready();")],entry_point:Some("ext:background/main.js")}],..Default::default()}).unwrap();
436    assert_eq!(runtime.execute_script::<u32>("check", "ready").unwrap(), 42);
437}
438
439#[test]
440fn rejected_or_stalled_bootstrap_entries_return_errors() {
441    for modules in [
442        &[(
443            "ext:test/main.js",
444            "await Promise.reject(Error('failed bootstrap'));",
445        )][..],
446        &[("ext:test/main.js", "await new Promise(()=>{});")][..],
447    ] {
448        assert!(
449            Runtime::new(RuntimeOptions {
450                extensions: vec![Extension {
451                    name: "ext:test",
452                    dependencies: &[],
453                    ops: vec![],
454                    modules,
455                    entry_point: Some("ext:test/main.js")
456                }],
457                ..Default::default()
458            })
459            .is_err()
460        );
461    }
462}
463
464#[test]
465fn private_tasks_drain_nested_microtasks_before_finishing_and_next_task() {
466    let mut runtime = Runtime::new(RuntimeOptions {
467        extensions: vec![Extension {
468            name: "ext:tasks",
469            dependencies: &[],
470            ops: vec![],
471            modules: &[(
472                "ext:tasks/main.js",
473                r#"
474            import {enqueueTask} from 'ext:core/tasks';
475            globalThis.queueTask = enqueueTask;
476        "#,
477            )],
478            entry_point: Some("ext:tasks/main.js"),
479        }],
480        ..Default::default()
481    })
482    .unwrap();
483    runtime.execute_script::<()>("tasks",r#"
484        globalThis.order = [];
485        queueTask(() => {
486            order.push('run');
487            Promise.resolve().then(() => {order.push('microtask');Promise.resolve().then(() => order.push('nested'));});
488        }, () => order.push('finish'));
489        queueTask(() => order.push('next'), () => order.push('next finish'));
490    "#).unwrap();
491    assert_eq!(
492        runtime
493            .execute_script::<String>("before", "order.join()")
494            .unwrap(),
495        ""
496    );
497    runtime.run_event_loop().unwrap();
498    assert_eq!(
499        runtime
500            .execute_script::<String>("after", "order.join()")
501            .unwrap(),
502        "run,microtask,nested,finish,next,next finish"
503    );
504    runtime.execute_script::<()>("throwing",r#"
505        queueTask(() => {Promise.resolve().then(() => order.push('error microtask'));throw Error('task failed');}, () => order.push('error finish'));
506    "#).unwrap();
507    assert!(
508        runtime
509            .run_event_loop()
510            .unwrap_err()
511            .to_string()
512            .contains("task failed")
513    );
514    assert_eq!(
515        runtime
516            .execute_script::<String>("cleanup", "order.slice(-2).join()")
517            .unwrap(),
518        "error microtask,error finish"
519    );
520}