char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f4a0f5b
main
1use std:: io; 2use std:: path:: Path ; 3use std:: process::{ Command , Output }; 4 5use avril_host:: environment:: Environment ; 6use avril_runtime:: caps::{ Capabilities , Category , Initial }; 7 8fn run ( source : & str , args : & [ & str ], values : & [( & str , & str )]) ->Output { 9let dir = tempfile:: tempdir (). unwrap (); 10 std:: fs:: write ( dir. path (). join ( "main.ts" ), source). unwrap (); 11Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 12. current_dir ( dir. path ()) 13. env_clear () 14. envs ( values. iter (). copied ()) 15. arg ( "run" ) 16. args ( args) 17. arg ( "main.ts" ) 18. output () 19. unwrap () 20} 21 22fn success ( output : Output ) ->String { 23assert! ( 24 output. status . success (), 25"{}" , 26String :: from_utf8_lossy ( & output. stderr ) 27); 28String :: from_utf8 ( output. stdout ). unwrap () 29} 30 31# [ test ] 32fn inherited_values_can_be_captured_changed_and_deleted () { 33let output =run ( 34r#" 35import {env} from 'avril:std'; 36console.log(env.get('TOKEN'), env.has('EMPTY'), env.get('EMPTY') === '', 37env.get('MISSING') === undefined, env.has('MISSING')); 38const captured = env.get('TOKEN'); 39env.delete('TOKEN'); 40console.log(captured, env.has('TOKEN')); 41env.set('TOKEN', 'new'); 42env.set('EMPTY', ''); 43env.set('NUMBER', 42 as unknown as string); 44env.delete('MISSING'); 45console.log(env.get('TOKEN'), env.get('EMPTY') === '', env.get('NUMBER')); 46"# , 47& [], 48& [( "TOKEN" , "secret" ), ( "EMPTY" , "" )], 49); 50assert_eq! ( 51success ( output), 52"secret true true true false\nsecret false\nnew true 42\n" 53); 54} 55 56# [ test ] 57fn snapshots_are_filtered_independent_and_safe_for_special_names () { 58let output =run ( 59r#" 60import {env} from 'avril:std'; 61const snapshot = env.toObject(); 62console.log(Object.getPrototypeOf(snapshot) === null, 63Object.keys(snapshot).sort().join(','), snapshot.__proto__, snapshot.constructor); 64snapshot.VISIBLE = 'snapshot'; 65delete snapshot.constructor; 66env.set('__proto__', 'changed'); 67console.log(env.get('VISIBLE'), env.get('constructor'), snapshot.__proto__); 68"# , 69& [ 70"-0" , 71"--allow-env=VISIBLE" , 72"--allow-env=__proto__" , 73"--allow-env=constructor" , 74], 75& [ 76( "VISIBLE" , "original" ), 77( "SECRET" , "hidden" ), 78( "__proto__" , "proto" ), 79( "constructor" , "ctor" ), 80], 81); 82assert_eq! ( 83success ( output), 84"true VISIBLE,__proto__,constructor proto ctor\noriginal ctor proto\n" 85); 86} 87 88# [ test ] 89fn named_operations_require_permission_even_for_missing_variables () { 90for argsin [ 91vec! [ "-0" ], 92vec! [ 93"--deny-env=SECRET" , 94"--deny-env=MISSING" , 95"--allow-env=SECRET" , 96], 97] { 98let output =run ( 99r#" 100import {env, errors} from 'avril:std'; 101for (const name of ['SECRET', 'MISSING']) { 102for (const action of [() => env.get(name), () => env.has(name), 103() => env.set(name, 'replacement'), () => env.delete(name)]) { 104try {action(); throw Error('Allowed');} 105catch (error) {if (!(error instanceof errors.PermissionDenied)) throw error;} 106} 107} 108console.log('denied', Object.keys(env.toObject()).length); 109"# , 110& args, 111& [( "SECRET" , "secret" )], 112); 113assert_eq! ( success ( output), "denied 0\n" ); 114} 115} 116 117# [ test ] 118fn cli_grants_are_exact_repeatable_and_do_not_split_commas () { 119let output =run ( 120r#" 121import {env, errors} from 'avril:std'; 122console.log(Object.keys(env.toObject()).sort().join('|')); 123for (const name of ['FIRST_EXTRA', 'FIRST,SECOND', 'DENIED']) { 124try {env.get(name); throw Error('Allowed');} 125catch (error) {if (!(error instanceof errors.PermissionDenied)) throw error;} 126} 127"# , 128& [ 129"-0" , 130"--allow-env=FIRST" , 131"--allow-env=WITH,COMMA" , 132"--allow-env=SECOND" , 133"--allow-env=DENIED" , 134"--deny-env=DENIED" , 135], 136& [ 137( "FIRST" , "1" ), 138( "SECOND" , "2" ), 139( "WITH,COMMA" , "3" ), 140( "DENIED" , "4" ), 141( "FIRST_EXTRA" , "5" ), 142], 143); 144assert_eq! ( success ( output), "FIRST|SECOND|WITH,COMMA\n" ); 145} 146 147# [ test ] 148fn pledges_are_atomic_monotonic_and_apply_to_retained_methods () { 149let output =run ( 150r#" 151import {env, pledge, errors} from 'avril:std'; 152const {get, set, delete: remove, toObject} = env; 153const captured = get('SECRET'); 154for (const request of [{env: false, read: {deny: ['\0']}}, 155{env: {allow: ['']}}, {env: {deny: ['A=B']}}, {env: {deny: ['A\0B']}}, 156{env: {allow: 'SECRET'}}, {env: true}]) { 157try {pledge(request as any); throw Error('Invalid pledge accepted');} 158catch (error) {if (!(error instanceof TypeError)) throw error;} 159if (get('SECRET') !== captured) throw Error('Partial pledge'); 160} 161pledge({env: {allow: ['SECRET', 'PUBLIC']}}); 162pledge({env: {deny: ['SECRET']}}); 163pledge({env: {allow: ['SECRET', 'OTHER']}}); 164for (const action of [() => get('SECRET'), () => set('SECRET', 'new'), 165() => remove('SECRET'), () => get('OTHER'), () => get('PUBLIC')]) { 166try {action(); throw Error('Authority restored');} 167catch (error) {if (!(error instanceof errors.PermissionDenied)) throw error;} 168} 169console.log(captured, Object.keys(toObject()).length); 170pledge({env: false}); 171console.log(Object.keys(env.toObject()).length); 172"# , 173& [], 174& [ 175( "SECRET" , "captured" ), 176( "PUBLIC" , "public" ), 177( "OTHER" , "other" ), 178], 179); 180assert_eq! ( success ( output), "captured 0\n0\n" ); 181} 182 183# [ test ] 184fn invalid_names_values_and_missing_arguments_fail_without_mutating_state () { 185let output =run ( 186r#" 187import {env, errors} from 'avril:std'; 188for (const name of ['', 'A=B', 'A\0B']) { 189for (const action of [() => env.get(name), () => env.has(name), 190() => env.set(name, 'value'), () => env.delete(name)]) { 191try {action(); throw Error('Invalid name accepted');} 192catch (error) {if (!(error instanceof errors.InvalidInput)) throw error;} 193} 194} 195try {env.set('VALID', 'bad\0value'); throw Error('Invalid value accepted');} 196catch (error) {if (!(error instanceof errors.InvalidInput)) throw error;} 197for (const action of [() => (env.get as any)(), () => (env.has as any)(), 198() => (env.set as any)('VALID'), () => (env.delete as any)(), () => env.get(Symbol() as any)]) { 199try {action(); throw Error('Invalid arguments accepted');} 200catch (error) {if (!(error instanceof TypeError)) throw error;} 201} 202console.log(env.get('VALID')); 203"# , 204& [], 205& [( "VALID" , "original" )], 206); 207assert_eq! ( success ( output), "original\n" ); 208for flagin [ "--allow-env=A=B" , "--deny-env=A=B" , "--allow-env=" ] { 209let output =run ( "throw Error('Executed');" , & [ flag], & []); 210assert! ( !output. status . success ()); 211assert! ( !String :: from_utf8_lossy ( & output. stderr ). contains ( "Executed" )); 212} 213} 214 215# [ test ] 216fn captured_env_api_survives_poisoned_globals_and_prototypes () { 217let output =run ( 218r#" 219import {env, pledge, errors} from 'avril:std'; 220const log = console.log; 221const objectPrototype = Object.prototype; 222const poison = () => {throw Error('Poisoned');}; 223globalThis.String = poison; globalThis.Object = poison; 224objectPrototype.toString = poison; 225objectPrototype.get = poison; 226objectPrototype.set = poison; 227Array.prototype[Symbol.iterator] = poison; 228env.set('KEY', 'new'); 229const snapshot = env.toObject(); 230pledge({env: {allow: ['KEY']}}); 231log(env.get('KEY'), snapshot.KEY); 232const cases = [['KEY=bad', errors.InvalidInput], ['DENIED', errors.PermissionDenied]]; 233for (let index = 0; index < cases.length; index++) { 234try {env.get(cases[index][0]); throw Error('Allowed');} 235catch (error) {if (!(error instanceof cases[index][1])) throw error;} 236} 237"# , 238& [], 239& [( "KEY" , "old" )], 240); 241assert_eq! ( success ( output), "new new\n" ); 242} 243 244# [ test ] 245fn environment_authority_is_inherited_as_a_snapshot () { 246let cwd =Path :: new ( "." ); 247let mut initial =Initial :: new ( true ); 248for keyin [ "A" , "B" , "DENIED" ] { 249 initial. grant ( Category :: Env , false , key, cwd). unwrap (); 250} 251 initial. grant ( Category :: Env , true , "DENIED" , cwd). unwrap (); 252let parent =Capabilities :: new ( initial, cwd. to_owned ()); 253let child =Capabilities :: inherit ( parent. clone ()); 254let mut env =Environment :: new ([( "A" . into (), "a" . into ()), ( "B" . into (), "b" . into ())]). unwrap (); 255assert_eq! ( 256 env. get ( & child, "DENIED" ). unwrap_err (). kind (), 257 io:: ErrorKind :: PermissionDenied 258); 259assert! ( 260 parent 261. pledge ( vec! [( Category :: Env , None , vec! [ "A" . into (), "" . into ()])]) 262. is_err () 263); 264assert_eq! ( env. get ( & child, "A" ). unwrap (). as_deref (), Some ( "a" )); 265// Later pledges narrow only the parent and future workers. 266 parent 267. pledge ( vec! [( Category :: Env , None , vec! [ "A" . into ()])]) 268. unwrap (); 269assert_eq! ( 270 env. get ( & parent, "A" ). unwrap_err (). kind (), 271 io:: ErrorKind :: PermissionDenied 272); 273assert_eq! ( env. get ( & child, "A" ). unwrap (). as_deref (), Some ( "a" )); 274 env. set ( & child, "A" , "replacement" . into ()). unwrap (); 275 env. delete ( & child, "A" ). unwrap (); 276assert_eq! ( env. to_object ( & child), vec! [( "B" . into (), "b" . into ())]); 277let later =Capabilities :: inherit ( parent. clone ()); 278assert_eq! ( 279 env. get ( & later, "A" ). unwrap_err (). kind (), 280 io:: ErrorKind :: PermissionDenied 281); 282 child 283. pledge ( vec! [( Category :: Env , Some ( vec! []), vec! [])]) 284. unwrap (); 285assert! ( env. to_object ( & child). is_empty ()); 286assert_eq! ( env. get ( & parent, "B" ). unwrap (). as_deref (), Some ( "b" )); 287} 288 289# [ test ] 290fn mutations_do_not_change_the_host_process_environment () { 291let original = std:: env:: var_os ( "PATH" ); 292let caps =Capabilities :: new ( Initial :: new ( false ), Path :: new ( "." ). to_owned ()); 293let mut env =Environment :: from_process (); 294 env. set ( & caps, "PATH" , "runtime-local" . into ()). unwrap (); 295assert_eq! ( std:: env:: var_os ( "PATH" ), original); 296 env. delete ( & caps, "PATH" ). unwrap (); 297assert_eq! ( std:: env:: var_os ( "PATH" ), original); 298} 299 300# [ cfg ( unix )] 301# [ test ] 302fn non_utf8_host_variables_are_not_lossily_exposed () { 303use std:: ffi:: OsString ; 304use std:: os:: unix:: ffi:: OsStringExt ; 305let dir = tempfile:: tempdir (). unwrap (); 306 std:: fs:: write ( 307 dir. path (). join ( "main.js" ), 308"import {env} from 'avril:std'; console.log(JSON.stringify(env.toObject()));" , 309) 310. unwrap (); 311let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 312. current_dir ( dir. path ()) 313. env_clear () 314. env ( OsString :: from_vec ( vec! [ b'A' , 0xff ]), "secret" ) 315. env ( "BAD_VALUE" , OsString :: from_vec ( vec! [ 0xff ])) 316. args ([ "run" , "main.js" ]) 317. output () 318. unwrap (); 319assert_eq! ( success ( output), "{}\n" ); 320} 321 322# [ cfg ( windows )] 323# [ test ] 324fn ascii_case_folding_is_shared_by_storage_and_permissions () { 325let cwd =Path :: new ( "." ); 326let mut initial =Initial :: new ( true ); 327 initial. grant ( Category :: Env , false , "path" , cwd). unwrap (); 328let caps =Capabilities :: new ( initial, cwd. to_owned ()); 329let mut env =Environment :: new ([( "Path" . into (), "original" . into ())]). unwrap (); 330assert_eq! ( env. get ( & caps, "PATH" ). unwrap (). as_deref (), Some ( "original" )); 331assert_eq! ( 332 env. to_object ( & caps), 333vec! [( "Path" . into (), "original" . into ())] 334); 335 env. set ( & caps, "pAtH" , "changed" . into ()). unwrap (); 336assert_eq! ( env. get ( & caps, "path" ). unwrap (). as_deref (), Some ( "changed" )); 337assert_eq! ( env. to_object ( & caps). len (), 1 ); 338 caps. pledge ( vec! [( Category :: Env , None , vec! [ "PaTh" . into ()])]) 339. unwrap (); 340assert! ( env. get ( & caps, "PATH" ). is_err ()); 341assert! ( env. to_object ( & caps). is_empty ()); 342} 343 344# [ cfg ( unix )] 345# [ test ] 346fn unix_names_are_case_sensitive_in_storage_and_permissions () { 347let cwd =Path :: new ( "." ); 348let mut initial =Initial :: new ( true ); 349 initial. grant ( Category :: Env , false , "Path" , cwd). unwrap (); 350let caps =Capabilities :: new ( initial, cwd. to_owned ()); 351let env =Environment :: new ([ 352( "Path" . into (), "allowed" . into ()), 353( "PATH" . into (), "secret" . into ()), 354]) 355. unwrap (); 356assert_eq! ( env. get ( & caps, "Path" ). unwrap (). as_deref (), Some ( "allowed" )); 357assert! ( env. get ( & caps, "PATH" ). is_err ()); 358assert_eq! ( 359 env. to_object ( & caps), 360vec! [( "Path" . into (), "allowed" . into ())] 361); 362}