char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1use std:: fs; 2use std:: path:: Path ; 3use std:: process::{ Command , Output }; 4 5use avril_runtime:: import_map:: ImportMap ; 6use url:: Url ; 7 8fn run ( root : & Path , entry : & str , files : & [( & str , & str )], args : & [ & str ]) ->Output { 9for ( name, source) in files{ 10let path = root. join ( name); 11 fs:: create_dir_all ( path. parent (). unwrap ()). unwrap (); 12 fs:: write ( path, source). unwrap (); 13} 14Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 15. arg ( "run" ) 16. current_dir ( root) 17. args ( args) 18. arg ( entry) 19. output () 20. unwrap () 21} 22 23# [ test ] 24fn sibling_libraries_own_their_maps_without_workspace_registration () { 25let dir = tempfile:: tempdir (). unwrap (); 26let output =run ( 27 dir. path (), 28"app/main.ts" , 29& [ 30( "app/avril.conf" , "imports: {dep: './dep.ts'}" ), 31( 32"app/main.ts" , 33"import {value} from 'dep'; import {value as other} from '../library/mod.ts'; console.log(value, other, (await import('../library/mod.ts')).value);" , 34), 35( "app/dep.ts" , "export const value = 'app';" ), 36( "library/avril.conf" , "imports: {dep: './dep.ts'}" ), 37( 38"library/mod.ts" , 39"import {value as dependency} from 'dep'; export const value = dependency;" , 40), 41( "library/dep.ts" , "export const value = 'library';" ), 42], 43& [], 44); 45assert! ( 46 output. status . success (), 47"{}" , 48String :: from_utf8_lossy ( & output. stderr ) 49); 50assert_eq! ( output. stdout , b"app library library\n" ); 51let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 52. arg ( "run" ) 53. current_dir ( dir. path (). join ( "library" )) 54. arg ( "mod.ts" ) 55. output () 56. unwrap (); 57assert! ( 58 output. status . success (), 59"{}" , 60String :: from_utf8_lossy ( & output. stderr ) 61); 62} 63 64# [ test ] 65fn nested_boundaries_do_not_inherit_consumer_aliases () { 66let dir = tempfile:: tempdir (). unwrap (); 67let output =run ( 68 dir. path (), 69"main.js" , 70& [ 71( "avril.conf" , "imports: {consumerOnly: './dep.js'}" ), 72( "main.js" , "import './library/mod.js';" ), 73( "dep.js" , "export {};" ), 74( "library/avril.conf" , "" ), 75( "library/mod.js" , "import 'consumerOnly';" ), 76], 77& [], 78); 79assert! ( !output. status . success ()); 80assert! ( 81String :: from_utf8_lossy ( & output. stderr ) 82. contains ( "Unsupported module specifier: consumerOnly" ) 83); 84} 85 86# [ test ] 87fn aliases_and_prefixes_share_canonical_module_identity () { 88let dir = tempfile:: tempdir (). unwrap (); 89let output =run ( 90 dir. path (), 91"sub/main.ts" , 92& [ 93( 94"avril.conf" , 95"imports: {shared: './lib/shared.ts', 'lib/': './lib/'}" , 96), 97( 98"sub/main.ts" , 99"import * as a from 'shared'; import * as b from 'lib/shared.ts'; import * as c from '../lib/./shared.ts'; console.log(a === b, b === c, a.count, (await import('shared')) === a);" , 100), 101( 102"lib/shared.ts" , 103"export const count = globalThis.count = (globalThis.count ?? 0) + 1;" , 104), 105], 106& [], 107); 108assert! ( 109 output. status . success (), 110"{}" , 111String :: from_utf8_lossy ( & output. stderr ) 112); 113assert_eq! ( output. stdout , b"true true 1 true\n" ); 114} 115 116# [ test ] 117fn scopes_use_longest_matches_and_fall_back_to_the_package_map () { 118let base =Url :: parse ( "file:///project/avril.conf" ). unwrap (); 119let map =ImportMap :: parse ( r#" 120imports: {dep: './default.ts', common: './common.ts', 'lib/': './lib/', 'lib/special/': './special/'}, 121scopes: { 122'./src/': {dep: './src.ts'}, 123'./src/nested/': {dep: './nested.ts', blocked: null}, 124}, 125"# , & base). unwrap (); 126let referrer =Url :: parse ( "file:///project/src/nested/main.ts" ). unwrap (); 127assert_eq! ( 128 map. resolve ( "dep" , & referrer). unwrap (). as_deref (), 129Some ( "file:///project/nested.ts" ) 130); 131assert_eq! ( 132 map. resolve ( "common" , & referrer). unwrap (). as_deref (), 133Some ( "file:///project/common.ts" ) 134); 135assert_eq! ( 136 map. resolve ( "lib/special/mod.ts" , & referrer) 137. unwrap () 138. as_deref (), 139Some ( "file:///project/special/mod.ts" ) 140); 141assert! ( 142 map. resolve ( "blocked" , & referrer) 143. unwrap_err () 144. to_string () 145. contains ( "blocked" ) 146); 147assert! ( 148 map. resolve ( "lib/../outside.ts" , & referrer) 149. unwrap_err () 150. to_string () 151. contains ( "escapes" ) 152); 153assert! ( map. resolve ( "lib/%2e%2e/outside.ts" , & referrer). is_err ()); 154} 155 156# [ test ] 157fn normalizes_url_like_keys_and_does_not_recursively_resolve_aliases () { 158let base =Url :: parse ( "file:///project/avril.conf" ). unwrap (); 159let map =ImportMap :: parse ( 160"imports: {'./dep.ts': './other.ts', other: './dep.ts'}," , 161& base, 162) 163. unwrap (); 164let referrer =Url :: parse ( "file:///project/sub/main.ts" ). unwrap (); 165assert_eq! ( 166 map. resolve ( "../dep.ts" , & referrer). unwrap (). as_deref (), 167Some ( "file:///project/other.ts" ) 168); 169assert_eq! ( 170 map. resolve ( "other" , & referrer). unwrap (). as_deref (), 171Some ( "file:///project/dep.ts" ) 172); 173} 174 175# [ test ] 176fn unknown_options_do_not_affect_import_resolution () { 177let base =Url :: parse ( "file:///project/avril.conf" ). unwrap (); 178let map =ImportMap :: parse ( 179"custom: {imports: false, nested: [null, 42]}, 180imports: {dep: './dep.ts'}, 181scopes: {'./tests/': {dep: './test-dep.ts'}}" , 182& base, 183) 184. unwrap (); 185for ( referrer, target) in [ 186( "./main.ts" , "./dep.ts" ), 187( "./tests/main.ts" , "./test-dep.ts" ), 188] { 189assert_eq! ( 190 map. resolve ( "dep" , & base. join ( referrer). unwrap ()). unwrap (), 191Some ( base. join ( target). unwrap (). to_string ()) 192); 193} 194} 195 196# [ test ] 197fn invalid_schema_reports_the_offending_source_location () { 198let base =Url :: parse ( "file:///project/avril.conf" ). unwrap (); 199for ( source, expected) in [ 200( "imports: []" , "Expected an object" ), 201( "imports: {dep: true}" , "strings or null" ), 202( "imports: {dep: 'bare'}" , "URLs or relative paths" ), 203( "imports: {'lib/': './lib'}" , "end with '/'" ), 204( "imports: {'': './dep.ts'}" , "must not be empty" ), 205( 206"imports: {'./dep.ts': './a.ts', 'file:///project/dep.ts': './b.ts'}" , 207"Duplicate normalized" , 208), 209] { 210let error =ImportMap :: parse ( source, & base). unwrap_err (); 211assert! ( error. message . contains ( expected), "{source}: {error}" ); 212assert! ( source. get ( error. span ). is_some ()); 213} 214let dir = tempfile:: tempdir (). unwrap (); 215let output =run ( 216 dir. path (), 217"main.js" , 218& [ 219( "main.js" , "import 'dep';" ), 220( "avril.conf" , "// heading\nimports: {dep: true}" ), 221], 222& [], 223); 224assert! ( !output. status . success ()); 225assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "avril.conf:2:16" )); 226} 227 228# [ test ] 229fn discovery_and_mapped_imports_do_not_expand_execution_authority () { 230let dir = tempfile:: tempdir (). unwrap (); 231let output =run ( 232 dir. path (), 233"app/main.js" , 234& [ 235( "avril.conf" , "imports: {secret: './secret.js'}" ), 236( "secret.js" , "console.log('secret');" ), 237( "app/main.js" , "import 'secret';" ), 238], 239& [ "-0" , "--allow-js-exec=app" ], 240); 241assert! ( !output. status . success ()); 242assert! ( 243String :: from_utf8_lossy ( & output. stderr ). contains ( "Unsupported module specifier: secret" ) 244); 245let output =run ( 246 dir. path (), 247"app/main.js" , 248& [( "app/avril.conf" , "imports: {secret: '../secret.js'}" )], 249& [ "-0" , "--allow-js-exec=app" ], 250); 251assert! ( !output. status . success ()); 252assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "PermissionDenied: js-exec" )); 253assert! ( output. stdout . is_empty ()); 254} 255 256# [ test ] 257fn runtime_revocation_still_applies_to_mapped_cached_modules () { 258let dir = tempfile:: tempdir (). unwrap (); 259let output =run ( 260 dir. path (), 261"main.js" , 262& [ 263( "avril.conf" , "imports: {dep: './dep.js'}" ), 264( 265"main.js" , 266"import * as Avril from 'avril:std'; await import('dep'); Avril.pledge({jsExec: {deny: ['./dep.js']}}); try {await import('dep'); throw new Error('allowed');} catch (e) {console.log(e.message.includes('PermissionDenied'));}" , 267), 268( "dep.js" , "export {};" ), 269], 270& [], 271); 272assert! ( 273 output. status . success (), 274"{}" , 275String :: from_utf8_lossy ( & output. stderr ) 276); 277assert_eq! ( output. stdout , b"true\n" ); 278} 279 280# [ test ] 281fn scope_file_keys_are_exact_and_colons_can_appear_in_bare_suffixes () { 282let base =Url :: parse ( "file:///project/avril.conf" ). unwrap (); 283let map =ImportMap :: parse ( 284"imports: {dep: './default.ts', 'lib/': './lib/', 'lib/a:b.ts': './lib/a:b.ts'}, scopes: {'./src': {dep: './scoped.ts'}}" , 285& base, 286) 287. unwrap (); 288assert_eq! ( 289 map. resolve ( "dep" , & Url :: parse ( "file:///project/src" ). unwrap ()) 290. unwrap () 291. as_deref (), 292Some ( "file:///project/scoped.ts" ) 293); 294assert_eq! ( 295 map. resolve ( 296"dep" , 297& Url :: parse ( "file:///project/src-legacy/main.ts" ). unwrap () 298) 299. unwrap () 300. as_deref (), 301Some ( "file:///project/default.ts" ) 302); 303assert_eq! ( 304 map. resolve ( "lib/a:b.ts" , & base). unwrap (). as_deref (), 305Some ( "file:///project/lib/a:b.ts" ) 306); 307assert! ( map. resolve ( "lib/..%2fsecret.ts" , & base). is_err ()); 308} 309 310# [ test ] 311fn cached_maps_do_not_skip_revoked_ancestor_authority () { 312let dir = tempfile:: tempdir (). unwrap (); 313let output =run ( 314 dir. path (), 315"app/main.js" , 316& [ 317( "avril.conf" , "imports: {dep: './app/dep.js'}" ), 318( "app/dep.js" , "export {};" ), 319( 320"app/main.js" , 321"import {pledge} from 'avril:std'; await import('dep'); pledge({jsExec: {allow: ['./app', './avril.conf']}}); try {await import('dep'); throw new Error('allowed');} catch(e) {console.log(e.message.includes('Unsupported module specifier: dep'));}" , 322), 323], 324& [], 325); 326assert! ( 327 output. status . success (), 328"{}" , 329String :: from_utf8_lossy ( & output. stderr ) 330); 331assert_eq! ( output. stdout , b"true\n" ); 332} 333 334# [ test ] 335fn denied_config_existence_is_not_observable () { 336let dir = tempfile:: tempdir (). unwrap (); 337let files =[( "main.js" , "import 'dep';" )]; 338let args =[ "-0" , "--allow-js-exec=." , "--deny-js-exec=avril.conf" ]; 339let missing =run ( dir. path (), "main.js" , & files, & args); 340let present =run ( 341 dir. path (), 342"main.js" , 343& [( "avril.conf" , "not: valid: syntax" )], 344& args, 345); 346assert! ( !missing. status . success ()); 347assert_eq! ( present. stderr , missing. stderr ); 348} 349 350# [ cfg ( unix )] 351# [ test ] 352fn config_symlinks_are_rejected_without_probing_referents () { 353use std:: os:: unix:: fs:: symlink; 354let dir = tempfile:: tempdir (). unwrap (); 355 fs:: write ( dir. path (). join ( "main.js" ), "import 'dep';" ). unwrap (); 356 fs:: write ( dir. path (). join ( "existing" ), "" ). unwrap (); 357let config = dir. path (). join ( "avril.conf" ); 358symlink ( dir. path (). join ( "existing" ), & config). unwrap (); 359let present =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 360. arg ( "run" ) 361. current_dir ( dir. path ()) 362. args ([ "-0" , "--allow-js-exec=." , "main.js" ]) 363. output () 364. unwrap (); 365 fs:: remove_file ( & config). unwrap (); 366symlink ( dir. path (). join ( "missing" ), & config). unwrap (); 367let missing =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 368. arg ( "run" ) 369. current_dir ( dir. path ()) 370. args ([ "-0" , "--allow-js-exec=." , "main.js" ]) 371. output () 372. unwrap (); 373assert! ( !present. status . success ()); 374assert_eq! ( present. stderr , missing. stderr ); 375assert! ( String :: from_utf8_lossy ( & present. stderr ). contains ( "non-symlink" )); 376} 377 378# [ cfg ( unix )] 379# [ test ] 380fn discovery_stops_before_shared_ancestors () { 381use std:: os:: unix:: fs:: PermissionsExt ; 382let dir = tempfile:: tempdir (). unwrap (); 383let output =run ( 384 dir. path (), 385"shared/app/main.js" , 386& [ 387( "shared/avril.conf" , "imports: {dep: './dep.js'}" ), 388( "shared/dep.js" , "console.log('hijacked');" ), 389( "shared/app/main.js" , "import 'dep';" ), 390], 391& [], 392); 393assert! ( output. status . success ()); 394 fs:: set_permissions ( dir. path (). join ( "shared" ), fs:: Permissions :: from_mode ( 0o777 )). unwrap (); 395let output =run ( dir. path (), "shared/app/main.js" , & [], & []); 396assert! ( !output. status . success ()); 397assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "Unsupported module specifier" )); 398assert! ( output. stdout . is_empty ()); 399}