use std::cell::Cell; use std::collections::HashMap; use std::io; use std::rc::Rc; use avril_core::{Error, Extension, ModuleLoader, ModuleSource, Op, Runtime, RuntimeOptions}; #[derive(Default)] struct Loader { sources: HashMap<&'static str, ModuleSource>, resolves: Cell, loads: Cell, } impl ModuleLoader for Loader { fn resolve(&self, specifier: &str, _: &str) -> Result { self.resolves.set(self.resolves.get() + 1); Ok(if specifier == "private-alias" { "ext:test/util.js" } else { specifier } .into()) } fn load(&self, name: &str) -> Result { self.loads.set(self.loads.get() + 1); self.sources .get(name) .cloned() .ok_or_else(|| Error::Module(format!("Missing {name}"))) } } fn extension() -> Extension { Extension { name: "ext:test", dependencies: &[], ops: vec![Op::asynchronous("echo", |value: u32| async move { Ok::<_, io::Error>(value) })], modules: &[ ( "ext:test/main.js", r#" import {value} from './nested/../util.js'; import {primordials,ops} from 'ext:core'; import {echo} from 'ext:core/ops'; import {ArrayPrototypePush} from 'ext:core/primordials'; if(!Object.isFrozen(primordials)||!Object.isFrozen(ops)||echo!==ops.echo)throw Error('Captured exports'); globalThis.boot=[await echo(value)];ArrayPrototypePush(boot,import.meta.url,import.meta.main); globalThis.extensionDynamic=()=>import('./util.js'); "#, ), ("ext:test/util.js", "export const value=42;"), ], entry_point: Some("ext:test/main.js"), } } #[test] fn extension_modules_have_relative_imports_captured_exports_and_top_level_await() { let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![extension()], ..Default::default() }) .unwrap(); let boot: serde_json::Value = runtime.execute_script("check", "boot").unwrap(); assert_eq!(boot, serde_json::json!([42, "ext:test/main.js", false])); runtime.execute_script::<()>("guest",r#"extensionDynamic().then(()=>{throw Error('Allowed extension dynamic import');},e=>{if(!e.message.includes('not yet implemented'))throw e;});"#).unwrap(); runtime.run_event_loop().unwrap(); } #[test] fn extension_module_can_be_registered_as_guest_builtin() { let loader = Rc::new(Loader { sources: HashMap::from([( "guest", "import {greeting, greet} from 'avril:greeter'; globalThis.result = `${greeting}|${greet('Avril')}`;".into(), )]), ..Default::default() }); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:greeter", dependencies: &[], ops: vec![], modules: &[( "ext:greeter/main.js", "globalThis.greeterRuns = (globalThis.greeterRuns ?? 0) + 1; export const greeting = 'hello'; export function greet(name) { return `${greeting}, ${name}!`; }", )], entry_point: None, }], module_loader: Some(loader), }) .unwrap(); assert!( runtime .execute_script::("check", "globalThis.greeterRuns === undefined") .unwrap() ); runtime .register_extension_module("avril:greeter", "ext:greeter/main.js") .unwrap(); runtime.execute_module("guest").unwrap(); assert_eq!( runtime.execute_script::("check", "result").unwrap(), "hello|hello, Avril!" ); assert_eq!( runtime .execute_script::("check", "greeterRuns") .unwrap(), 1 ); assert!( runtime .register_extension_module("avril:greeter", "ext:greeter/main.js") .is_err() ); assert!( runtime .register_extension_module("ext:greeter", "ext:greeter/main.js") .is_err() ); assert!( runtime .register_extension_module("avril:missing", "ext:missing/main.js") .is_err() ); assert_eq!( runtime .execute_script::("check", "greeterRuns") .unwrap(), 1 ); } #[test] fn static_guest_imports_cannot_access_cached_internal_modules_or_bootstrap() { for specifier in [ "ext:test/util.js", "ext:test/main.js", "ext:core", "ext:core/ops", "ext:core/event_hooks", "ext:core/primordials", "ext:core/promises", "ext:core/bootstrap", "ext:core/stack_trace.js", "ext:core/source_map", "ext:unknown", ] { let source = format!("import {{value}} from '{specifier}';"); let loader = Rc::new(Loader { sources: HashMap::from([("guest", source.into())]), ..Default::default() }); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![extension()], module_loader: Some(loader.clone()), }) .unwrap(); assert!( runtime .execute_module("guest") .unwrap_err() .to_string() .contains("private") ); assert_eq!(loader.resolves.get(), 0); assert_eq!(loader.loads.get(), 1); } } #[test] fn dynamic_imports_and_guest_loader_aliases_cannot_reach_internal_modules() { let loader = Rc::new(Loader { sources: HashMap::from([("static-alias", "import 'private-alias';".into())]), ..Default::default() }); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![extension()], module_loader: Some(loader.clone()), }) .unwrap(); runtime.execute_script::<()>("ext:test/main.js",r#" const opsSymbol = Symbol.for('__avril.ops'); if ('mapSourceLocation' in globalThis || opsSymbol in globalThis || Reflect.ownKeys(globalThis).includes(opsSymbol)) throw Error('Public native registry'); globalThis.denied=0; for(const name of ['ext:test/util.js','ext:core','ext:core/stack_trace.js','ext:core/source_map','ext:unknown','private-alias']) { import(name).then(()=>{throw Error('Leaked internal module');},e=>{if(!e.message.includes('private')||e.message.includes('not yet implemented'))throw e;denied++;}); } "#).unwrap(); runtime.run_event_loop().unwrap(); assert_eq!( runtime.execute_script::("check", "denied").unwrap(), 6 ); assert!( runtime .execute_module("static-alias") .unwrap_err() .to_string() .contains("private") ); assert_eq!(loader.loads.get(), 1); } #[test] fn private_entry_names_and_public_builtin_registration_are_rejected() { let loader = Rc::new(Loader { sources: HashMap::from([("guest", "globalThis.isMain=import.meta.main;".into())]), ..Default::default() }); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![extension()], module_loader: Some(loader.clone()), }) .unwrap(); assert!( runtime .execute_module("ext:test/main.js") .unwrap_err() .to_string() .contains("private") ); assert!( runtime .register_module("ext:core", |scope| Some(avril_core::v8::Object::new(scope))) .unwrap_err() .to_string() .contains("private") ); runtime.execute_module("guest").unwrap(); assert!(runtime.execute_script::("check", "isMain").unwrap()); assert_eq!(loader.loads.get(), 1); } #[test] fn extension_cycles_share_module_instances_and_initialization_runs_once() { let extensions = vec![ Extension { name: "ext:cycle", dependencies: &[], ops: vec![], modules: &[ ( "ext:cycle/main.js", "import {sum} from './a.js';globalThis.total=sum();", ), ( "ext:cycle/a.js", "import {b} from './b.js';export const a=20;export function sum(){return a+b;}globalThis.aRuns=(globalThis.aRuns??0)+1;", ), ( "ext:cycle/b.js", "import {a} from './a.js';export const b=22;export function readA(){return a;}", ), ], entry_point: Some("ext:cycle/main.js"), }, Extension { name: "ext:consumer", dependencies: &[], ops: vec![], modules: &[( "ext:consumer/main.js", "import {a} from 'ext:cycle/a.js';globalThis.again=a;", )], entry_point: Some("ext:consumer/main.js"), }, ]; let mut runtime = Runtime::new(RuntimeOptions { extensions, ..Default::default() }).unwrap(); assert_eq!( runtime .execute_script::>("check", "[total,again,aRuns]") .unwrap(), [42, 20, 1] ); } #[test] fn extension_entry_points_run_after_dependencies() { let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![ Extension { name: "ext:dependent", dependencies: &["ext:dependency"], ops: vec![], modules: &[( "ext:dependent/main.js", "globalThis.order.push('dependent');", )], entry_point: Some("ext:dependent/main.js"), }, Extension { name: "ext:dependency", dependencies: &[], ops: vec![], modules: &[( "ext:dependency/main.js", "globalThis.order = ['dependency'];", )], entry_point: Some("ext:dependency/main.js"), }, ], ..Default::default() }) .unwrap(); assert_eq!( runtime .execute_script::>("check", "order") .unwrap(), ["dependency", "dependent"] ); } #[test] fn missing_cyclic_and_invalid_extension_dependencies_fail_before_bootstrap() { for (dependencies, expected) in [ (&["ext:missing"][..], "depends on missing extension"), (&["ext:test"][..], "Extension dependency cycle"), (&["dependency"][..], "Invalid extension dependency"), ] { let result = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies, ops: vec![], modules: &[("ext:test/main.js", "globalThis.booted = true;")], entry_point: Some("ext:test/main.js"), }], ..Default::default() }); let Err(error) = result else { panic!("extension dependency was accepted"); }; assert!(error.to_string().contains(expected)); } let result = Runtime::new(RuntimeOptions { extensions: vec![ Extension { name: "ext:first", dependencies: &["ext:second"], ops: vec![], modules: &[("ext:first/main.js", "")], entry_point: Some("ext:first/main.js"), }, Extension { name: "ext:second", dependencies: &["ext:first"], ops: vec![], modules: &[("ext:second/main.js", "")], entry_point: Some("ext:second/main.js"), }, ], ..Default::default() }); let Err(error) = result else { panic!("extension dependency cycle was accepted"); }; assert!( error .to_string() .contains("ext:first -> ext:second -> ext:first") ); } #[test] fn extensions_never_fall_back_to_the_guest_loader() { for modules in [ &[("ext:test/main.js", "import './missing.js';")][..], &[("ext:test/main.js", "import '../../escape.js';")][..], &[("ext:test/main.js", "import 'guest';")][..], &[("ext:test/main.js", "import 'avril:std';")][..], ] { let loader = Rc::new(Loader::default()); let result = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![], modules, entry_point: Some("ext:test/main.js"), }], module_loader: Some(loader.clone()), }); assert!(result.is_err()); assert_eq!(loader.loads.get(), 0); assert_eq!(loader.resolves.get(), 0); } } #[test] fn duplicate_sources_and_invalid_extension_registration_fail_before_bootstrap() { for modules in [ &[("ext:test/main.js", ""), ("ext:test/main.js", "")][..], &[("ext:other/main.js", "")][..], &[("ext:test/../main.js", "")][..], ] { let result = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![], modules, entry_point: None, }], ..Default::default() }); assert!(matches!(result, Err(Error::Module(_)))); } } #[test] fn inherited_eval_origins_never_authorize_internal_imports() { 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(); runtime.execute_script::<()>("guest",r#" globalThis.evalDenied=0; invokeEval(eval,"globalThis.fromEval=import('ext:core')"); const fromFunction=makeFunction("return import('ext:core/primordials')")(); for(const promise of [fromEval,fromFunction]) promise.then(()=>{throw Error('Eval gained extension authority');},e=>{if(!e.message.includes('private'))throw e;evalDenied++;}); "#).unwrap(); runtime.run_event_loop().unwrap(); assert_eq!( runtime .execute_script::("check", "evalDenied") .unwrap(), 2 ); } #[test] fn bootstrap_waits_for_its_entry_not_unrelated_host_work() { let mut runtime=Runtime::new(RuntimeOptions{extensions:vec![Extension{name:"ext:background",dependencies:&[],ops:vec![Op::asynchronous("pending",|():()|futures::future::pending::>()),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(); assert_eq!(runtime.execute_script::("check", "ready").unwrap(), 42); } #[test] fn rejected_or_stalled_bootstrap_entries_return_errors() { for modules in [ &[( "ext:test/main.js", "await Promise.reject(Error('failed bootstrap'));", )][..], &[("ext:test/main.js", "await new Promise(()=>{});")][..], ] { assert!( Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![], modules, entry_point: Some("ext:test/main.js") }], ..Default::default() }) .is_err() ); } } #[test] fn private_tasks_drain_nested_microtasks_before_finishing_and_next_task() { let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:tasks", dependencies: &[], ops: vec![], modules: &[( "ext:tasks/main.js", r#" import {enqueueTask} from 'ext:core/tasks'; globalThis.queueTask = enqueueTask; "#, )], entry_point: Some("ext:tasks/main.js"), }], ..Default::default() }) .unwrap(); runtime.execute_script::<()>("tasks",r#" globalThis.order = []; queueTask(() => { order.push('run'); Promise.resolve().then(() => {order.push('microtask');Promise.resolve().then(() => order.push('nested'));}); }, () => order.push('finish')); queueTask(() => order.push('next'), () => order.push('next finish')); "#).unwrap(); assert_eq!( runtime .execute_script::("before", "order.join()") .unwrap(), "" ); runtime.run_event_loop().unwrap(); assert_eq!( runtime .execute_script::("after", "order.join()") .unwrap(), "run,microtask,nested,finish,next,next finish" ); runtime.execute_script::<()>("throwing",r#" queueTask(() => {Promise.resolve().then(() => order.push('error microtask'));throw Error('task failed');}, () => order.push('error finish')); "#).unwrap(); assert!( runtime .run_event_loop() .unwrap_err() .to_string() .contains("task failed") ); assert_eq!( runtime .execute_script::("cleanup", "order.slice(-2).join()") .unwrap(), "error microtask,error finish" ); }