diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 6018196..5be6093 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -200,6 +200,7 @@ pub struct BuiltinWorld { pub sym_search: P, pub sym_split: P, pub sym_iterator: P, + pub stub_objects: Vec>, } fn intern_label(string_forge: &StringForge, label: &str) -> u32 { @@ -290,6 +291,7 @@ impl BuiltinWorld { let sym_search = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_split = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_iterator = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let stub_objects = Vec::new(); /// Overwrite placeholder slots (set up by make_pair) with real values. /// ctor.vec[0] = constructor.prototype -> proto @@ -380,6 +382,7 @@ impl BuiltinWorld { sym_search, sym_split, sym_iterator, + stub_objects, } } diff --git a/crates/oxide_vm/src/bindings/bind_global.rs b/crates/oxide_vm/src/bindings/bind_global.rs new file mode 100644 index 0000000..ac5f339 --- /dev/null +++ b/crates/oxide_vm/src/bindings/bind_global.rs @@ -0,0 +1,18 @@ +use std::sync::Arc; + +use oxide_kernel::kernel::{KernelCore, KernelSession}; +use oxide_types::object::JsObject; + +use crate::bindings::apply_binding_table; + +pub fn bind_global(core: &Arc, session: &KernelSession, global: &mut JsObject) { + apply_binding_table( + session.builtin_world(), + global, + core, + &[ + ("escape", crate::builtins::global::js_escape as *const (), 1), + ("unescape", crate::builtins::global::js_unescape as *const (), 1), + ], + ); +} diff --git a/crates/oxide_vm/src/bindings/bind_stubs.rs b/crates/oxide_vm/src/bindings/bind_stubs.rs new file mode 100644 index 0000000..0bb7d8b --- /dev/null +++ b/crates/oxide_vm/src/bindings/bind_stubs.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +use oxide_kernel::kernel::{KernelCore, KernelSession}; +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::mem::P; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::bindings::{bind_global_value, configure_native_constructor}; + +const STUBS: [(&str, *const (), u8); 8] = [ + ("Proxy", crate::builtins::stubs::proxy_stub as *const (), 2), + ("BigInt", crate::builtins::stubs::bigint_stub as *const (), 1), + ("WeakMap", crate::builtins::stubs::weakmap_stub as *const (), 0), + ("WeakSet", crate::builtins::stubs::weakset_stub as *const (), 0), + ("WeakRef", crate::builtins::stubs::weakref_stub as *const (), 1), + ("FinalizationRegistry", crate::builtins::stubs::finalization_registry_stub as *const (), 1), + ("SharedArrayBuffer", crate::builtins::stubs::shared_array_buffer_stub as *const (), 1), + ("Atomics", crate::builtins::stubs::atomics_stub as *const (), 0), +]; + +pub fn bind_stubs(core: &Arc, session: &mut KernelSession, global: &mut JsObject) { + let builtin_world = Arc::get_mut(&mut session.builtin_world) + .expect("BuiltinWorld must be uniquely owned during init_kernel_builtins"); + + for (name, native_fn, arg_count) in STUBS { + let mut stub = JsObject::new_empty( + EMPTY_SHAPE_ID, + JsValue::from_js_object(builtin_world.function_proto.as_ptr() as *mut JsObject), + ); + stub.set_function(true); + configure_native_constructor(&mut stub, native_fn, arg_count); + + let stub = P::new(stub); + let stub_ptr = stub.as_ptr() as *mut JsObject; + bind_global_value(core, global, name, JsValue::from_js_object(stub_ptr)); + builtin_world.stub_objects.push(stub); + } +} diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index 337b2ea..7f8e1e6 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -3,6 +3,7 @@ pub mod bind_boolean; pub mod bind_date; pub mod bind_error; pub mod bind_function; +pub mod bind_global; pub mod bind_json; pub mod bind_map; pub mod bind_math; @@ -11,6 +12,7 @@ pub mod bind_object; pub mod bind_regexp; pub mod bind_set; pub mod bind_string; +pub mod bind_stubs; pub mod bind_symbol; use std::sync::Arc; @@ -77,7 +79,7 @@ pub(crate) fn bind_global_value(core: &Arc, global: &mut JsObject, n global.bump_generation(); } -pub fn init_kernel_builtins(core: &Arc, session: &KernelSession) { +pub fn init_kernel_builtins(core: &Arc, session: &mut KernelSession) { let global_ptr = session.global_object().as_ptr() as *mut oxide_types::object::JsObject; let global = unsafe { &mut *global_ptr }; @@ -95,6 +97,8 @@ pub fn init_kernel_builtins(core: &Arc, session: &KernelSession) { bind_function::bind_function(core, session, global); bind_regexp::bind_regexp(core, session, global); bind_symbol::bind_symbol(core, session, global); + bind_global::bind_global(core, session, global); + bind_stubs::bind_stubs(core, session, global); bind_global_value(core, global, "globalThis", JsValue::from_js_object(global_ptr)); } diff --git a/crates/oxide_vm/src/builtins/global.rs b/crates/oxide_vm/src/builtins/global.rs index d6c2dfe..2869bf0 100644 --- a/crates/oxide_vm/src/builtins/global.rs +++ b/crates/oxide_vm/src/builtins/global.rs @@ -1,7 +1,131 @@ -use std::sync::Arc; +use crate::coercion; +use crate::native::NativeResult; +use crate::vm::Vm; -use oxide_kernel::kernel::KernelCore; +const ESCAPE_SAFE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./"; -pub fn register_global_properties(_kernel: &Arc, _bump: &bumpalo::Bump) -> Result<(), String> { - Ok(()) +fn escape_string(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + for ch in input.chars() { + if ESCAPE_SAFE.contains(ch) { + out.push(ch); + continue; + } + + let code = ch as u32; + if code <= 0xFF { + out.push('%'); + out.push_str(&format!("{code:02X}")); + continue; + } + + let mut buf = [0u16; 2]; + for unit in ch.encode_utf16(&mut buf).iter() { + out.push_str(&format!("%u{:04X}", *unit)); + } + } + out +} + +fn parse_hex_u8(slice: &[u8]) -> Option { + std::str::from_utf8(slice).ok().and_then(|s| u8::from_str_radix(s, 16).ok()) +} + +fn parse_hex_u16(slice: &[u8]) -> Option { + std::str::from_utf8(slice).ok().and_then(|s| u16::from_str_radix(s, 16).ok()) +} + +fn unescape_string(input: &str) -> String { + let bytes = input.as_bytes(); + let mut out = String::with_capacity(input.len()); + let mut i = 0; + + while i < bytes.len() { + if bytes[i] == b'%' { + if i + 6 <= bytes.len() && bytes[i + 1] == b'u' { + if let Some(unit) = parse_hex_u16(&bytes[i + 2..i + 6]) { + if (0xD800..=0xDBFF).contains(&unit) + && i + 12 <= bytes.len() + && bytes[i + 6] == b'%' + && bytes[i + 7] == b'u' + { + if let Some(low) = parse_hex_u16(&bytes[i + 8..i + 12]) { + if (0xDC00..=0xDFFF).contains(&low) { + let pair = [unit, low]; + let decoded = char::decode_utf16(pair).next(); + match decoded { + Some(Ok(ch)) => out.push(ch), + _ => { + out.push_str(&input[i..i + 12]); + } + } + i += 12; + continue; + } + } + } + + if let Some(ch) = char::from_u32(unit as u32) { + out.push(ch); + } else { + out.push_str(&input[i..i + 6]); + } + i += 6; + continue; + } + } else if i + 3 <= bytes.len() { + if let Some(value) = parse_hex_u8(&bytes[i + 1..i + 3]) { + out.push(value as char); + i += 3; + continue; + } + } + } + + let ch = input[i..].chars().next().expect("valid utf-8 char boundary"); + out.push(ch); + i += ch.len_utf8(); + } + + out +} + +pub fn js_escape(vm: &mut Vm, args: &[u8]) -> NativeResult { + let input = if args.len() > 1 { + coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + } else { + "undefined".to_string() + }; + NativeResult::Ok(vm.intern(&escape_string(&input))) +} + +pub fn js_unescape(vm: &mut Vm, args: &[u8]) -> NativeResult { + let input = if args.len() > 1 { + coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + } else { + "undefined".to_string() + }; + NativeResult::Ok(vm.intern(&unescape_string(&input))) +} + +#[cfg(test)] +mod tests { + use super::{escape_string, unescape_string}; + + #[test] + fn escape_encodes_spaces_and_unicode() { + assert_eq!(escape_string("hello world"), "hello%20world"); + assert_eq!(escape_string("AΩ你"), "A%u03A9%u4F60"); + } + + #[test] + fn unescape_decodes_percent_sequences() { + assert_eq!(unescape_string("hello%20world"), "hello world"); + assert_eq!(unescape_string("%u03A9%u4F60"), "Ω你"); + } + + #[test] + fn unescape_keeps_invalid_sequences() { + assert_eq!(unescape_string("%uXYZ1%2G"), "%uXYZ1%2G"); + } } diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 62e3ba7..820cb61 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -16,8 +16,8 @@ use oxide_types::value::JsValue; impl Vm { pub fn new() -> Self { let core = KernelCore::new(KernelConfig::minimal()); - let session = KernelSession::new(&core); - bindings::init_kernel_builtins(&core, &session); + let mut session = KernelSession::new(&core); + bindings::init_kernel_builtins(&core, &mut session); let obj_proto = P::clone(&session.builtin_world().object_proto); Self { regs: [JsValue::undefined(); 256], @@ -51,8 +51,8 @@ impl Vm { } pub fn with_kernel_core(core: Arc) -> Self { - let session = KernelSession::new(&core); - bindings::init_kernel_builtins(&core, &session); + let mut session = KernelSession::new(&core); + bindings::init_kernel_builtins(&core, &mut session); let obj_proto = P::clone(&session.builtin_world().object_proto); Self { regs: [JsValue::undefined(); 256], @@ -86,8 +86,8 @@ impl Vm { } pub fn full_reset(&mut self) { - let new_session = KernelSession::new(&self.kernel_core); - bindings::init_kernel_builtins(&self.kernel_core, &new_session); + let mut new_session = KernelSession::new(&self.kernel_core); + bindings::init_kernel_builtins(&self.kernel_core, &mut new_session); self.object_prototype = P::clone(&new_session.builtin_world().object_proto); self.session = new_session; self.clear_execution_state(); diff --git a/crates/oxide_vm/tests/template_tests.rs b/crates/oxide_vm/tests/template_tests.rs index c6d5747..6f2d888 100644 --- a/crates/oxide_vm/tests/template_tests.rs +++ b/crates/oxide_vm/tests/template_tests.rs @@ -37,7 +37,10 @@ fn eval_val(source: &str) -> (Vm, Result) { fn to_str(vm: &Vm, val: JsValue) -> String { if val.is_string() { - vm.kernel_core().string_forge().lookup(val.as_string_index()).unwrap_or_default() + vm.kernel_core() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() } else { format!("{}", val) }