char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
38d7f6c
main
1mod common; 2 3# [ test ] 4fn std_facades_preserve_namespaces_and_class_bindings () { 5let output = common:: run ( 6r#" 7const std = await import('avril:std'), sync = await import('avril:std/sync'); 8const expected = [ 9'ChildProcess', 'Conn', 'DatagramConn', 'FileHandle', 'FsWatcher', 'Listener', 10'OutputLimitError', 'ProcessAbortError', 'ProcessError', 'ProcessTimeoutError', 11'SeekMode', 'addSignalListener', 'avril', 'chmod', 'chown', 'connect', 'copyFile', 12'create', 'env', 'errors', 'inspect', 'lchown', 'link', 'listen', 'listenDatagram', 13'lstat', 'lutime', 'mkdir', 'open', 'pledge', 'process', 'readDir', 'readLink', 14'realPath', 'remove', 'removeSignalListener', 'rename', 'run', 'runSync', 'runtime', 15'serve', 'serveFile', 'spawn', 'stat', 'stderr', 'stdin', 'stdout', 'symlink', 16'truncate', 'upgradeWebSocket', 'utime', 'watchFs', 17]; 18const expectedSync = [ 19'FileHandle', 'SeekMode', 'chmod', 'chown', 'copyFile', 'create', 'errors', 'lchown', 20'link', 'lstat', 'lutime', 'mkdir', 'open', 'readDir', 'readLink', 'realPath', 21'remove', 'rename', 'stat', 'stderr', 'stdin', 'stdout', 'symlink', 'truncate', 'utime', 22]; 23if (Object.keys(std).sort().join() !== expected.join()) throw Error('std exports'); 24if (Object.keys(sync).sort().join() !== expectedSync.join()) throw Error('sync exports'); 25for (const name of ['errors', 'FileHandle', 'SeekMode']) { 26if (std[name] !== sync[name]) throw Error('sync identity: ' + name); 27} 28for (const name of ['Conn', 'Listener', 'DatagramConn', 'FileHandle', 'FsWatcher', 29'ChildProcess', 'ProcessError', 'OutputLimitError', 'ProcessTimeoutError', 'ProcessAbortError']) { 30if (Object.hasOwn(std[name], Symbol.hasInstance)) throw Error('Runtime hasInstance: ' + name); 31} 32if (std.Conn.length !== 0 || std.Listener.length !== 0) throw Error('Constructor arity'); 33for (const name of ['run', 'runSync', 'spawn']) { 34if (std[name].length !== 1 || Object.hasOwn(std[name], 'prototype')) throw Error('Function shape: ' + name); 35} 36const result = {code: 1, signal: null, success: false, reason: null, error: null, stdout: null, stderr: null}; 37const error = new std.OutputLimitError(result); 38if (!(error instanceof std.OutputLimitError) || !(error instanceof std.ProcessError) || 39!(error instanceof Error) || error.result !== result) throw Error('Error inheritance'); 40for (const name of ['ProcessError', 'OutputLimitError', 'ProcessTimeoutError', 'ProcessAbortError']) { 41if (std[name].isError !== Error.isError || !std[name].isError(error) || std[name].isError({})) 42throw Error('Error statics: ' + name); 43} 44console.log('facades'); 45"# , 46& [], 47); 48assert_eq! ( output. stdout , b"facades\n" ); 49} 50 51# [ test ] 52fn base_snapshots_and_pledge_conversion_boundaries_are_preserved () { 53let output = common:: run ( 54r#" 55import {env, pledge, process, runtime} from 'avril:std'; 56const reads = []; 57const grant = {toString() {reads.push('string'); return 'AVRIL_TYPED_BASE';}}; 58const grants = []; 59Object.defineProperty(grants, 0, {get() {reads.push('grant'); return grant;}}); 60grants[Symbol.iterator] = () => {throw Error('Pledge iterated grants');}; 61pledge({get env() { 62reads.push('env'); 63return { 64get allow() {reads.push('allow'); return grants;}, 65get deny() {reads.push('deny'); return [];}, 66}; 67}}); 68if (reads.join(',') !== 'env,allow,grant,string,deny') throw Error('Pledge reads: ' + reads); 69const name = {toString() {reads.push('name'); return 'AVRIL_TYPED_BASE';}}; 70const value = {toString() {reads.push('value'); return 'saved';}}; 71env.set(name, value); 72if (reads.slice(-2).join(',') !== 'name,value') throw Error('Environment conversion'); 73const snapshot = env.toObject(); 74env.delete(name); 75if (Object.getPrototypeOf(snapshot) !== null || snapshot.AVRIL_TYPED_BASE !== 'saved' || env.has(name)) 76throw Error('Environment snapshot'); 77if (!Object.isFrozen(env) || !Object.isFrozen(process) || !Object.isFrozen(process.args) || 78!Object.isFrozen(runtime) || typeof process.pid !== 'number' || typeof runtime.v8 !== 'string') 79throw Error('Runtime initialization'); 80let read = false; 81try { 82pledge({unknown: false, get env() {read = true; return false;}}); 83throw Error('Unknown pledge accepted'); 84} catch (error) {if (!(error instanceof TypeError)) throw error;} 85if (read) throw Error('Pledge read values before validating names'); 86if (env.set.length !== 2 || env.get.length !== 1 || pledge.length !== 1) throw Error('Base arity'); 87console.log('base conversion'); 88"# , 89& [], 90); 91assert_eq! ( output. stdout , b"base conversion\n" ); 92} 93 94# [ test ] 95fn stdlib_exports_retained_bindings_not_mutable_global_lookups () { 96let output = common:: run ( 97r#" 98const avril = Symbol.for('runtime:avril'); 99if (Symbol.for('__avril.base') in globalThis) throw new Error('Leaked base'); 100if ('pledge' in globalThis) throw new Error('Unexpected global pledge'); 101const poison = () => { throw new Error('Poisoned global'); }; 102globalThis.pledge = poison; 103Symbol.for = poison; globalThis.Symbol = poison; 104globalThis.Promise = poison; Object.freeze = poison; 105const std = await import('avril:std'); 106const again = await import(new URL('avril:std')); 107try { std.pledge = poison; throw new Error('Unsealed std'); } 108catch(e) { if (!(e instanceof TypeError)) throw e; } 109if (Object.isFrozen(std.stdin) || Object.isFrozen(std.stdout) || Object.isFrozen(std.stderr)) throw new Error('Frozen streams'); 110if (std.avril !== avril) throw new Error('Wrong avril symbol'); 111if (std !== again || std.stdin !== again.stdin 112|| std.stdout !== again.stdout || std.stderr !== again.stderr) throw new Error('Wrong bindings'); 113std.pledge({jsExec:false}); 114std.pledge({net:false,read:false,jsExec:false}); 115if ((await import('avril:std')) !== std) throw new Error('Lost builtin access'); 116console.log('retained'); 117"# , 118& [], 119); 120assert_eq! ( output. stdout , b"retained\n" ); 121} 122 123# [ test ] 124fn typescript_imports_stdlib_and_invalid_exports_fail_normally () { 125let dir = tempfile:: tempdir (). unwrap (); 126let path = dir. path (). join ( "main.ts" ); 127 std:: fs:: write ( 128& path, 129r#" 130import { pledge, stdout } from 'avril:std'; 131import type { Pledge } from 'avril:std'; 132const restrictions: Pledge = {net:false}; 133pledge(restrictions); 134await stdout.getWriter().write(new Uint8Array([1,2,3])); 135for (const name of ['avril:missing','avril:std?query','avril:std#fragment']) { 136try { await import(name); throw new Error('Unexpected builtin'); } 137catch(e) { if (!e.message.includes('Unsupported module specifier')) throw e; } 138} 139"# , 140) 141. unwrap (); 142let output = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 143. arg ( "run" ) 144. args ([ "-0" , path. to_str (). unwrap ()]) 145. output () 146. unwrap (); 147assert! ( 148 output. status . success (), 149"{}" , 150String :: from_utf8_lossy ( & output. stderr ) 151); 152assert_eq! ( output. stdout , [ 1 , 2 , 3 ]); 153 std:: fs:: write ( 154& path, 155"import { missing } from 'avril:std'; console.log(missing);" , 156) 157. unwrap (); 158let output = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 159. arg ( "run" ) 160. arg ( path) 161. output () 162. unwrap (); 163assert! ( !output. status . success ()); 164assert! ( 165String :: from_utf8_lossy ( & output. stderr ) 166. contains ( "does not provide an export named 'missing'" ) 167); 168} 169 170# [ test ] 171fn embedded_modules_stay_private_with_and_without_ambient_permissions () { 172let dir = tempfile:: tempdir (). unwrap (); 173let path = dir. path (). join ( "main.js" ); 174for zeroin [ false , true ] { 175 std:: fs:: write ( & path, r#" 176import {pledge} from 'avril:std'; 177async function denied(promise) { 178try {await promise;} catch(e) {if(e.message.includes('private')) return;throw e;} 179throw Error('Extension module escaped'); 180} 181for(const name of ['ext:core','ext:core/ops','ext:core/primordials','ext:web/idl.js','ext:web/events.js','ext:http_types/headers.js','ext:http_types/body.js','ext:encoding/main.js']) await denied(import(name)); 182new Headers({a:"globalThis.fromEval=import('ext:fetch/main.js')"}).forEach(eval); 183await denied(fromEval); 184new URLSearchParams('a='+encodeURIComponent("globalThis.fromParams=import('ext:web/main.js')")).forEach(eval); 185await denied(fromParams); 186pledge({net:false,read:false,jsExec:false}); 187if ((await import('avril:std')).pledge !== pledge) throw Error('Lost public builtin'); 188if (new TextDecoder().decode(new TextEncoder().encode('private')) !== 'private') throw Error('Lost encoding'); 189console.log('private'); 190"# ). unwrap (); 191let mut command = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )); 192 command. arg ( "run" ); 193if zero{ 194 command. arg ( "-0" ); 195} 196let output = command. arg ( & path). output (). unwrap (); 197assert! ( 198 output. status . success (), 199"{}" , 200String :: from_utf8_lossy ( & output. stderr ) 201); 202assert_eq! ( output. stdout , b"private\n" ); 203 std:: fs:: write ( & path, "import 'ext:web/idl.js'; throw Error('Escaped');" ). unwrap (); 204let mut command = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )); 205 command. arg ( "run" ); 206if zero{ 207 command. arg ( "-0" ); 208} 209let output = command. arg ( & path). output (). unwrap (); 210assert! ( !output. status . success ()); 211assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "Extension modules are private" )); 212} 213}