char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
11.0 KiB179 linesraw
1mod common;
2
3#[test]
4fn web_globals_expose_self_navigator_and_performance() {
5    let output = common::run(
6        r#"
7        const global = globalThis;
8        if (self !== global || self !== globalThis) throw Error('self alias');
9        const selfDescriptor = Object.getOwnPropertyDescriptor(global, 'self');
10        if (selfDescriptor.get === undefined || selfDescriptor.enumerable !== true || !selfDescriptor.configurable) throw Error('self descriptor');
11        if (navigator !== global.navigator || !(navigator instanceof WorkerNavigator) || typeof Navigator !== 'undefined') throw Error('Navigator interface');
12        if (Object.prototype.toString.call(navigator) !== '[object WorkerNavigator]') throw Error('Navigator tag');
13        if (!navigator.userAgent.startsWith('avril/') || navigator.appCodeName !== 'Mozilla' || navigator.appName !== 'Netscape' || navigator.appVersion !== '4.0' || navigator.product !== 'Gecko') throw Error('Navigator identification');
14        if (!navigator.platform || navigator.language !== 'en-US' || navigator.languages !== navigator.languages || !Object.isFrozen(navigator.languages) || navigator.languages.join() !== 'en-US' || !Number.isInteger(navigator.hardwareConcurrency) || navigator.hardwareConcurrency < 1 || !navigator.onLine) throw Error('Navigator mixins');
15        const navigatorDescriptor = Object.getOwnPropertyDescriptor(global, 'navigator');
16        if (!navigatorDescriptor.get || navigatorDescriptor.enumerable !== true || !navigatorDescriptor.configurable) throw Error('navigator descriptor');
17        try { new WorkerNavigator(); throw Error('Navigator constructor was callable'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
18        const userAgent = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgent').get;
19        try { userAgent.call({}); throw Error('Navigator brand check'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
20
21        if (performance !== global.performance || !(performance instanceof Performance) || !(performance instanceof EventTarget)) throw Error('Performance identity');
22        if (Object.prototype.toString.call(performance) !== '[object Performance]') throw Error('Performance tag');
23        if (typeof performance.timeOrigin !== 'number' || performance.timeOrigin <= 0 || performance.now() < 0) throw Error('Performance clock');
24        const opsSymbol = Symbol.for('__avril.ops');
25        if (opsSymbol in global || global[opsSymbol] !== undefined || Reflect.ownKeys(global).includes(opsSymbol)) throw Error('Native op registry exposed');
26        const before = performance.now(), after = performance.now();
27        if (after < before) throw Error('Performance clock moved backwards');
28        const sample = performance.now();
29        if (Math.abs(sample * 10 - Math.round(sample * 10)) > 1e-7) throw Error('Performance precision');
30        if (Math.abs(Date.now() - (performance.timeOrigin + performance.now())) > 5000) throw Error('Time origin');
31        const start = performance.now();
32        await new Promise(resolve => setTimeout(resolve, 10));
33        if (performance.now() < start + 5) throw Error('Timer and performance clocks diverged');
34        const json = performance.toJSON();
35        if (Object.keys(json).join() !== 'timeOrigin' || json.timeOrigin !== performance.timeOrigin) throw Error('Performance JSON');
36        for (const call of [() => new Performance(), () => performance.now.call({}), () => Object.getOwnPropertyDescriptor(Performance.prototype, 'timeOrigin').get.call({}), () => Performance.prototype.toJSON.call({})]) {
37            try { call(); throw Error('Performance brand or constructor'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
38        }
39        const performanceDescriptor = Object.getOwnPropertyDescriptor(global, 'performance');
40        if (!performanceDescriptor.get || performanceDescriptor.enumerable !== true || !performanceDescriptor.configurable) throw Error('performance descriptor');
41        if ('op_navigator_metadata' in global || 'op_timer_time_origin' in global) throw Error('Native helper leaked');
42        console.log('globals');
43    "#,
44        &[],
45    );
46    assert_eq!(output.stdout, b"globals\n");
47}
48
49#[test]
50fn base64_globals_follow_forgiving_decode_and_latin1_encode() {
51    let output = common::run(
52        r#"
53        if (btoa('') !== '' || btoa('f') !== 'Zg==' || btoa('fo') !== 'Zm8=' || btoa('foo') !== 'Zm9v' || btoa('\x00\x80\xff') !== 'AID/') throw Error('btoa vectors');
54        if (atob('') !== '' || atob(' Z m 9 v \t\n') !== 'foo' || atob('AQ') !== '\x01' || atob('AQI') !== '\x01\x02' || atob('AQIDBA==') !== '\x01\x02\x03\x04' || atob('AB==') !== '\x00') throw Error('atob vectors');
55        if (atob(btoa('binary\x00\x80\xff')) !== 'binary\x00\x80\xff' || atob('\f') !== '') throw Error('Base64 round trip');
56        const long = 'x'.repeat(10000);
57        if (atob(btoa(long)) !== long) throw Error('Large Base64 round trip');
58        function fails(fn, name, code) {
59            try { fn(); throw Error('Expected ' + name); }
60            catch (error) {
61                if (error.message === 'Expected ' + name) throw error;
62                if (!(error instanceof DOMException) || error.name !== name || error.code !== code) throw error;
63            }
64        }
65        fails(() => btoa('éĀ'), 'InvalidCharacterError', 5);
66        fails(() => btoa('\ud800'), 'InvalidCharacterError', 5);
67        for (const value of ['A', 'AA===', 'AQ\v', 'AQ\u00a0', 'é', '====']) fails(() => atob(value), 'InvalidCharacterError', 5);
68        for (const fn of [() => atob(), () => btoa()]) {
69            try { fn(); throw Error('Missing argument accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
70        }
71        try { btoa(Symbol()); throw Error('Symbol accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
72        if (DOMException.INVALID_CHARACTER_ERR !== 5 || DOMException.prototype.INVALID_CHARACTER_ERR !== 5 || new DOMException('', 'QuotaExceededError').code !== 22 || DOMException.QUOTA_EXCEEDED_ERR !== 22 || new DOMException('', 'TypeMismatchError').code !== 17 || DOMException.TYPE_MISMATCH_ERR !== 17) throw Error('DOMException legacy codes');
73        for(const name of ['DOMStringSizeError','NoDataAllowedError','ValidationError']){
74            if(new DOMException('',name).code!==0)throw Error('Obsolete exception name got a code');
75        }
76        const codeGetter=Object.getOwnPropertyDescriptor(DOMException.prototype,'code').get;
77        try{codeGetter.call({name:'AbortError'});throw Error('Unbranded exception accepted');}catch(e){if(!(e instanceof TypeError))throw e;}
78        const exception=new DOMException('','AbortError');
79        Object.defineProperty(exception,'name',{value:'TimeoutError'});
80        if(exception.code!==20)throw Error('Public name changed native exception code');
81        if (atob.length !== 1 || btoa.length !== 1 || Object.getOwnPropertyDescriptor(globalThis, 'atob').enumerable !== true || Object.getOwnPropertyDescriptor(globalThis, 'btoa').enumerable !== true) throw Error('Base64 descriptors');
82        console.log('base64');
83    "#,
84        &[],
85    );
86    assert_eq!(output.stdout, b"base64\n");
87}
88
89#[test]
90fn web_globals_capture_intrinsics_and_native_clocks() {
91    let output = common::run(
92        r#"
93        const now = performance.now.bind(performance), origin = performance.timeOrigin;
94        String.prototype.charCodeAt = () => {throw Error('charCodeAt');};
95        String.prototype.slice = () => {throw Error('slice');};
96        String.fromCharCode = () => {throw Error('fromCharCode');};
97        Array.prototype.push = () => {throw Error('push');};
98        Array.prototype.join = () => {throw Error('join');};
99        Date.now = () => {throw Error('Date.now');};
100        if (btoa('foo') !== 'Zm9v' || atob('Zm9v') !== 'foo' || now() < 0 || performance.timeOrigin !== origin) throw Error('Captured operations');
101        console.log('captured');
102    "#,
103        &[],
104    );
105    assert_eq!(output.stdout, b"captured\n");
106}
107
108#[test]
109fn base64_operations_are_not_constructors_and_replaceable_globals_can_be_reassigned() {
110    let output = common::run(
111        r#"
112        for(const fn of [atob,btoa]){
113            try{new fn('');throw Error('Operation used as constructor');}catch(e){if(!(e instanceof TypeError))throw e;}
114        }
115        const original=self;
116        self={replacement:true};
117        performance={replacement:true};
118        if(!self.replacement||!performance.replacement||globalThis!==original)throw Error('Globals not replaceable');
119        for(const name of ['self','performance']){
120            const descriptor=Object.getOwnPropertyDescriptor(globalThis,name);
121            if(!descriptor.writable||!descriptor.enumerable||!descriptor.configurable||descriptor.get)throw Error('Replacement descriptor');
122        }
123        console.log('replaceable');
124    "#,
125        &[],
126    );
127    assert_eq!(output.stdout, b"replaceable\n");
128}
129
130#[test]
131fn global_avril_symbol_marks_the_runtime_in_main_and_workers() {
132    let output = common::run(
133        r#"
134        import { avril } from 'avril:std';
135        const key = Symbol.for('runtime:avril');
136        if (avril !== key) throw Error('avril symbol');
137        if (globalThis[key] !== true || globalThis[avril] !== true) throw Error('Marker');
138        const descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
139        if (!descriptor || descriptor.value !== true || descriptor.writable !== false || descriptor.enumerable !== false || descriptor.configurable !== false) throw Error('Descriptor');
140        if (Reflect.ownKeys(globalThis).includes(key) !== true) throw Error('Own key');
141        try { globalThis[key] = false; } catch {}
142        if (globalThis[key] !== true) throw Error('Writable');
143        try { Object.defineProperty(globalThis, key, { value: false }); throw Error('Redefined'); }
144        catch (error) { if (!(error instanceof TypeError)) throw error; }
145        console.log('marker');
146    "#,
147        &[],
148    );
149    assert_eq!(output.stdout, b"marker\n");
150    let dir = tempfile::tempdir().unwrap();
151    std::fs::write(
152        dir.path().join("worker.js"),
153        r#"
154        postMessage(globalThis[Symbol.for('runtime:avril')]); close();
155    "#,
156    )
157    .unwrap();
158    std::fs::write(dir.path().join("main.js"), r#"
159        const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
160        const marker = await new Promise((resolve, reject) => { worker.onmessage = event => resolve(event.data); worker.onerror = reject; });
161        if (marker !== true) throw Error('Worker marker');
162        console.log('worker marker');
163    "#).unwrap();
164    let output = common::wait(
165        std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
166            .arg("run")
167            .arg(dir.path().join("main.js"))
168            .stdout(std::process::Stdio::piped())
169            .stderr(std::process::Stdio::piped())
170            .spawn()
171            .unwrap(),
172    );
173    assert!(
174        output.status.success(),
175        "{}",
176        String::from_utf8_lossy(&output.stderr)
177    );
178    assert_eq!(output.stdout, b"worker marker\n");
179}