char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
711e4d5
main
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 { 10sources : HashMap < &' static str , ModuleSource >, 11resolves : Cell < usize >, 12loads : Cell < usize >, 13} 14impl ModuleLoader for Loader { 15fn resolve ( & self , specifier : & str , _: & str ) ->Result < String , Error > { 16self . resolves . set ( self . resolves . get () +1 ); 17Ok ( if specifier =="private-alias" { 18"ext:test/util.js" 19} else { 20 specifier 21} 22. into ()) 23} 24fn load ( & self , name : & str ) ->Result < ModuleSource , Error > { 25self . loads . set ( self . loads . get () +1 ); 26self . sources 27. get ( name) 28. cloned () 29. ok_or_else ( ||Error :: Module ( format! ( "Missing {name}" ))) 30} 31} 32 33fn extension () ->Extension { 34Extension { 35name : "ext:test" , 36dependencies : & [], 37ops : vec! [ Op :: asynchronous ( "echo" , |value : u32 |async move { 38Ok ::< _ , io:: Error >( value) 39})], 40modules : & [ 41( 42"ext:test/main.js" , 43r#" 44import {value} from './nested/../util.js'; 45import {primordials,ops} from 'ext:core'; 46import {echo} from 'ext:core/ops'; 47import {ArrayPrototypePush} from 'ext:core/primordials'; 48if(!Object.isFrozen(primordials)||!Object.isFrozen(ops)||echo!==ops.echo)throw Error('Captured exports'); 49globalThis.boot=[await echo(value)];ArrayPrototypePush(boot,import.meta.url,import.meta.main); 50globalThis.extensionDynamic=()=>import('./util.js'); 51"# , 52), 53( "ext:test/util.js" , "export const value=42;" ), 54], 55entry_point : Some ( "ext:test/main.js" ), 56} 57} 58 59# [ test ] 60fn extension_modules_have_relative_imports_captured_exports_and_top_level_await () { 61let mut runtime =Runtime :: new ( RuntimeOptions { 62extensions : vec! [ extension ()], 63 ..Default :: default () 64}) 65. unwrap (); 66let boot: serde_json:: Value = runtime. execute_script ( "check" , "boot" ). unwrap (); 67assert_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 () { 74let loader =Rc :: new ( Loader { 75sources : HashMap :: from ([( 76"guest" , 77"import {greeting, greet} from 'avril:greeter'; globalThis.result = `${greeting}|${greet('Avril')}`;" . into (), 78)]), 79 ..Default :: default () 80}); 81let mut runtime =Runtime :: new ( RuntimeOptions { 82extensions : vec! [ Extension { 83name : "ext:greeter" , 84dependencies : & [], 85ops : vec! [], 86modules : & [( 87"ext:greeter/main.js" , 88"globalThis.greeterRuns = (globalThis.greeterRuns ?? 0) + 1; export const greeting = 'hello'; export function greet(name) { return `${greeting}, ${name}!`; }" , 89)], 90entry_point : None , 91}], 92module_loader : Some ( loader), 93}) 94. unwrap (); 95assert! ( 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 (); 104assert_eq! ( 105 runtime. execute_script ::< String >( "check" , "result" ). unwrap (), 106"hello|hello, Avril!" 107); 108assert_eq! ( 109 runtime 110. execute_script ::< u32 >( "check" , "greeterRuns" ) 111. unwrap (), 1121 113); 114assert! ( 115 runtime 116. register_extension_module ( "avril:greeter" , "ext:greeter/main.js" ) 117. is_err () 118); 119assert! ( 120 runtime 121. register_extension_module ( "ext:greeter" , "ext:greeter/main.js" ) 122. is_err () 123); 124assert! ( 125 runtime 126. register_extension_module ( "avril:missing" , "ext:missing/main.js" ) 127. is_err () 128); 129assert_eq! ( 130 runtime 131. execute_script ::< u32 >( "check" , "greeterRuns" ) 132. unwrap (), 1331 134); 135} 136 137# [ test ] 138fn static_guest_imports_cannot_access_cached_internal_modules_or_bootstrap () { 139for specifierin [ 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] { 152let source =format! ( "import {{value}} from '{specifier}';" ); 153let loader =Rc :: new ( Loader { 154sources : HashMap :: from ([( "guest" , source. into ())]), 155 ..Default :: default () 156}); 157let mut runtime =Runtime :: new ( RuntimeOptions { 158extensions : vec! [ extension ()], 159module_loader : Some ( loader. clone ()), 160}) 161. unwrap (); 162assert! ( 163 runtime 164. execute_module ( "guest" ) 165. unwrap_err () 166. to_string () 167. contains ( "private" ) 168); 169assert_eq! ( loader. resolves . get (), 0 ); 170assert_eq! ( loader. loads . get (), 1 ); 171} 172} 173 174# [ test ] 175fn dynamic_imports_and_guest_loader_aliases_cannot_reach_internal_modules () { 176let loader =Rc :: new ( Loader { 177sources : HashMap :: from ([( "static-alias" , "import 'private-alias';" . into ())]), 178 ..Default :: default () 179}); 180let mut runtime =Runtime :: new ( RuntimeOptions { 181extensions : vec! [ extension ()], 182module_loader : Some ( loader. clone ()), 183}) 184. unwrap (); 185 runtime. execute_script ::<()>( "ext:test/main.js" , r#" 186const opsSymbol = Symbol.for('__avril.ops'); 187if ('mapSourceLocation' in globalThis || opsSymbol in globalThis || Reflect.ownKeys(globalThis).includes(opsSymbol)) throw Error('Public native registry'); 188globalThis.denied=0; 189for(const name of ['ext:test/util.js','ext:core','ext:core/stack_trace.js','ext:core/source_map','ext:unknown','private-alias']) { 190import(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 (); 194assert_eq! ( 195 runtime. execute_script ::< usize >( "check" , "denied" ). unwrap (), 1966 197); 198assert! ( 199 runtime 200. execute_module ( "static-alias" ) 201. unwrap_err () 202. to_string () 203. contains ( "private" ) 204); 205assert_eq! ( loader. loads . get (), 1 ); 206} 207 208# [ test ] 209fn private_entry_names_and_public_builtin_registration_are_rejected () { 210let loader =Rc :: new ( Loader { 211sources : HashMap :: from ([( "guest" , "globalThis.isMain=import.meta.main;" . into ())]), 212 ..Default :: default () 213}); 214let mut runtime =Runtime :: new ( RuntimeOptions { 215extensions : vec! [ extension ()], 216module_loader : Some ( loader. clone ()), 217}) 218. unwrap (); 219assert! ( 220 runtime 221. execute_module ( "ext:test/main.js" ) 222. unwrap_err () 223. to_string () 224. contains ( "private" ) 225); 226assert! ( 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 (); 234assert! ( runtime. execute_script ::< bool >( "check" , "isMain" ). unwrap ()); 235assert_eq! ( loader. loads . get (), 1 ); 236} 237 238# [ test ] 239fn extension_cycles_share_module_instances_and_initialization_runs_once () { 240let extensions =vec! [ 241Extension { 242name : "ext:cycle" , 243dependencies : & [], 244ops : vec! [], 245modules : & [ 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], 259entry_point : Some ( "ext:cycle/main.js" ), 260}, 261Extension { 262name : "ext:consumer" , 263dependencies : & [], 264ops : vec! [], 265modules : & [( 266"ext:consumer/main.js" , 267"import {a} from 'ext:cycle/a.js';globalThis.again=a;" , 268)], 269entry_point : Some ( "ext:consumer/main.js" ), 270}, 271]; 272let mut runtime =Runtime :: new ( RuntimeOptions { extensions, ..Default :: default () }). unwrap (); 273assert_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 () { 283let mut runtime =Runtime :: new ( RuntimeOptions { 284extensions : vec! [ 285Extension { 286name : "ext:dependent" , 287dependencies : & [ "ext:dependency" ], 288ops : vec! [], 289modules : & [( 290"ext:dependent/main.js" , 291"globalThis.order.push('dependent');" , 292)], 293entry_point : Some ( "ext:dependent/main.js" ), 294}, 295Extension { 296name : "ext:dependency" , 297dependencies : & [], 298ops : vec! [], 299modules : & [( 300"ext:dependency/main.js" , 301"globalThis.order = ['dependency'];" , 302)], 303entry_point : Some ( "ext:dependency/main.js" ), 304}, 305], 306 ..Default :: default () 307}) 308. unwrap (); 309assert_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 () { 319for ( dependencies, expected) in [ 320( & [ "ext:missing" ][ ..], "depends on missing extension" ), 321( & [ "ext:test" ][ ..], "Extension dependency cycle" ), 322( & [ "dependency" ][ ..], "Invalid extension dependency" ), 323] { 324let result =Runtime :: new ( RuntimeOptions { 325extensions : vec! [ Extension { 326name : "ext:test" , 327 dependencies, 328ops : vec! [], 329modules : & [( "ext:test/main.js" , "globalThis.booted = true;" )], 330entry_point : Some ( "ext:test/main.js" ), 331}], 332 ..Default :: default () 333}); 334let Err ( error) = resultelse { 335panic! ( "extension dependency was accepted" ); 336}; 337assert! ( error. to_string (). contains ( expected)); 338} 339 340let result =Runtime :: new ( RuntimeOptions { 341extensions : vec! [ 342Extension { 343name : "ext:first" , 344dependencies : & [ "ext:second" ], 345ops : vec! [], 346modules : & [( "ext:first/main.js" , "" )], 347entry_point : Some ( "ext:first/main.js" ), 348}, 349Extension { 350name : "ext:second" , 351dependencies : & [ "ext:first" ], 352ops : vec! [], 353modules : & [( "ext:second/main.js" , "" )], 354entry_point : Some ( "ext:second/main.js" ), 355}, 356], 357 ..Default :: default () 358}); 359let Err ( error) = resultelse { 360panic! ( "extension dependency cycle was accepted" ); 361}; 362assert! ( 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 () { 371for modulesin [ 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] { 377let loader =Rc :: new ( Loader :: default ()); 378let result =Runtime :: new ( RuntimeOptions { 379extensions : vec! [ Extension { 380name : "ext:test" , 381dependencies : & [], 382ops : vec! [], 383 modules, 384entry_point : Some ( "ext:test/main.js" ), 385}], 386module_loader : Some ( loader. clone ()), 387}); 388assert! ( result. is_err ()); 389assert_eq! ( loader. loads . get (), 0 ); 390assert_eq! ( loader. resolves . get (), 0 ); 391} 392} 393 394# [ test ] 395fn duplicate_sources_and_invalid_extension_registration_fail_before_bootstrap () { 396for modulesin [ 397& [( "ext:test/main.js" , "" ), ( "ext:test/main.js" , "" )][ ..], 398& [( "ext:other/main.js" , "" )][ ..], 399& [( "ext:test/../main.js" , "" )][ ..], 400] { 401let result =Runtime :: new ( RuntimeOptions { 402extensions : vec! [ Extension { 403name : "ext:test" , 404dependencies : & [], 405ops : vec! [], 406 modules, 407entry_point : None , 408}], 409 ..Default :: default () 410}); 411assert! ( matches! ( result, Err ( Error :: Module ( _)))); 412} 413} 414 415# [ test ] 416fn inherited_eval_origins_never_authorize_internal_imports () { 417let 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#" 419globalThis.evalDenied=0; 420invokeEval(eval,"globalThis.fromEval=import('ext:core')"); 421const fromFunction=makeFunction("return import('ext:core/primordials')")(); 422for(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 (); 425assert_eq! ( 426 runtime 427. execute_script ::< u32 >( "check" , "evalDenied" ) 428. unwrap (), 4292 430); 431} 432 433# [ test ] 434fn bootstrap_waits_for_its_entry_not_unrelated_host_work () { 435let 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 (); 436assert_eq! ( runtime. execute_script ::< u32 >( "check" , "ready" ). unwrap (), 42 ); 437} 438 439# [ test ] 440fn rejected_or_stalled_bootstrap_entries_return_errors () { 441for modulesin [ 442& [( 443"ext:test/main.js" , 444"await Promise.reject(Error('failed bootstrap'));" , 445)][ ..], 446& [( "ext:test/main.js" , "await new Promise(()=>{});" )][ ..], 447] { 448assert! ( 449Runtime :: new ( RuntimeOptions { 450extensions : vec! [ Extension { 451name : "ext:test" , 452dependencies : & [], 453ops : vec! [], 454 modules, 455entry_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 () { 466let mut runtime =Runtime :: new ( RuntimeOptions { 467extensions : vec! [ Extension { 468name : "ext:tasks" , 469dependencies : & [], 470ops : vec! [], 471modules : & [( 472"ext:tasks/main.js" , 473r#" 474import {enqueueTask} from 'ext:core/tasks'; 475globalThis.queueTask = enqueueTask; 476"# , 477)], 478entry_point : Some ( "ext:tasks/main.js" ), 479}], 480 ..Default :: default () 481}) 482. unwrap (); 483 runtime. execute_script ::<()>( "tasks" , r#" 484globalThis.order = []; 485queueTask(() => { 486order.push('run'); 487Promise.resolve().then(() => {order.push('microtask');Promise.resolve().then(() => order.push('nested'));}); 488}, () => order.push('finish')); 489queueTask(() => order.push('next'), () => order.push('next finish')); 490"# ). unwrap (); 491assert_eq! ( 492 runtime 493. execute_script ::< String >( "before" , "order.join()" ) 494. unwrap (), 495"" 496); 497 runtime. run_event_loop (). unwrap (); 498assert_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#" 505queueTask(() => {Promise.resolve().then(() => order.push('error microtask'));throw Error('task failed');}, () => order.push('error finish')); 506"# ). unwrap (); 507assert! ( 508 runtime 509. run_event_loop () 510. unwrap_err () 511. to_string () 512. contains ( "task failed" ) 513); 514assert_eq! ( 515 runtime 516. execute_script ::< String >( "cleanup" , "order.slice(-2).join()" ) 517. unwrap (), 518"error microtask,error finish" 519); 520}