mod common; #[test] fn std_facades_preserve_namespaces_and_class_bindings() { let output = common::run( r#" const std = await import('avril:std'), sync = await import('avril:std/sync'); const expected = [ 'ChildProcess', 'Conn', 'DatagramConn', 'FileHandle', 'FsWatcher', 'Listener', 'OutputLimitError', 'ProcessAbortError', 'ProcessError', 'ProcessTimeoutError', 'SeekMode', 'addSignalListener', 'avril', 'chmod', 'chown', 'connect', 'copyFile', 'create', 'env', 'errors', 'inspect', 'lchown', 'link', 'listen', 'listenDatagram', 'lstat', 'lutime', 'mkdir', 'open', 'pledge', 'process', 'readDir', 'readLink', 'realPath', 'remove', 'removeSignalListener', 'rename', 'run', 'runSync', 'runtime', 'serve', 'serveFile', 'spawn', 'stat', 'stderr', 'stdin', 'stdout', 'symlink', 'truncate', 'upgradeWebSocket', 'utime', 'watchFs', ]; const expectedSync = [ 'FileHandle', 'SeekMode', 'chmod', 'chown', 'copyFile', 'create', 'errors', 'lchown', 'link', 'lstat', 'lutime', 'mkdir', 'open', 'readDir', 'readLink', 'realPath', 'remove', 'rename', 'stat', 'stderr', 'stdin', 'stdout', 'symlink', 'truncate', 'utime', ]; if (Object.keys(std).sort().join() !== expected.join()) throw Error('std exports'); if (Object.keys(sync).sort().join() !== expectedSync.join()) throw Error('sync exports'); for (const name of ['errors', 'FileHandle', 'SeekMode']) { if (std[name] !== sync[name]) throw Error('sync identity: ' + name); } for (const name of ['Conn', 'Listener', 'DatagramConn', 'FileHandle', 'FsWatcher', 'ChildProcess', 'ProcessError', 'OutputLimitError', 'ProcessTimeoutError', 'ProcessAbortError']) { if (Object.hasOwn(std[name], Symbol.hasInstance)) throw Error('Runtime hasInstance: ' + name); } if (std.Conn.length !== 0 || std.Listener.length !== 0) throw Error('Constructor arity'); for (const name of ['run', 'runSync', 'spawn']) { if (std[name].length !== 1 || Object.hasOwn(std[name], 'prototype')) throw Error('Function shape: ' + name); } const result = {code: 1, signal: null, success: false, reason: null, error: null, stdout: null, stderr: null}; const error = new std.OutputLimitError(result); if (!(error instanceof std.OutputLimitError) || !(error instanceof std.ProcessError) || !(error instanceof Error) || error.result !== result) throw Error('Error inheritance'); for (const name of ['ProcessError', 'OutputLimitError', 'ProcessTimeoutError', 'ProcessAbortError']) { if (std[name].isError !== Error.isError || !std[name].isError(error) || std[name].isError({})) throw Error('Error statics: ' + name); } console.log('facades'); "#, &[], ); assert_eq!(output.stdout, b"facades\n"); } #[test] fn base_snapshots_and_pledge_conversion_boundaries_are_preserved() { let output = common::run( r#" import {env, pledge, process, runtime} from 'avril:std'; const reads = []; const grant = {toString() {reads.push('string'); return 'AVRIL_TYPED_BASE';}}; const grants = []; Object.defineProperty(grants, 0, {get() {reads.push('grant'); return grant;}}); grants[Symbol.iterator] = () => {throw Error('Pledge iterated grants');}; pledge({get env() { reads.push('env'); return { get allow() {reads.push('allow'); return grants;}, get deny() {reads.push('deny'); return [];}, }; }}); if (reads.join(',') !== 'env,allow,grant,string,deny') throw Error('Pledge reads: ' + reads); const name = {toString() {reads.push('name'); return 'AVRIL_TYPED_BASE';}}; const value = {toString() {reads.push('value'); return 'saved';}}; env.set(name, value); if (reads.slice(-2).join(',') !== 'name,value') throw Error('Environment conversion'); const snapshot = env.toObject(); env.delete(name); if (Object.getPrototypeOf(snapshot) !== null || snapshot.AVRIL_TYPED_BASE !== 'saved' || env.has(name)) throw Error('Environment snapshot'); if (!Object.isFrozen(env) || !Object.isFrozen(process) || !Object.isFrozen(process.args) || !Object.isFrozen(runtime) || typeof process.pid !== 'number' || typeof runtime.v8 !== 'string') throw Error('Runtime initialization'); let read = false; try { pledge({unknown: false, get env() {read = true; return false;}}); throw Error('Unknown pledge accepted'); } catch (error) {if (!(error instanceof TypeError)) throw error;} if (read) throw Error('Pledge read values before validating names'); if (env.set.length !== 2 || env.get.length !== 1 || pledge.length !== 1) throw Error('Base arity'); console.log('base conversion'); "#, &[], ); assert_eq!(output.stdout, b"base conversion\n"); } #[test] fn stdlib_exports_retained_bindings_not_mutable_global_lookups() { let output = common::run( r#" const avril = Symbol.for('runtime:avril'); if (Symbol.for('__avril.base') in globalThis) throw new Error('Leaked base'); if ('pledge' in globalThis) throw new Error('Unexpected global pledge'); const poison = () => { throw new Error('Poisoned global'); }; globalThis.pledge = poison; Symbol.for = poison; globalThis.Symbol = poison; globalThis.Promise = poison; Object.freeze = poison; const std = await import('avril:std'); const again = await import(new URL('avril:std')); try { std.pledge = poison; throw new Error('Unsealed std'); } catch(e) { if (!(e instanceof TypeError)) throw e; } if (Object.isFrozen(std.stdin) || Object.isFrozen(std.stdout) || Object.isFrozen(std.stderr)) throw new Error('Frozen streams'); if (std.avril !== avril) throw new Error('Wrong avril symbol'); if (std !== again || std.stdin !== again.stdin || std.stdout !== again.stdout || std.stderr !== again.stderr) throw new Error('Wrong bindings'); std.pledge({jsExec:false}); std.pledge({net:false,read:false,jsExec:false}); if ((await import('avril:std')) !== std) throw new Error('Lost builtin access'); console.log('retained'); "#, &[], ); assert_eq!(output.stdout, b"retained\n"); } #[test] fn typescript_imports_stdlib_and_invalid_exports_fail_normally() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("main.ts"); std::fs::write( &path, r#" import { pledge, stdout } from 'avril:std'; import type { Pledge } from 'avril:std'; const restrictions: Pledge = {net:false}; pledge(restrictions); await stdout.getWriter().write(new Uint8Array([1,2,3])); for (const name of ['avril:missing','avril:std?query','avril:std#fragment']) { try { await import(name); throw new Error('Unexpected builtin'); } catch(e) { if (!e.message.includes('Unsupported module specifier')) throw e; } } "#, ) .unwrap(); let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .args(["-0", path.to_str().unwrap()]) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, [1, 2, 3]); std::fs::write( &path, "import { missing } from 'avril:std'; console.log(missing);", ) .unwrap(); let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .arg(path) .output() .unwrap(); assert!(!output.status.success()); assert!( String::from_utf8_lossy(&output.stderr) .contains("does not provide an export named 'missing'") ); } #[test] fn embedded_modules_stay_private_with_and_without_ambient_permissions() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("main.js"); for zero in [false, true] { std::fs::write(&path,r#" import {pledge} from 'avril:std'; async function denied(promise) { try {await promise;} catch(e) {if(e.message.includes('private')) return;throw e;} throw Error('Extension module escaped'); } for(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)); new Headers({a:"globalThis.fromEval=import('ext:fetch/main.js')"}).forEach(eval); await denied(fromEval); new URLSearchParams('a='+encodeURIComponent("globalThis.fromParams=import('ext:web/main.js')")).forEach(eval); await denied(fromParams); pledge({net:false,read:false,jsExec:false}); if ((await import('avril:std')).pledge !== pledge) throw Error('Lost public builtin'); if (new TextDecoder().decode(new TextEncoder().encode('private')) !== 'private') throw Error('Lost encoding'); console.log('private'); "#).unwrap(); let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_avril")); command.arg("run"); if zero { command.arg("-0"); } let output = command.arg(&path).output().unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"private\n"); std::fs::write(&path, "import 'ext:web/idl.js'; throw Error('Escaped');").unwrap(); let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_avril")); command.arg("run"); if zero { command.arg("-0"); } let output = command.arg(&path).output().unwrap(); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("Extension modules are private")); } }