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