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
13.6 KiB197 linesraw
1mod common;
2
3#[test]
4fn navigator_exposes_all_html_worker_mixins_with_web_idl_brands() {
5    let output = common::run(
6        r#"
7        const attributes = ['appCodeName','appName','appVersion','platform','product','userAgent',
8            'language','languages','onLine','hardwareConcurrency'];
9        for (const name of attributes) {
10            const descriptor = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype,name);
11            if (!descriptor.get || !!descriptor.set !== (name === 'userAgent') || !descriptor.enumerable || !descriptor.configurable) throw Error('Descriptor '+name);
12            for (const receiver of [{},WorkerNavigator.prototype,new EventTarget()]) {
13                try {descriptor.get.call(receiver); throw Error('Brand accepted '+name);} catch (error) {if (!(error instanceof TypeError)) throw error;}
14            }
15            if (Object.hasOwn(navigator,name)) throw Error('Own attribute '+name);
16        }
17        const languages = navigator.languages;
18        if (languages !== navigator.languages || !Object.isFrozen(languages) || languages[0] !== navigator.language) throw Error('Languages');
19        if (typeof navigator.appVersion !== 'string' || navigator.appVersion.length === 0 ||
20            typeof navigator.userAgent !== 'string' || navigator.userAgent.length === 0 ||
21            navigator.hardwareConcurrency < 1 || !Number.isInteger(navigator.hardwareConcurrency) ||
22            navigator.onLine !== true) throw Error('Metadata');
23        const channel = new BroadcastChannel('navigator');
24        for (const clone of [() => structuredClone(navigator), () => postMessage(navigator), () => channel.postMessage(navigator)]) {
25            try {clone(); throw Error('Navigator cloned');} catch (error) {if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error;}
26        }
27        channel.close();
28        WeakMap.prototype.get = () => {throw Error('Guest getter');};
29        globalThis.WeakMap = () => {throw Error('Guest WeakMap');};
30        if (navigator.languages !== languages || navigator.userAgent.length === 0) throw Error('Changed metadata');
31        console.log('navigator');
32    "#,
33        &[],
34    );
35    assert_eq!(output.stdout, b"navigator\n");
36}
37
38#[test]
39fn user_agent_is_writable_with_string_conversion_and_brand_checks() {
40    let output = common::run(
41        r#"
42        const original = navigator.userAgent;
43        const identity = JSON.stringify(navigator.userAgentData);
44        navigator.userAgent = 'custom/1.0';
45        if (navigator.userAgent !== 'custom/1.0') throw Error('Assignment');
46        if (JSON.stringify(navigator.userAgentData) !== identity) throw Error('Identity changed');
47        for (const value of [42, null, undefined, '', {toString() {return 'object/1.0';}}]) {
48            navigator.userAgent = value;
49            if (navigator.userAgent !== String(value)) throw Error('String conversion');
50        }
51        const setter = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgent').set;
52        for (const receiver of [{}, WorkerNavigator.prototype, new EventTarget()]) {
53            try { setter.call(receiver, 'bad'); throw Error('Brand accepted'); }
54            catch (error) { if (!(error instanceof TypeError)) throw error; }
55        }
56        navigator.userAgent = original;
57        try { navigator.userAgent = Symbol(); throw Error('Symbol accepted'); }
58        catch (error) { if (!(error instanceof TypeError)) throw error; }
59        const failure = new Error('conversion failed');
60        try { navigator.userAgent = {toString() {throw failure;}}; throw Error('Conversion accepted'); }
61        catch (error) { if (error !== failure) throw error; }
62        if (navigator.userAgent !== original) throw Error('Failed assignment mutated UA');
63        WeakMap.prototype.get = () => {throw Error('Guest getter');};
64        globalThis.String = () => {throw Error('Guest conversion');};
65        navigator.userAgent = 123;
66        if (navigator.userAgent !== '123') throw Error('Primordials');
67        console.log('writable UA');
68    "#,
69        &[],
70    );
71    assert_eq!(output.stdout, b"writable UA\n");
72}
73
74#[test]
75fn navigator_user_agent_data_matches_spec_and_deno() {
76    let output = common::run(
77        r#"
78        const data = navigator.userAgentData;
79        if (!(data instanceof NavigatorUAData) || typeof NavigatorUAData !== 'function' || typeof Navigator !== 'undefined') throw Error('Interface');
80        if (Object.prototype.toString.call(data) !== '[object NavigatorUAData]') throw Error('Tag');
81        if ('vendor' in navigator || 'productSub' in navigator || 'vendorSub' in navigator) throw Error('Window-only');
82        try { new NavigatorUAData(); throw Error('Constructor accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
83        for (const name of ['brands','mobile','platform']) {
84            const descriptor = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, name);
85            if (!descriptor.get || descriptor.set || !descriptor.enumerable || !descriptor.configurable) throw Error('Descriptor '+name);
86            for (const receiver of [{}, NavigatorUAData.prototype, navigator, WorkerNavigator.prototype]) {
87                try { descriptor.get.call(receiver); throw Error('Brand accepted '+name); } catch (error) { if (!(error instanceof TypeError)) throw error; }
88            }
89            if (Object.hasOwn(data, name)) throw Error('Own attribute '+name);
90        }
91        const userAgentData = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgentData');
92        if (!userAgentData.get || userAgentData.set || !userAgentData.enumerable || !userAgentData.configurable) throw Error('Descriptor userAgentData');
93        for (const receiver of [{}, WorkerNavigator.prototype, new EventTarget(), data]) {
94            try { userAgentData.get.call(receiver); throw Error('Brand accepted userAgentData'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
95        }
96        if (Object.hasOwn(navigator, 'userAgentData')) throw Error('Own userAgentData');
97        const toJSON = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, 'toJSON');
98        if (typeof toJSON.value !== 'function' || !toJSON.enumerable || !toJSON.configurable) throw Error('Method toJSON');
99        for (const receiver of [{}, NavigatorUAData.prototype, navigator]) {
100            try { toJSON.value.call(receiver); throw Error('Brand accepted toJSON'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
101        }
102        const ghev = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, 'getHighEntropyValues');
103        if (typeof ghev.value !== 'function' || !ghev.enumerable || !ghev.configurable) throw Error('Method getHighEntropyValues');
104        for (const receiver of [{}, NavigatorUAData.prototype, navigator]) {
105            let value;
106            try { value = ghev.value.call(receiver, []); } catch (error) { throw Error('Sync throw brand'); }
107            if (!(value instanceof Promise)) throw Error('Brand not a promise');
108            try { await value; throw Error('Brand accepted getHighEntropyValues'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
109        }
110        if (data !== navigator.userAgentData) throw Error('Identity');
111        if (data.brands !== data.brands) throw Error('SameObject');
112        const brands = data.brands;
113        if (!Array.isArray(brands) || !Object.isFrozen(brands) || brands.length !== 1) throw Error('Brands array');
114        const entry = brands[0];
115        if (!Object.isFrozen(entry) || Object.keys(entry).join() !== 'brand,version' || entry.brand !== 'avril') throw Error('Brand entry');
116        if (data.mobile !== false || typeof data.platform !== 'string') throw Error('Low entropy');
117        if (data.platform === navigator.platform) throw Error('Platform vocabulary');
118        if (!['Linux','macOS','Windows'].includes(data.platform)) throw Error('Platform value');
119        if (NavigatorUAData.prototype.getHighEntropyValues.length !== 1) throw Error('Arity');
120        for (const hints of [undefined, 'architecture', 42, null, {}, {length: 1}, [Symbol()]]) {
121            let value;
122            try { value = hints === undefined ? data.getHighEntropyValues() : data.getHighEntropyValues(hints); } catch (error) { throw Error('Sync throw hints'); }
123            if (!(value instanceof Promise)) throw Error('Hints not a promise');
124            try { await value; throw Error('Hints accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; }
125        }
126        const empty = await data.getHighEntropyValues([]);
127        if (Object.getPrototypeOf(empty) !== Object.prototype || Object.isFrozen(empty) || Object.keys(empty).join() !== 'brands,mobile,platform') throw Error('Empty hints');
128        if (!(empty instanceof Object) || empty.brands === brands || Object.isFrozen(empty.brands)) throw Error('Empty brands copy');
129        if (JSON.stringify(empty.brands) !== JSON.stringify(brands) || empty.mobile !== false || empty.platform !== data.platform) throw Error('Empty values');
130        if (!(data.getHighEntropyValues(['bogus', '42', 42]) instanceof Promise)) throw Error('Promise');
131        const ignored = await data.getHighEntropyValues(['bogus', '42', 42]);
132        if (Object.keys(ignored).join() !== 'brands,mobile,platform') throw Error('Unknown hints');
133        const full = await data.getHighEntropyValues(['architecture','bitness','model','platformVersion','uaFullVersion','fullVersionList','wow64']);
134        if (Object.keys(full).sort().join() !== 'architecture,bitness,brands,fullVersionList,mobile,model,platform,platformVersion,uaFullVersion,wow64') throw Error('Full keys');
135        if (!['x86','arm'].includes(full.architecture) || !['32','64'].includes(full.bitness)) throw Error('High values');
136        if (full.model !== '' || full.platformVersion !== '' || full.wow64 !== false) throw Error('Empty high values');
137        if (typeof full.uaFullVersion !== 'string' || full.uaFullVersion.split('.').length !== 3) throw Error('Full version');
138        if (navigator.userAgent !== 'avril/'+full.uaFullVersion || brands[0].version !== full.uaFullVersion.split('.')[0]) throw Error('Version split');
139        if (full.fullVersionList.length !== 1 || full.fullVersionList[0].brand !== 'avril' || full.fullVersionList[0].version !== full.uaFullVersion) throw Error('Version list');
140        if (Object.getPrototypeOf(full) !== Object.prototype || Object.isFrozen(full)) throw Error('Full plain');
141        if (JSON.stringify(structuredClone(full)) !== JSON.stringify(full)) throw Error('Full cloneable');
142        const json = data.toJSON();
143        if (Object.getPrototypeOf(json) !== Object.prototype || Object.keys(json).join() !== 'brands,mobile,platform') throw Error('JSON keys');
144        if (json.brands === brands || !Array.isArray(json.brands) || Object.isFrozen(json.brands)) throw Error('JSON brands copy');
145        if (JSON.stringify(json.brands) !== JSON.stringify(brands) || json.mobile !== false || json.platform !== data.platform) throw Error('JSON values');
146        if (JSON.stringify(data) !== JSON.stringify(json)) throw Error('Stringify');
147        const brandsClone = structuredClone(brands);
148        if (brandsClone === brands || JSON.stringify(brandsClone) !== JSON.stringify(brands)) throw Error('Brands cloneable');
149        const entryClone = structuredClone(entry);
150        if (entryClone === entry || JSON.stringify(entryClone) !== JSON.stringify(entry)) throw Error('Entry cloneable');
151        const channel = new BroadcastChannel('userAgentData');
152        for (const clone of [() => structuredClone(data), () => postMessage(data), () => channel.postMessage(data)]) {
153            try { clone(); throw Error('UA data cloned'); } catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; }
154        }
155        channel.close();
156        WeakMap.prototype.get = () => { throw Error('Guest getter'); };
157        globalThis.WeakMap = () => { throw Error('Guest WeakMap'); };
158        Promise.resolve = () => { throw Error('Guest resolve'); };
159        globalThis.Promise = class extends Promise {};
160        if (data.brands !== brands || data.platform.length === 0 || (await data.getHighEntropyValues([])).platform !== data.platform) throw Error('Changed data');
161        console.log('userAgentData');
162    "#,
163        &[],
164    );
165    assert_eq!(output.stdout, b"userAgentData\n");
166}
167
168#[test]
169fn worker_navigators_have_the_same_runtime_identity() {
170    let dir = tempfile::tempdir().unwrap();
171    std::fs::write(dir.path().join("worker.js"),r#"
172        if (!(navigator instanceof WorkerNavigator) || typeof Navigator !== 'undefined') throw Error('Worker interface');
173        postMessage([navigator.userAgent,navigator.platform,navigator.languages,navigator.hardwareConcurrency]); close();
174    "#).unwrap();
175    std::fs::write(dir.path().join("main.js"),r#"
176        const worker = new Worker(new URL('./worker.js',import.meta.url),{type:'module'});
177        const attributes = await new Promise((resolve,reject) => {worker.onmessage = e => resolve(e.data); worker.onerror = reject;});
178        if (attributes[0] !== navigator.userAgent || attributes[1] !== navigator.platform ||
179            attributes[2].join() !== navigator.languages.join() || attributes[3] !== navigator.hardwareConcurrency) throw Error('Identity');
180        console.log('worker navigator');
181    "#).unwrap();
182    let output = common::wait(
183        std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
184            .arg("run")
185            .arg(dir.path().join("main.js"))
186            .stdout(std::process::Stdio::piped())
187            .stderr(std::process::Stdio::piped())
188            .spawn()
189            .unwrap(),
190    );
191    assert!(
192        output.status.success(),
193        "{}",
194        String::from_utf8_lossy(&output.stderr)
195    );
196    assert_eq!(output.stdout, b"worker navigator\n");
197}