mod common; #[test] fn navigator_exposes_all_html_worker_mixins_with_web_idl_brands() { let output = common::run( r#" const attributes = ['appCodeName','appName','appVersion','platform','product','userAgent', 'language','languages','onLine','hardwareConcurrency']; for (const name of attributes) { const descriptor = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype,name); if (!descriptor.get || !!descriptor.set !== (name === 'userAgent') || !descriptor.enumerable || !descriptor.configurable) throw Error('Descriptor '+name); for (const receiver of [{},WorkerNavigator.prototype,new EventTarget()]) { try {descriptor.get.call(receiver); throw Error('Brand accepted '+name);} catch (error) {if (!(error instanceof TypeError)) throw error;} } if (Object.hasOwn(navigator,name)) throw Error('Own attribute '+name); } const languages = navigator.languages; if (languages !== navigator.languages || !Object.isFrozen(languages) || languages[0] !== navigator.language) throw Error('Languages'); if (typeof navigator.appVersion !== 'string' || navigator.appVersion.length === 0 || typeof navigator.userAgent !== 'string' || navigator.userAgent.length === 0 || navigator.hardwareConcurrency < 1 || !Number.isInteger(navigator.hardwareConcurrency) || navigator.onLine !== true) throw Error('Metadata'); const channel = new BroadcastChannel('navigator'); for (const clone of [() => structuredClone(navigator), () => postMessage(navigator), () => channel.postMessage(navigator)]) { try {clone(); throw Error('Navigator cloned');} catch (error) {if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error;} } channel.close(); WeakMap.prototype.get = () => {throw Error('Guest getter');}; globalThis.WeakMap = () => {throw Error('Guest WeakMap');}; if (navigator.languages !== languages || navigator.userAgent.length === 0) throw Error('Changed metadata'); console.log('navigator'); "#, &[], ); assert_eq!(output.stdout, b"navigator\n"); } #[test] fn user_agent_is_writable_with_string_conversion_and_brand_checks() { let output = common::run( r#" const original = navigator.userAgent; const identity = JSON.stringify(navigator.userAgentData); navigator.userAgent = 'custom/1.0'; if (navigator.userAgent !== 'custom/1.0') throw Error('Assignment'); if (JSON.stringify(navigator.userAgentData) !== identity) throw Error('Identity changed'); for (const value of [42, null, undefined, '', {toString() {return 'object/1.0';}}]) { navigator.userAgent = value; if (navigator.userAgent !== String(value)) throw Error('String conversion'); } const setter = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgent').set; for (const receiver of [{}, WorkerNavigator.prototype, new EventTarget()]) { try { setter.call(receiver, 'bad'); throw Error('Brand accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } navigator.userAgent = original; try { navigator.userAgent = Symbol(); throw Error('Symbol accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } const failure = new Error('conversion failed'); try { navigator.userAgent = {toString() {throw failure;}}; throw Error('Conversion accepted'); } catch (error) { if (error !== failure) throw error; } if (navigator.userAgent !== original) throw Error('Failed assignment mutated UA'); WeakMap.prototype.get = () => {throw Error('Guest getter');}; globalThis.String = () => {throw Error('Guest conversion');}; navigator.userAgent = 123; if (navigator.userAgent !== '123') throw Error('Primordials'); console.log('writable UA'); "#, &[], ); assert_eq!(output.stdout, b"writable UA\n"); } #[test] fn navigator_user_agent_data_matches_spec_and_deno() { let output = common::run( r#" const data = navigator.userAgentData; if (!(data instanceof NavigatorUAData) || typeof NavigatorUAData !== 'function' || typeof Navigator !== 'undefined') throw Error('Interface'); if (Object.prototype.toString.call(data) !== '[object NavigatorUAData]') throw Error('Tag'); if ('vendor' in navigator || 'productSub' in navigator || 'vendorSub' in navigator) throw Error('Window-only'); try { new NavigatorUAData(); throw Error('Constructor accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } for (const name of ['brands','mobile','platform']) { const descriptor = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, name); if (!descriptor.get || descriptor.set || !descriptor.enumerable || !descriptor.configurable) throw Error('Descriptor '+name); for (const receiver of [{}, NavigatorUAData.prototype, navigator, WorkerNavigator.prototype]) { try { descriptor.get.call(receiver); throw Error('Brand accepted '+name); } catch (error) { if (!(error instanceof TypeError)) throw error; } } if (Object.hasOwn(data, name)) throw Error('Own attribute '+name); } const userAgentData = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgentData'); if (!userAgentData.get || userAgentData.set || !userAgentData.enumerable || !userAgentData.configurable) throw Error('Descriptor userAgentData'); for (const receiver of [{}, WorkerNavigator.prototype, new EventTarget(), data]) { try { userAgentData.get.call(receiver); throw Error('Brand accepted userAgentData'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } if (Object.hasOwn(navigator, 'userAgentData')) throw Error('Own userAgentData'); const toJSON = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, 'toJSON'); if (typeof toJSON.value !== 'function' || !toJSON.enumerable || !toJSON.configurable) throw Error('Method toJSON'); for (const receiver of [{}, NavigatorUAData.prototype, navigator]) { try { toJSON.value.call(receiver); throw Error('Brand accepted toJSON'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } const ghev = Object.getOwnPropertyDescriptor(NavigatorUAData.prototype, 'getHighEntropyValues'); if (typeof ghev.value !== 'function' || !ghev.enumerable || !ghev.configurable) throw Error('Method getHighEntropyValues'); for (const receiver of [{}, NavigatorUAData.prototype, navigator]) { let value; try { value = ghev.value.call(receiver, []); } catch (error) { throw Error('Sync throw brand'); } if (!(value instanceof Promise)) throw Error('Brand not a promise'); try { await value; throw Error('Brand accepted getHighEntropyValues'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } if (data !== navigator.userAgentData) throw Error('Identity'); if (data.brands !== data.brands) throw Error('SameObject'); const brands = data.brands; if (!Array.isArray(brands) || !Object.isFrozen(brands) || brands.length !== 1) throw Error('Brands array'); const entry = brands[0]; if (!Object.isFrozen(entry) || Object.keys(entry).join() !== 'brand,version' || entry.brand !== 'avril') throw Error('Brand entry'); if (data.mobile !== false || typeof data.platform !== 'string') throw Error('Low entropy'); if (data.platform === navigator.platform) throw Error('Platform vocabulary'); if (!['Linux','macOS','Windows'].includes(data.platform)) throw Error('Platform value'); if (NavigatorUAData.prototype.getHighEntropyValues.length !== 1) throw Error('Arity'); for (const hints of [undefined, 'architecture', 42, null, {}, {length: 1}, [Symbol()]]) { let value; try { value = hints === undefined ? data.getHighEntropyValues() : data.getHighEntropyValues(hints); } catch (error) { throw Error('Sync throw hints'); } if (!(value instanceof Promise)) throw Error('Hints not a promise'); try { await value; throw Error('Hints accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } const empty = await data.getHighEntropyValues([]); if (Object.getPrototypeOf(empty) !== Object.prototype || Object.isFrozen(empty) || Object.keys(empty).join() !== 'brands,mobile,platform') throw Error('Empty hints'); if (!(empty instanceof Object) || empty.brands === brands || Object.isFrozen(empty.brands)) throw Error('Empty brands copy'); if (JSON.stringify(empty.brands) !== JSON.stringify(brands) || empty.mobile !== false || empty.platform !== data.platform) throw Error('Empty values'); if (!(data.getHighEntropyValues(['bogus', '42', 42]) instanceof Promise)) throw Error('Promise'); const ignored = await data.getHighEntropyValues(['bogus', '42', 42]); if (Object.keys(ignored).join() !== 'brands,mobile,platform') throw Error('Unknown hints'); const full = await data.getHighEntropyValues(['architecture','bitness','model','platformVersion','uaFullVersion','fullVersionList','wow64']); if (Object.keys(full).sort().join() !== 'architecture,bitness,brands,fullVersionList,mobile,model,platform,platformVersion,uaFullVersion,wow64') throw Error('Full keys'); if (!['x86','arm'].includes(full.architecture) || !['32','64'].includes(full.bitness)) throw Error('High values'); if (full.model !== '' || full.platformVersion !== '' || full.wow64 !== false) throw Error('Empty high values'); if (typeof full.uaFullVersion !== 'string' || full.uaFullVersion.split('.').length !== 3) throw Error('Full version'); if (navigator.userAgent !== 'avril/'+full.uaFullVersion || brands[0].version !== full.uaFullVersion.split('.')[0]) throw Error('Version split'); if (full.fullVersionList.length !== 1 || full.fullVersionList[0].brand !== 'avril' || full.fullVersionList[0].version !== full.uaFullVersion) throw Error('Version list'); if (Object.getPrototypeOf(full) !== Object.prototype || Object.isFrozen(full)) throw Error('Full plain'); if (JSON.stringify(structuredClone(full)) !== JSON.stringify(full)) throw Error('Full cloneable'); const json = data.toJSON(); if (Object.getPrototypeOf(json) !== Object.prototype || Object.keys(json).join() !== 'brands,mobile,platform') throw Error('JSON keys'); if (json.brands === brands || !Array.isArray(json.brands) || Object.isFrozen(json.brands)) throw Error('JSON brands copy'); if (JSON.stringify(json.brands) !== JSON.stringify(brands) || json.mobile !== false || json.platform !== data.platform) throw Error('JSON values'); if (JSON.stringify(data) !== JSON.stringify(json)) throw Error('Stringify'); const brandsClone = structuredClone(brands); if (brandsClone === brands || JSON.stringify(brandsClone) !== JSON.stringify(brands)) throw Error('Brands cloneable'); const entryClone = structuredClone(entry); if (entryClone === entry || JSON.stringify(entryClone) !== JSON.stringify(entry)) throw Error('Entry cloneable'); const channel = new BroadcastChannel('userAgentData'); for (const clone of [() => structuredClone(data), () => postMessage(data), () => channel.postMessage(data)]) { try { clone(); throw Error('UA data cloned'); } catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; } } channel.close(); WeakMap.prototype.get = () => { throw Error('Guest getter'); }; globalThis.WeakMap = () => { throw Error('Guest WeakMap'); }; Promise.resolve = () => { throw Error('Guest resolve'); }; globalThis.Promise = class extends Promise {}; if (data.brands !== brands || data.platform.length === 0 || (await data.getHighEntropyValues([])).platform !== data.platform) throw Error('Changed data'); console.log('userAgentData'); "#, &[], ); assert_eq!(output.stdout, b"userAgentData\n"); } #[test] fn worker_navigators_have_the_same_runtime_identity() { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("worker.js"),r#" if (!(navigator instanceof WorkerNavigator) || typeof Navigator !== 'undefined') throw Error('Worker interface'); postMessage([navigator.userAgent,navigator.platform,navigator.languages,navigator.hardwareConcurrency]); close(); "#).unwrap(); std::fs::write(dir.path().join("main.js"),r#" const worker = new Worker(new URL('./worker.js',import.meta.url),{type:'module'}); const attributes = await new Promise((resolve,reject) => {worker.onmessage = e => resolve(e.data); worker.onerror = reject;}); if (attributes[0] !== navigator.userAgent || attributes[1] !== navigator.platform || attributes[2].join() !== navigator.languages.join() || attributes[3] !== navigator.hardwareConcurrency) throw Error('Identity'); console.log('worker navigator'); "#).unwrap(); let output = common::wait( std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .arg(dir.path().join("main.js")) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .unwrap(), ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"worker navigator\n"); }