mod common; #[test] fn web_globals_expose_self_navigator_and_performance() { let output = common::run( r#" const global = globalThis; if (self !== global || self !== globalThis) throw Error('self alias'); const selfDescriptor = Object.getOwnPropertyDescriptor(global, 'self'); if (selfDescriptor.get === undefined || selfDescriptor.enumerable !== true || !selfDescriptor.configurable) throw Error('self descriptor'); if (navigator !== global.navigator || !(navigator instanceof WorkerNavigator) || typeof Navigator !== 'undefined') throw Error('Navigator interface'); if (Object.prototype.toString.call(navigator) !== '[object WorkerNavigator]') throw Error('Navigator tag'); if (!navigator.userAgent.startsWith('avril/') || navigator.appCodeName !== 'Mozilla' || navigator.appName !== 'Netscape' || navigator.appVersion !== '4.0' || navigator.product !== 'Gecko') throw Error('Navigator identification'); 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'); const navigatorDescriptor = Object.getOwnPropertyDescriptor(global, 'navigator'); if (!navigatorDescriptor.get || navigatorDescriptor.enumerable !== true || !navigatorDescriptor.configurable) throw Error('navigator descriptor'); try { new WorkerNavigator(); throw Error('Navigator constructor was callable'); } catch (error) { if (!(error instanceof TypeError)) throw error; } const userAgent = Object.getOwnPropertyDescriptor(WorkerNavigator.prototype, 'userAgent').get; try { userAgent.call({}); throw Error('Navigator brand check'); } catch (error) { if (!(error instanceof TypeError)) throw error; } if (performance !== global.performance || !(performance instanceof Performance) || !(performance instanceof EventTarget)) throw Error('Performance identity'); if (Object.prototype.toString.call(performance) !== '[object Performance]') throw Error('Performance tag'); if (typeof performance.timeOrigin !== 'number' || performance.timeOrigin <= 0 || performance.now() < 0) throw Error('Performance clock'); const opsSymbol = Symbol.for('__avril.ops'); if (opsSymbol in global || global[opsSymbol] !== undefined || Reflect.ownKeys(global).includes(opsSymbol)) throw Error('Native op registry exposed'); const before = performance.now(), after = performance.now(); if (after < before) throw Error('Performance clock moved backwards'); const sample = performance.now(); if (Math.abs(sample * 10 - Math.round(sample * 10)) > 1e-7) throw Error('Performance precision'); if (Math.abs(Date.now() - (performance.timeOrigin + performance.now())) > 5000) throw Error('Time origin'); const start = performance.now(); await new Promise(resolve => setTimeout(resolve, 10)); if (performance.now() < start + 5) throw Error('Timer and performance clocks diverged'); const json = performance.toJSON(); if (Object.keys(json).join() !== 'timeOrigin' || json.timeOrigin !== performance.timeOrigin) throw Error('Performance JSON'); for (const call of [() => new Performance(), () => performance.now.call({}), () => Object.getOwnPropertyDescriptor(Performance.prototype, 'timeOrigin').get.call({}), () => Performance.prototype.toJSON.call({})]) { try { call(); throw Error('Performance brand or constructor'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } const performanceDescriptor = Object.getOwnPropertyDescriptor(global, 'performance'); if (!performanceDescriptor.get || performanceDescriptor.enumerable !== true || !performanceDescriptor.configurable) throw Error('performance descriptor'); if ('op_navigator_metadata' in global || 'op_timer_time_origin' in global) throw Error('Native helper leaked'); console.log('globals'); "#, &[], ); assert_eq!(output.stdout, b"globals\n"); } #[test] fn base64_globals_follow_forgiving_decode_and_latin1_encode() { let output = common::run( r#" if (btoa('') !== '' || btoa('f') !== 'Zg==' || btoa('fo') !== 'Zm8=' || btoa('foo') !== 'Zm9v' || btoa('\x00\x80\xff') !== 'AID/') throw Error('btoa vectors'); 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'); if (atob(btoa('binary\x00\x80\xff')) !== 'binary\x00\x80\xff' || atob('\f') !== '') throw Error('Base64 round trip'); const long = 'x'.repeat(10000); if (atob(btoa(long)) !== long) throw Error('Large Base64 round trip'); function fails(fn, name, code) { try { fn(); throw Error('Expected ' + name); } catch (error) { if (error.message === 'Expected ' + name) throw error; if (!(error instanceof DOMException) || error.name !== name || error.code !== code) throw error; } } fails(() => btoa('éĀ'), 'InvalidCharacterError', 5); fails(() => btoa('\ud800'), 'InvalidCharacterError', 5); for (const value of ['A', 'AA===', 'AQ\v', 'AQ\u00a0', 'é', '====']) fails(() => atob(value), 'InvalidCharacterError', 5); for (const fn of [() => atob(), () => btoa()]) { try { fn(); throw Error('Missing argument accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } try { btoa(Symbol()); throw Error('Symbol accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } 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'); for(const name of ['DOMStringSizeError','NoDataAllowedError','ValidationError']){ if(new DOMException('',name).code!==0)throw Error('Obsolete exception name got a code'); } const codeGetter=Object.getOwnPropertyDescriptor(DOMException.prototype,'code').get; try{codeGetter.call({name:'AbortError'});throw Error('Unbranded exception accepted');}catch(e){if(!(e instanceof TypeError))throw e;} const exception=new DOMException('','AbortError'); Object.defineProperty(exception,'name',{value:'TimeoutError'}); if(exception.code!==20)throw Error('Public name changed native exception code'); if (atob.length !== 1 || btoa.length !== 1 || Object.getOwnPropertyDescriptor(globalThis, 'atob').enumerable !== true || Object.getOwnPropertyDescriptor(globalThis, 'btoa').enumerable !== true) throw Error('Base64 descriptors'); console.log('base64'); "#, &[], ); assert_eq!(output.stdout, b"base64\n"); } #[test] fn web_globals_capture_intrinsics_and_native_clocks() { let output = common::run( r#" const now = performance.now.bind(performance), origin = performance.timeOrigin; String.prototype.charCodeAt = () => {throw Error('charCodeAt');}; String.prototype.slice = () => {throw Error('slice');}; String.fromCharCode = () => {throw Error('fromCharCode');}; Array.prototype.push = () => {throw Error('push');}; Array.prototype.join = () => {throw Error('join');}; Date.now = () => {throw Error('Date.now');}; if (btoa('foo') !== 'Zm9v' || atob('Zm9v') !== 'foo' || now() < 0 || performance.timeOrigin !== origin) throw Error('Captured operations'); console.log('captured'); "#, &[], ); assert_eq!(output.stdout, b"captured\n"); } #[test] fn base64_operations_are_not_constructors_and_replaceable_globals_can_be_reassigned() { let output = common::run( r#" for(const fn of [atob,btoa]){ try{new fn('');throw Error('Operation used as constructor');}catch(e){if(!(e instanceof TypeError))throw e;} } const original=self; self={replacement:true}; performance={replacement:true}; if(!self.replacement||!performance.replacement||globalThis!==original)throw Error('Globals not replaceable'); for(const name of ['self','performance']){ const descriptor=Object.getOwnPropertyDescriptor(globalThis,name); if(!descriptor.writable||!descriptor.enumerable||!descriptor.configurable||descriptor.get)throw Error('Replacement descriptor'); } console.log('replaceable'); "#, &[], ); assert_eq!(output.stdout, b"replaceable\n"); } #[test] fn global_avril_symbol_marks_the_runtime_in_main_and_workers() { let output = common::run( r#" import { avril } from 'avril:std'; const key = Symbol.for('runtime:avril'); if (avril !== key) throw Error('avril symbol'); if (globalThis[key] !== true || globalThis[avril] !== true) throw Error('Marker'); const descriptor = Object.getOwnPropertyDescriptor(globalThis, key); if (!descriptor || descriptor.value !== true || descriptor.writable !== false || descriptor.enumerable !== false || descriptor.configurable !== false) throw Error('Descriptor'); if (Reflect.ownKeys(globalThis).includes(key) !== true) throw Error('Own key'); try { globalThis[key] = false; } catch {} if (globalThis[key] !== true) throw Error('Writable'); try { Object.defineProperty(globalThis, key, { value: false }); throw Error('Redefined'); } catch (error) { if (!(error instanceof TypeError)) throw error; } console.log('marker'); "#, &[], ); assert_eq!(output.stdout, b"marker\n"); let dir = tempfile::tempdir().unwrap(); std::fs::write( dir.path().join("worker.js"), r#" postMessage(globalThis[Symbol.for('runtime:avril')]); 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 marker = await new Promise((resolve, reject) => { worker.onmessage = event => resolve(event.data); worker.onerror = reject; }); if (marker !== true) throw Error('Worker marker'); console.log('worker marker'); "#).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 marker\n"); }