use deno_core::v8;
use std::collections::HashMap;
const NATIVE_TAG: &str = "__browser_oxide_native__";
pub struct IframeRealmStore {
pub contexts: HashMap<u32, v8::Global<v8::Context>>,
pub globals: HashMap<u32, v8::Global<v8::Object>>,
pub orig_fp_tostring: Option<v8::Global<v8::Function>>,
pub native_tag_sym: Option<v8::Global<v8::Symbol>>,
}
impl Default for IframeRealmStore {
fn default() -> Self {
Self::new()
}
}
impl IframeRealmStore {
pub fn new() -> Self {
Self {
contexts: HashMap::new(),
globals: HashMap::new(),
orig_fp_tostring: None,
native_tag_sym: None,
}
}
}
pub fn create_child_realm(
scope: &mut v8::PinScope,
) -> Option<(v8::Global<v8::Context>, v8::Global<v8::Object>)> {
let ctx = v8::Context::new(scope, v8::ContextOptions::default());
let global = {
let cscope = &mut v8::ContextScope::new(scope, ctx);
let g = ctx.global(cscope);
v8::Global::new(cscope, g)
};
Some((v8::Global::new(scope, ctx), global))
}
pub fn capture_original_fp_tostring(scope: &mut v8::PinScope) -> Option<v8::Global<v8::Function>> {
let ctx = scope.get_current_context();
let global = ctx.global(scope);
let fkey = v8::String::new(scope, "Function")?;
let fctor = global.get(scope, fkey.into())?;
let fctor = v8::Local::<v8::Object>::try_from(fctor).ok()?;
let pkey = v8::String::new(scope, "prototype")?;
let fproto = fctor.get(scope, pkey.into())?;
let fproto = v8::Local::<v8::Object>::try_from(fproto).ok()?;
let tskey = v8::String::new(scope, "toString")?;
let ts = fproto.get(scope, tskey.into())?;
let ts = v8::Local::<v8::Function>::try_from(ts).ok()?;
Some(v8::Global::new(scope, ts))
}
fn fp_to_string_cb<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue,
) {
let this: v8::Local<v8::Value> = args.this().into();
let data = args.data();
let orig_fn: Option<v8::Local<v8::Function>>;
let tag_sym: Option<v8::Local<v8::Symbol>>;
if let Ok(arr) = v8::Local::<v8::Array>::try_from(data) {
orig_fn = arr
.get_index(scope, 0)
.and_then(|v| v8::Local::<v8::Function>::try_from(v).ok());
tag_sym = arr
.get_index(scope, 1)
.and_then(|v| v8::Local::<v8::Symbol>::try_from(v).ok());
} else {
orig_fn = v8::Local::<v8::Function>::try_from(data).ok();
tag_sym = None;
}
if let Ok(this_obj) = v8::Local::<v8::Object>::try_from(this) {
let maybe_tag: Option<String> = if let Some(sym) = tag_sym {
if let Some(tagv) = this_obj.get(scope, sym.into()) {
if tagv.is_string() {
Some(tagv.to_rust_string_lossy(scope))
} else {
None
}
} else {
None
}
} else if let Some(key) = v8::String::new(scope, NATIVE_TAG) {
let sym = v8::Symbol::for_api(scope, key);
if let Some(tagv) = this_obj.get(scope, sym.into()) {
if tagv.is_string() {
Some(tagv.to_rust_string_lossy(scope))
} else {
None
}
} else {
None
}
} else {
None
};
if let Some(tag) = maybe_tag {
let s = format!("function {tag}() {{ [native code] }}");
if let Some(out) = v8::String::new(scope, &s) {
rv.set(out.into());
return;
}
}
}
if this.is_proxy() {
if let Ok(proxy) = v8::Local::<v8::Proxy>::try_from(this) {
let target = proxy.get_target(scope);
if target.is_function() {
let name = v8::Local::<v8::Object>::try_from(target)
.ok()
.and_then(|to| {
v8::String::new(scope, "name")
.and_then(|k| to.get(scope, k.into()))
.map(|nv| nv.to_rust_string_lossy(scope))
})
.unwrap_or_default();
let s = format!("function {name}() {{ [native code] }}");
if let Some(out) = v8::String::new(scope, &s) {
rv.set(out.into());
}
return;
}
}
}
if let Some(orig) = orig_fn {
if let Some(res) = orig.call(scope, this, &[]) {
rv.set(res);
}
return;
}
if let Some(out) = v8::String::new(scope, "function () { [native code] }") {
rv.set(out.into());
}
}
pub fn install_native_fp_tostring(
scope: &mut v8::PinScope,
original: &v8::Global<v8::Function>,
native_tag_sym: Option<&v8::Global<v8::Symbol>>,
) -> bool {
let orig_local = v8::Local::new(scope, original);
let data_val: v8::Local<v8::Value> = if let Some(sym_g) = native_tag_sym {
let sym_local = v8::Local::new(scope, sym_g);
let arr = v8::Array::new(scope, 2);
let i0 = v8::Integer::new(scope, 0);
let i1 = v8::Integer::new(scope, 1);
arr.set(scope, i0.into(), orig_local.into());
arr.set(scope, i1.into(), sym_local.into());
arr.into()
} else {
orig_local.into()
};
let tmpl = v8::FunctionTemplate::builder(fp_to_string_cb)
.length(0)
.constructor_behavior(v8::ConstructorBehavior::Throw)
.side_effect_type(v8::SideEffectType::HasNoSideEffect)
.data(data_val)
.build(scope);
if let Some(name) = v8::String::new(scope, "toString") {
tmpl.set_class_name(name);
}
let func = match tmpl.get_function(scope) {
Some(f) => f,
None => return false,
};
if let Some(name) = v8::String::new(scope, "toString") {
func.set_name(name);
}
let ctx = scope.get_current_context();
let global = ctx.global(scope);
let Some(fkey) = v8::String::new(scope, "Function") else {
return false;
};
let Some(fctor) = global.get(scope, fkey.into()) else {
return false;
};
let Ok(fctor) = v8::Local::<v8::Object>::try_from(fctor) else {
return false;
};
let Some(pkey) = v8::String::new(scope, "prototype") else {
return false;
};
let Some(fproto) = fctor.get(scope, pkey.into()) else {
return false;
};
let Ok(fproto) = v8::Local::<v8::Object>::try_from(fproto) else {
return false;
};
let Some(tskey) = v8::String::new(scope, "toString") else {
return false;
};
fproto.define_own_property(
scope,
tskey.into(),
func.into(),
v8::PropertyAttribute::DONT_ENUM,
);
true
}
#[cfg(test)]
mod tests {
use super::*;
use deno_core::{JsRuntime, RuntimeOptions};
#[test]
fn verify_inner_global_property_visibility() {
let mut rt = JsRuntime::new(RuntimeOptions::default());
let main_ctx = rt.main_context();
v8::scope_with_context!(let scope, rt.v8_isolate(), &main_ctx);
let child_ctx = v8::Context::new(scope, v8::ContextOptions::default());
{
let cs = &mut v8::ContextScope::new(scope, child_ctx);
let window_proto_src = v8::String::new(cs, "(function Window(){}).prototype").unwrap();
let window_proto_script = v8::Script::compile(cs, window_proto_src, None).unwrap();
let window_proto_val = window_proto_script.run(cs).unwrap();
let proxy = child_ctx.global(cs);
proxy.set_prototype(cs, window_proto_val);
let proto_after = proxy
.get_prototype(cs)
.expect("proxy must have a prototype");
let inner = v8::Local::<v8::Object>::try_from(proto_after)
.expect("prototype after set_prototype must still be an Object (inner global)");
let inner_hash = inner.get_identity_hash();
let proxy_hash = proxy.get_identity_hash();
assert_ne!(
inner_hash, proxy_hash,
"inner global must differ from proxy"
);
let key = v8::String::new(cs, "__testProp__").unwrap();
let val = v8::Integer::new(cs, 42);
inner.create_data_property(cs, key.into(), val.into());
let src = v8::String::new(cs, "typeof __testProp__ + ':' + __testProp__").unwrap();
let script = v8::Script::compile(cs, src, None).unwrap();
let res = script.run(cs).unwrap().to_rust_string_lossy(cs);
assert_eq!(res, "number:42",
"inner-global property must be visible inside realm after set_prototype; got: {res}");
}
}
#[test]
fn child_realm_has_genuine_native_intrinsics() {
let mut rt = JsRuntime::new(RuntimeOptions::default());
let main_ctx = rt.main_context();
v8::scope_with_context!(let scope, rt.v8_isolate(), &main_ctx);
let parent_obj_hash = {
let g = scope.get_current_context().global(scope);
let k = v8::String::new(scope, "Object").unwrap();
let o = g.get(scope, k.into()).unwrap();
v8::Local::<v8::Object>::try_from(o)
.unwrap()
.get_identity_hash()
};
let (ctx_g, _glob_g) = create_child_realm(scope).expect("child realm created");
let ctx = v8::Local::new(scope, &ctx_g);
let cs = &mut v8::ContextScope::new(scope, ctx);
let src = v8::String::new(
cs,
"JSON.stringify({\
objTS: Function.prototype.toString.call(Object),\
fnName: Function.name,\
objName: Object.name,\
arrTS: Array.prototype.slice.toString(),\
typeofWin: typeof globalThis,\
isProxyish: (function(){try{return String(globalThis).indexOf('Proxy')>=0}catch(e){return 'err'}})()\
})",
)
.unwrap();
let script = v8::Script::compile(cs, src, None).unwrap();
let res = script.run(cs).unwrap();
let json = res.to_rust_string_lossy(cs);
assert!(
json.contains("function Object() { [native code] }"),
"child Object must be a real native, got: {json}"
);
assert!(
json.contains("function slice() { [native code] }"),
"child Array.prototype.slice must be native, got: {json}"
);
assert!(
json.contains("\"objName\":\"Object\""),
"child Object.name must be 'Object', got: {json}"
);
let child_obj_hash = {
let g = ctx.global(cs);
let k = v8::String::new(cs, "Object").unwrap();
let o = g.get(cs, k.into()).unwrap();
v8::Local::<v8::Object>::try_from(o)
.unwrap()
.get_identity_hash()
};
assert_ne!(
parent_obj_hash, child_obj_hash,
"child realm Object must be realm-distinct from parent \
(real per-frame realm, not parent-aliased)"
);
}
#[test]
fn native_fp_tostring_uses_js_symbol_registry() {
use deno_core::JsRuntime;
let mut rt = JsRuntime::new(RuntimeOptions::default());
let orig_g = {
let main_ctx = rt.main_context();
v8::scope_with_context!(let scope, rt.v8_isolate(), &main_ctx);
capture_original_fp_tostring(scope).expect("capture original")
};
rt.execute_script(
"<test>",
r#"
const _nativeTag = Symbol.for('__browser_oxide_native__');
function myTaggedFn() {}
Object.defineProperty(myTaggedFn, _nativeTag, { value: 'myTaggedFn', configurable: true });
globalThis.__testFn = myTaggedFn;
"#,
)
.expect("setup script");
let native_tag_sym_g: Option<v8::Global<v8::Symbol>> = {
let main_ctx = rt.main_context();
v8::scope_with_context!(let scope, rt.v8_isolate(), &main_ctx);
let src = v8::String::new(scope, "Symbol.for('__browser_oxide_native__')").unwrap();
let script = v8::Script::compile(scope, src, None).unwrap();
let val = script.run(scope).unwrap();
let sym = v8::Local::<v8::Symbol>::try_from(val).ok().unwrap();
Some(v8::Global::new(scope, sym))
};
{
let main_ctx = rt.main_context();
v8::scope_with_context!(let scope, rt.v8_isolate(), &main_ctx);
install_native_fp_tostring(scope, &orig_g, native_tag_sym_g.as_ref());
}
let s = rt
.execute_script(
"<test>",
"Function.prototype.toString.call(globalThis.__testFn)",
)
.map(|v| {
let main_ctx = rt.main_context();
v8::scope_with_context!(let scope, rt.v8_isolate(), &main_ctx);
let local = v8::Local::new(scope, &v);
local.to_rust_string_lossy(scope)
})
.expect("eval");
assert_eq!(
s, "function myTaggedFn() { [native code] }",
"tagged function should return native-code string; got: {s}"
);
let s2 = rt
.execute_script(
"<test>",
"Function.prototype.toString.call(Array.prototype.slice)",
)
.map(|v| {
let main_ctx = rt.main_context();
v8::scope_with_context!(let scope, rt.v8_isolate(), &main_ctx);
let local = v8::Local::new(scope, &v);
local.to_rust_string_lossy(scope)
})
.expect("eval2");
assert!(
s2.contains("[native code]"),
"real native should return [native code]; got: {s2}"
);
}
}