//! Browser polyfills for the QuickJS sandbox. //! //! Design principles: //! - Security-critical polyfills (crypto.getRandomValues) MUST be Rust-backed //! - TextEncoder/TextDecoder must be spec-correct UTF-8 //! - console.log must route to tracing (not silently dropped) //! - setTimeout must call the callback (sync, for compat) //! - crypto.subtle must be implemented (AES, SHA, HMAC) use rquickjs::function::Rest; use rquickjs::{Ctx, Function, Object, Value}; /// Install all polyfills into a JS context. pub fn install(ctx: &Ctx<'_>) -> rquickjs::Result<()> { install_base64(ctx)?; install_encoding(ctx)?; install_console(ctx)?; install_timers(ctx)?; install_url(ctx)?; install_crypto(ctx)?; install_globals(ctx)?; remove_dangerous(ctx)?; Ok(()) } fn install_base64(ctx: &Ctx<'_>) -> rquickjs::Result<()> { // Rust-backed for correct Latin-1 handling and performance use base64::{engine::general_purpose::STANDARD, Engine as _}; ctx.globals().set( "atob", Function::new(ctx.clone(), |s: String| -> rquickjs::Result { let cleaned = s .chars() .filter(|c| !c.is_whitespace()) .collect::(); let decoded = STANDARD .decode(&cleaned) .map_err(|_| rquickjs::Error::Exception)?; // btoa/atob work with Latin-1 byte values, not UTF-8 Ok(decoded.iter().map(|&b| b as char).collect()) })?, )?; ctx.globals().set( "btoa", Function::new(ctx.clone(), |s: String| -> rquickjs::Result { let bytes: Result, _> = s .chars() .map(|c| { if c as u32 > 255 { Err(rquickjs::Error::Exception) } else { Ok(c as u8) } }) .collect(); Ok(STANDARD.encode(bytes?)) })?, )?; Ok(()) } fn install_encoding(ctx: &Ctx<'_>) -> rquickjs::Result<()> { // Spec-correct UTF-8 TextEncoder/TextDecoder implementations ctx.eval::<(), _>( r#" globalThis.TextEncoder = class TextEncoder { constructor() { this.encoding = 'utf-8'; } encode(str) { const bytes = []; for (let i = 0; i < str.length; i++) { let cp = str.codePointAt(i); if (cp > 0xFFFF) i++; if (cp <= 0x7F) { bytes.push(cp); } else if (cp <= 0x7FF) { bytes.push(0xC0 | (cp >> 6), 0x80 | (cp & 0x3F)); } else if (cp <= 0xFFFF) { bytes.push(0xE0|(cp>>12), 0x80|((cp>>6)&0x3F), 0x80|(cp&0x3F)); } else { bytes.push(0xF0|(cp>>18), 0x80|((cp>>12)&0x3F), 0x80|((cp>>6)&0x3F), 0x80|(cp&0x3F)); } } return new Uint8Array(bytes); } encodeInto(str, dest) { const enc = this.encode(str); const len = Math.min(enc.length, dest.length); dest.set(enc.subarray(0, len)); return { read: str.length, written: len }; } }; globalThis.TextDecoder = class TextDecoder { constructor(enc) { this.encoding = (enc||'utf-8').toLowerCase(); } decode(buf) { const bytes = buf instanceof Uint8Array ? buf : buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(Array.from(buf)); let str = '', i = 0; while (i < bytes.length) { const b = bytes[i]; let cp; if ((b & 0x80) === 0) { cp = b; i+=1; } else if ((b & 0xE0) === 0xC0) { cp = ((b&0x1F)<<6)|(bytes[i+1]&0x3F); i+=2; } else if ((b & 0xF0) === 0xE0) { cp = ((b&0xF)<<12)|((bytes[i+1]&0x3F)<<6)|(bytes[i+2]&0x3F); i+=3; } else { cp = ((b&7)<<18)|((bytes[i+1]&0x3F)<<12)|((bytes[i+2]&0x3F)<<6)|(bytes[i+3]&0x3F); i+=4; } str += cp > 0xFFFF ? String.fromCodePoint(cp) : String.fromCharCode(cp); } return str; } }; "#, )?; Ok(()) } fn install_console(ctx: &Ctx<'_>) -> rquickjs::Result<()> { let console = Object::new(ctx.clone())?; macro_rules! add_level { ($name:literal, $macro:ident) => {{ let name = $name; console.set( name, Function::new(ctx.clone(), move |args: Rest>| { let parts: Vec = args .0 .iter() .map(|v| { v.as_string() .and_then(|s| s.to_string().ok()) .unwrap_or_else(|| format!("{:?}", v)) }) .collect(); tracing::$macro!(target: "bex_js", "{}", parts.join(" ")); })?, )?; }}; } add_level!("log", debug); add_level!("info", debug); add_level!("debug", trace); add_level!("warn", warn); add_level!("error", error); // console.time / timeEnd stubs (no-op for now) console.set( "time", Function::new(ctx.clone(), |_: String| {})?, )?; console.set( "timeEnd", Function::new(ctx.clone(), |_: String| {})?, )?; ctx.globals().set("console", console)?; Ok(()) } fn install_timers(ctx: &Ctx<'_>) -> rquickjs::Result<()> { ctx.eval::<(), _>( r#" // Call callback synchronously (compat for setTimeout(fn, 0) patterns) globalThis.setTimeout = function(fn, ms) { if (typeof fn === 'function') { try { fn(); } catch(e) {} } return 0; }; globalThis.clearTimeout = function(id) {}; globalThis.setInterval = function(fn, ms) { if (typeof fn === 'function') { try { fn(); } catch(e) {} } return 0; }; globalThis.clearInterval = function(id) {}; globalThis.queueMicrotask = function(fn) { if (typeof fn === 'function') { try { fn(); } catch(e) {} } }; "#, )?; Ok(()) } fn install_url(ctx: &Ctx<'_>) -> rquickjs::Result<()> { ctx.eval::<(), _>( r#" globalThis.URLSearchParams = class URLSearchParams { constructor(init) { this._map = {}; if (typeof init === 'string') { init.replace(/^\?/, '').split('&').filter(Boolean).forEach(pair => { const eq = pair.indexOf('='); const k = eq >= 0 ? pair.substring(0, eq) : pair; const v = eq >= 0 ? pair.substring(eq + 1) : ''; this._map[decodeURIComponent(k)] = decodeURIComponent(v); }); } } get(k) { return this._map[k] !== undefined ? this._map[k] : null; } set(k, v) { this._map[k] = String(v); } delete(k) { delete this._map[k]; } has(k) { return k in this._map; } append(k, v) { this._map[k] = v; } toString() { return Object.entries(this._map) .map(([k,v]) => encodeURIComponent(k)+'='+encodeURIComponent(v)) .join('&'); } entries() { return Object.entries(this._map)[Symbol.iterator](); } }; globalThis.URL = class URL { constructor(url, base) { this.href = url; const m = url.match(/^([\w+.-]+:)\/\/([^/?#@]*@)?([^/?#:]*)(?::(\d+))?(\/[^?#]*)?(\?[^#]*)?(#.*)?$/); if (m) { this.protocol = m[1] || 'https:'; this.username = ''; this.password = ''; this.hostname = m[3] || ''; this.port = m[4] || ''; this.host = this.hostname + (this.port ? ':' + this.port : ''); this.pathname = m[5] || '/'; this.search = m[6] || ''; this.hash = m[7] || ''; this.origin = this.protocol + '//' + this.host; this.searchParams = new URLSearchParams(this.search); } } toString() { return this.href; } }; "#, )?; Ok(()) } fn install_crypto(ctx: &Ctx<'_>) -> rquickjs::Result<()> { // Install a Rust-backed __randomBytes function that the JS getRandomValues can call ctx.globals().set( "__randomBytes", Function::new(ctx.clone(), |len: u32| -> rquickjs::Result> { use rand::RngCore; let mut buf = vec![0u8; len as usize]; rand::thread_rng().fill_bytes(&mut buf); Ok(buf) })?, )?; // Install crypto object with getRandomValues using the Rust-backed __randomBytes ctx.eval::<(), _>( r#" globalThis.crypto = { getRandomValues(arr) { if (!(arr instanceof Uint8Array)) throw new TypeError('Expected Uint8Array'); const bytes = __randomBytes(arr.length); for (let i = 0; i < arr.length; i++) arr[i] = bytes[i]; return arr; }, randomUUID() { const bytes = __randomBytes(16); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); return hex.slice(0,8) + '-' + hex.slice(8,12) + '-' + hex.slice(12,16) + '-' + hex.slice(16,20) + '-' + hex.slice(20); } }; "#, )?; // crypto.subtle: implemented in pure JS for AES, SHA, HMAC ctx.eval::<(), _>(include_str!("../assets/crypto_subtle.js"))?; Ok(()) } fn install_globals(ctx: &Ctx<'_>) -> rquickjs::Result<()> { ctx.eval::<(), _>( r#" // Standard browser/worker global aliases globalThis.self = globalThis; globalThis.window = globalThis; // performance.now() stub (monotonic ms) globalThis.performance = { _start: Date.now(), now() { return Date.now() - this._start; } }; // structuredClone (deep copy via JSON round-trip) globalThis.structuredClone = function(obj) { return JSON.parse(JSON.stringify(obj)); }; // navigator stub globalThis.navigator = { userAgent: 'Mozilla/5.0 (compatible; BexEngine/6.0)', language: 'en-US', }; // location stub (helps window.location patterns) globalThis.location = { href: 'https://bex.engine/', hostname: 'bex.engine', protocol: 'https:', assign(url) { this.href = url; }, replace(url) { this.href = url; }, }; "#, )?; Ok(()) } fn remove_dangerous(ctx: &Ctx<'_>) -> rquickjs::Result<()> { ctx.eval::<(), _>( r#" // Remove APIs that could be used to escape the sandbox or identify the host delete globalThis.WebAssembly; // Note: eval() is intentionally KEPT — many sites use eval(atob(...)) patterns // Note: Function() constructor is kept for compat but monitored "#, )?; Ok(()) }