mod common; #[test] fn exposed_constructors_ignore_reparenting_and_preserve_subclassing() { let output = common::run( r#" import { errors } from 'avril:std'; let calls = 0; function Other() { calls++; } for (const parent of [Other, null]) { for (const ctor of [DOMException, CustomEvent, ErrorEvent, PromiseRejectionEvent, MessageEvent, AbortSignal, MessagePort, ...Object.values(errors)]) { Object.setPrototypeOf(ctor, parent); } const exception = new DOMException('message', 'AbortError'); if (!(exception instanceof Error) || exception.message !== 'message' || exception.code !== 20) throw Error('Exception initialization'); const custom = new CustomEvent('custom', { detail: 42, cancelable: true }); const error = new ErrorEvent('error', { message: 'message', lineno: 7 }); const promise = Promise.resolve(); const rejection = new PromiseRejectionEvent('rejection', { promise, reason: 9 }); const message = new MessageEvent('message', { data: 3, origin: 'origin' }); if (custom.detail !== 42 || !custom.cancelable || error.message !== 'message' || error.lineno !== 7 || rejection.promise !== promise || rejection.reason !== 9 || message.data !== 3 || message.origin !== 'origin') throw Error('Event initialization'); const target = new EventTarget(); let events = 0; target.addEventListener('custom', () => events++); target.dispatchEvent(custom); if (events !== 1 || custom.target !== target) throw Error('Event base state'); const controller = new AbortController(); let aborted = false; controller.signal.addEventListener('abort', () => aborted = true); controller.abort(11); if (!aborted || controller.signal.reason !== 11 || !AbortSignal.abort().aborted) throw Error('Signal base state'); const channel = new MessageChannel(); let delivered = false; channel.port2.onmessage = event => { if (event.data !== 12 || event.type !== 'message' || event.target !== channel.port2) throw Error('Port delivery'); delivered = true; }; channel.port1.postMessage(12); await new Promise(resolve => setTimeout(resolve, 10)); if (!delivered) throw Error('MessagePort base state'); channel.port1.close(); channel.port2.close(); for (const [name, ctor] of Object.entries(errors)) { const error = new ctor('failure'); if (!(error instanceof Error) || error.name !== name || error.message !== 'failure') throw Error('File error initialization'); } class Child extends CustomEvent { #value = 13; get value() { return this.#value; } } const child = new Child('child', { detail: 14 }); if (!(child instanceof Child) || !(child instanceof CustomEvent) || !(child instanceof Event) || child.type !== 'child' || child.detail !== 14 || child.value !== 13) throw Error('Guest subclass'); } if (calls) throw Error('Guest superclass called'); console.log('constructors safe'); "#, &[], ); assert_eq!(output.stdout, b"constructors safe\n"); } #[test] fn message_event_initialization_uses_the_captured_base_method() { let output = common::run( r#" Event.prototype.initEvent = () => { throw Error('Guest base method'); }; const event = new MessageEvent('before'); event.initMessageEvent('after', true, true, 42); if (event.type !== 'after' || !event.bubbles || !event.cancelable || event.data !== 42) throw Error('Event initialization'); console.log('base method safe'); "#, &[], ); assert_eq!(output.stdout, b"base method safe\n"); } #[test] fn file_write_options_do_not_iterate_internal_arrays() { let output = common::run( r#" import { writeTextFile, readTextFile } from 'avril:fs'; Array.prototype[Symbol.iterator] = () => { throw Error('Guest array iterator'); }; const path = new URL('./data', import.meta.url); await writeTextFile(path, 'safe'); await writeTextFile(path, ' append', { append: true }); if (await readTextFile(path) !== 'safe append') throw Error('Write flags'); console.log('file options safe'); "#, &[], ); assert_eq!(output.stdout, b"file options safe\n"); } const PROMISE_TEST: &str = r#" import { stdin } from 'avril:std'; import * as fs from 'avril:fs'; const originalPromise = Promise, originalThen = Promise.prototype.then; function observe(promise) { Object.defineProperty(promise, 'constructor', { value: undefined, configurable: true }); const result = Reflect.apply(originalThen, promise, [value => value, error => { throw error; }]); Object.defineProperty(result, 'constructor', { value: originalPromise }); return result; } let hooks = 0; const poison = () => { hooks++; throw Error('Guest promise hook'); }; POISON const response = new Response(new Blob(['hello']).stream()); if (await observe(response.text()) !== 'hello') throw Error('Body consumption'); const source = new ReadableStream({ start(controller) { controller.enqueue('chunk'); controller.close(); }, }); const clone = structuredClone(source, { transfer: [source] }); const reader = clone.getReader(); if ((await observe(reader.read())).value !== 'chunk' || !(await observe(reader.read())).done) throw Error('Stream transfer'); reader.releaseLock(); const iterator = new ReadableStream({ start(controller) { controller.close(); } }).values(); if (!(await observe(iterator.next())).done || !(await observe(iterator.next())).done || !(await observe(iterator.return())).done) throw Error('Closed stream iterator'); let writes = ''; const stream = new ReadableStream({ start(controller) { controller.enqueue('pipe'); controller.close(); } }); await observe(stream.pipeTo(new WritableStream({ write(chunk) { writes += chunk; return new originalPromise(resolve => resolve()); } }))); if (writes !== 'pipe') throw Error('Promise callback assimilation'); const abortController = new AbortController(); const piping = new ReadableStream().pipeTo(new WritableStream(), { signal: abortController.signal }); abortController.abort(42); try { await observe(piping); throw Error('Abort accepted'); } catch (error) { if (error !== 42) throw error; } const bytes = await observe(new Response(new Blob(['hello']).stream().pipeThrough(new CompressionStream('gzip'))).bytes()); if (await observe(new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))).text()) !== 'hello') throw Error('Codec promises'); const path = new URL('./data', import.meta.url); await observe(fs.writeTextFile(path, 'file')); if (await observe(fs.readTextFile(path)) !== 'file') throw Error('File promises'); const input = stdin.getReader(); if (new TextDecoder().decode((await observe(input.read())).value) !== 'stdin') throw Error('Stdio promises'); input.releaseLock(); const wasm = new Response(new Uint8Array([0,97,115,109,1,0,0,0]), { headers: { 'content-type': 'application/wasm' } }); if (!(await observe(WebAssembly.compileStreaming(new originalPromise(resolve => resolve(wasm)))) instanceof WebAssembly.Module)) throw Error('Wasm promises'); let thenableCalls = 0; await observe(new WritableStream({ start() { return { then(resolve) { thenableCalls++; resolve(); } }; } }).close()); if (thenableCalls !== 1 || hooks !== 0) throw Error('Promise protocol'); console.log('promises safe'); "#; #[test] fn internal_reactions_ignore_promise_species() { promises("Object.defineProperty(Promise, Symbol.species, { get: poison });"); } #[test] fn internal_awaits_ignore_promise_constructor_getters() { promises( "Object.defineProperty(Promise.prototype, 'constructor', { get: poison, configurable: true });", ); } #[test] fn internal_assimilation_ignores_replaced_promise_then() { promises( "Promise.prototype.then = poison; Promise.resolve = poison; Promise.reject = poison; Promise.all = poison;", ); } fn promises(poison: &str) { let source = PROMISE_TEST.replace("POISON", poison); let output = common::run(&source, b"stdin"); assert_eq!(output.stdout, b"promises safe\n"); }