use crate::host::{self, with_host, JsObj};
use fusevm::Value;
pub fn parts(v: &Value) -> Option<(Value, Value)> {
with_host(|h| match h.get(v) {
Some(JsObj::Proxy {
target, handler, ..
}) => Some((target.clone(), handler.clone())),
_ => None,
})
}
fn revoked(v: &Value) -> bool {
with_host(|h| matches!(h.get(v), Some(JsObj::Proxy { revoked, .. }) if *revoked))
}
pub fn ultimate_target(v: &Value) -> Option<Value> {
let mut cur = parts(v)?.0;
for _ in 0..100 {
match parts(&cur) {
Some((t, _)) => cur = t,
None => return Some(cur),
}
}
Some(cur)
}
fn revoked_err(op: &str) -> String {
host::type_error(&format!(
"Cannot perform '{op}' on a proxy that has been revoked"
))
}
pub fn has_trap(v: &Value, name: &str) -> bool {
matches!(trap(v, name), Ok(Some(_)))
}
fn trap(v: &Value, name: &str) -> Result<Option<(Value, Value, Value)>, String> {
let Some((target, handler)) = parts(v) else {
return Ok(None);
};
if revoked(v) {
return Err(revoked_err(name));
}
let t = crate::builtins::get_property(&handler, name)?;
if matches!(t, Value::Undef) || with_host(|h| h.is_null(&t)) {
return Ok(None);
}
if !with_host(|h| host::is_callable(h, &t)) {
return Err(host::type_error(&format!(
"'{}' returned for property '{name}' of object '#<Object>' is not a function",
with_host(|h| h.str_of(&t))
)));
}
Ok(Some((t, target, handler)))
}
fn no_trap(v: &Value, op: &str) -> Result<Option<Value>, String> {
match parts(v) {
None => Ok(None),
Some((target, _)) if !revoked(v) => Ok(Some(target)),
Some(_) => Err(revoked_err(op)),
}
}
pub fn key_value(k: &str) -> Value {
with_host(|h| {
if let Some(s) = h.symbol_of_key(k) {
return s;
}
match k.strip_prefix("@@") {
Some(name) if host::WELL_KNOWN_SYMBOLS.contains(&name) => h.well_known_symbol(name),
_ => h.new_str(k),
}
})
}
fn call(t: &Value, handler: &Value, args: Vec<Value>) -> Result<Value, String> {
host::invoke(t, args, Some(handler.clone()))
}
fn invariant(msg: &str) -> String {
host::type_error(msg)
}
pub fn get(v: &Value, key: &str, receiver: &Value) -> Result<Option<Value>, String> {
if let Some((t, target, handler)) = trap(v, "get")? {
let k = key_value(key);
let got = call(&t, &handler, vec![target.clone(), k, receiver.clone()])?;
if let Some((val, writable, configurable, is_accessor)) =
crate::builtins::own_prop_facts(&target, key)
{
if !configurable && !is_accessor && !writable && !with_host(|h| h.strict_eq(&got, &val))
{
return Err(invariant(&format!(
"'get' on proxy: property '{key}' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value"
)));
}
}
return Ok(Some(got));
}
match no_trap(v, "get")? {
Some(target) => crate::builtins::get_property_recv(&target, key, receiver).map(Some),
None => Ok(None),
}
}
pub fn set(v: &Value, key: &str, val: &Value, receiver: &Value) -> Result<bool, String> {
if let Some((t, target, handler)) = trap(v, "set")? {
let k = key_value(key);
let r = call(
&t,
&handler,
vec![target.clone(), k, val.clone(), receiver.clone()],
)?;
if !with_host(|h| h.truthy(&r)) {
return Ok(false);
}
if let Some((cur, writable, configurable, is_accessor)) =
crate::builtins::own_prop_facts(&target, key)
{
if !configurable && !is_accessor && !writable && !with_host(|h| h.strict_eq(val, &cur))
{
return Err(invariant(&format!(
"'set' on proxy: trap returned truish for property '{key}' which exists in the proxy target as a non-configurable and non-writable data property with a different value"
)));
}
}
return Ok(true);
}
match no_trap(v, "set")? {
Some(target) => {
crate::builtins::set_property_pub(&target, key, val.clone())?;
Ok(true)
}
None => Ok(false),
}
}
pub fn has(v: &Value, key: &str) -> Result<Option<bool>, String> {
if let Some((t, target, handler)) = trap(v, "has")? {
let k = key_value(key);
let r = call(&t, &handler, vec![target.clone(), k])?;
let reported = with_host(|h| h.truthy(&r));
if !reported {
if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
if !configurable || !with_host(|h| h.is_extensible(&target)) {
return Err(invariant(&format!(
"'has' on proxy: trap returned falsish for property '{key}' which exists in the proxy target as non-configurable"
)));
}
}
}
return Ok(Some(reported));
}
match no_trap(v, "has")? {
Some(target) => crate::builtins::has_property(&target, key).map(Some),
None => Ok(None),
}
}
pub fn delete(v: &Value, key: &str) -> Result<Option<bool>, String> {
if let Some((t, target, handler)) = trap(v, "deleteProperty")? {
let k = key_value(key);
let r = call(&t, &handler, vec![target.clone(), k])?;
let reported = with_host(|h| h.truthy(&r));
if reported {
if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
if !configurable {
return Err(invariant(&format!(
"'deleteProperty' on proxy: trap returned truish for property '{key}' which is non-configurable in the proxy target"
)));
}
}
}
return Ok(Some(reported));
}
match no_trap(v, "deleteProperty")? {
Some(target) => crate::builtins::delete_property(&target, key).map(Some),
None => Ok(None),
}
}
pub fn own_keys(v: &Value) -> Result<Option<Vec<String>>, String> {
if let Some((t, target, handler)) = trap(v, "ownKeys")? {
let r = call(&t, &handler, vec![target.clone()])?;
let items = with_host(|h| h.iter_vec(&r))?;
let mut out = Vec::with_capacity(items.len());
for k in items {
out.push(host::to_property_key(&k)?);
}
let mut seen: Vec<&String> = Vec::with_capacity(out.len());
for k in &out {
if seen.contains(&k) {
return Err(invariant(&format!(
"'ownKeys' on proxy: trap returned duplicate entries for property '{k}'"
)));
}
seen.push(k);
}
let target_keys = with_host(|h| {
let mut ks = h.own_key_names(&target, false);
ks.extend(
h.own_symbol_keys(&target)
.iter()
.map(|sym| h.property_key(sym))
.collect::<Vec<_>>(),
);
ks
});
for k in &target_keys {
let pinned =
crate::builtins::own_prop_facts(&target, k).is_some_and(|(_, _, conf, _)| !conf);
if pinned && !out.contains(k) {
return Err(invariant(&format!(
"'ownKeys' on proxy: trap result did not include '{k}'"
)));
}
}
if !with_host(|h| h.is_extensible(&target)) {
for k in &target_keys {
if !out.contains(k) {
return Err(invariant(&format!(
"'ownKeys' on proxy: trap result did not include '{k}'"
)));
}
}
for k in &out {
if !target_keys.contains(k) {
return Err(invariant(
"'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible",
));
}
}
}
return Ok(Some(out));
}
match no_trap(v, "ownKeys")? {
Some(target) => {
let mut keys = with_host(|h| h.own_key_names(&target, false));
keys.extend(with_host(|h| {
h.own_symbol_keys(&target)
.iter()
.map(|s| h.property_key(s))
.collect::<Vec<_>>()
}));
Ok(Some(keys))
}
None => Ok(None),
}
}
pub fn get_own_descriptor(v: &Value, key: &str) -> Result<Option<Value>, String> {
if let Some((t, target, handler)) = trap(v, "getOwnPropertyDescriptor")? {
let k = key_value(key);
let d = call(&t, &handler, vec![target.clone(), k])?;
if matches!(d, Value::Undef) {
if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
if !configurable {
return Err(invariant(&format!(
"'getOwnPropertyDescriptor' on proxy: trap returned undefined for property '{key}' which is non-configurable in the proxy target"
)));
}
}
}
return Ok(Some(d));
}
match no_trap(v, "getOwnPropertyDescriptor")? {
Some(target) => {
let k = key_value(key);
crate::builtins::own_descriptor_pub(&target, k).map(Some)
}
None => Ok(None),
}
}
pub fn define_property(v: &Value, key: &str, desc: &Value) -> Result<bool, String> {
if let Some((t, target, handler)) = trap(v, "defineProperty")? {
let k = key_value(key);
let r = call(&t, &handler, vec![target.clone(), k, desc.clone()])?;
if !with_host(|h| h.truthy(&r)) {
return Ok(false);
}
if crate::builtins::own_prop_facts(&target, key).is_none()
&& !with_host(|h| h.is_extensible(&target))
{
return Err(invariant(&format!(
"'defineProperty' on proxy: trap returned truish for adding property '{key}' to the non-extensible proxy target"
)));
}
return Ok(true);
}
match no_trap(v, "defineProperty")? {
Some(target) => {
let k = key_value(key);
crate::builtins::define_property_pub(&target, k, desc.clone())?;
Ok(true)
}
None => Ok(false),
}
}
pub fn get_prototype_of(v: &Value) -> Result<Option<Value>, String> {
if let Some((t, target, handler)) = trap(v, "getPrototypeOf")? {
let reported = call(&t, &handler, vec![target.clone()])?;
if !with_host(|h| h.is_extensible(&target)) {
let actual = crate::builtins::prototype_of(&target);
if !with_host(|h| h.strict_eq(&reported, &actual)) {
return Err(invariant(
"'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype",
));
}
}
return Ok(Some(reported));
}
match no_trap(v, "getPrototypeOf")? {
Some(target) => Ok(Some(crate::builtins::prototype_of(&target))),
None => Ok(None),
}
}
pub fn set_prototype_of(v: &Value, proto: &Value) -> Result<bool, String> {
if let Some((t, target, handler)) = trap(v, "setPrototypeOf")? {
call(&t, &handler, vec![target, proto.clone()])?;
return Ok(true);
}
match no_trap(v, "setPrototypeOf")? {
Some(target) => {
with_host(|h| h.set_proto(&target, proto.clone()));
Ok(true)
}
None => Ok(false),
}
}
pub fn is_extensible(v: &Value) -> Result<Option<bool>, String> {
if let Some((t, target, handler)) = trap(v, "isExtensible")? {
let reported = call(&t, &handler, vec![target.clone()])?;
let reported = with_host(|h| h.truthy(&reported));
if reported != with_host(|h| h.is_extensible(&target)) {
return Err(invariant(
"'isExtensible' on proxy: trap result does not reflect extensibility of proxy target",
));
}
return Ok(Some(reported));
}
match no_trap(v, "isExtensible")? {
Some(target) => Ok(Some(with_host(|h| h.is_extensible(&target)))),
None => Ok(None),
}
}
pub fn prevent_extensions(v: &Value) -> Result<bool, String> {
if let Some((t, target, handler)) = trap(v, "preventExtensions")? {
call(&t, &handler, vec![target])?;
return Ok(true);
}
match no_trap(v, "preventExtensions")? {
Some(target) => {
with_host(|h| h.prevent_extensions(&target));
Ok(true)
}
None => Ok(false),
}
}
pub fn apply(v: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Option<Value>, String> {
if let Some((t, target, handler)) = trap(v, "apply")? {
let this_arg = this.unwrap_or(Value::Undef);
let list = with_host(|h| h.new_array(args));
return call(&t, &handler, vec![target, this_arg, list]).map(Some);
}
match no_trap(v, "apply")? {
Some(target) => host::invoke(&target, args, this).map(Some),
None => Ok(None),
}
}
pub fn construct(v: &Value, args: Vec<Value>, new_target: &Value) -> Result<Option<Value>, String> {
if let Some((t, target, handler)) = trap(v, "construct")? {
let list = with_host(|h| h.new_array(args));
return call(&t, &handler, vec![target, list, new_target.clone()]).map(Some);
}
match no_trap(v, "construct")? {
Some(target) => host::construct_nt(&target, args, new_target.clone()).map(Some),
None => Ok(None),
}
}
pub fn own_enum_string_keys(v: &Value) -> Result<Vec<String>, String> {
let Some(keys) = own_keys(v)? else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for k in keys {
if host::is_symbol_key(&k) {
continue;
}
let Some(d) = get_own_descriptor(v, &k)? else {
continue;
};
let enumerable = with_host(|h| match h.get(&d) {
Some(JsObj::Object(p)) => p.get("enumerable").map(|e| h.truthy(e)).unwrap_or(false),
_ => false,
});
if enumerable {
out.push(k);
}
}
Ok(out)
}
pub fn own_enumerable(v: &Value, key: &str) -> Result<bool, String> {
let Some(d) = get_own_descriptor(v, key)? else {
return Ok(false);
};
Ok(with_host(|h| match h.get(&d) {
Some(JsObj::Object(p)) => p.get("enumerable").map(|e| h.truthy(e)).unwrap_or(false),
_ => false,
}))
}
pub fn own_enum_entries(v: &Value) -> Result<Vec<(String, Value)>, String> {
let keys = own_enum_string_keys(v)?;
let mut out = Vec::with_capacity(keys.len());
for k in keys {
let val = get(v, &k, v)?.unwrap_or(Value::Undef);
out.push((k, val));
}
Ok(out)
}
fn wraps_array(v: &Value) -> bool {
match ultimate_target(v) {
Some(t) => with_host(|h| matches!(h.get(&t), Some(JsObj::Array(_)))),
None => false,
}
}
pub fn iterate(v: &Value) -> Result<Option<Vec<Value>>, String> {
if parts(v).is_none() {
return Ok(None);
}
let array_backed = wraps_array(v);
let iter_fn = get(v, "@@iterator", v)?.unwrap_or(Value::Undef);
let default_array_iter =
array_backed && with_host(|h| matches!(h.get(&iter_fn), Some(JsObj::BoundMethod { .. })));
if !default_array_iter && with_host(|h| host::is_callable(h, &iter_fn)) {
let iterator = host::invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
return host::drain_iterator(&iterator).map(Some);
}
if array_backed {
let len_v = get(v, "length", v)?.unwrap_or(Value::Undef);
let len = with_host(|h| h.to_number(&len_v));
let len = if len.is_finite() && len > 0.0 {
len as usize
} else {
0
};
let mut out = Vec::with_capacity(len);
for i in 0..len {
out.push(get(v, &i.to_string(), v)?.unwrap_or(Value::Undef));
}
return Ok(Some(out));
}
let target = no_trap(v, "get")?.expect("checked it is a proxy");
host::iter_all(&target).map(Some)
}
pub fn json_snapshot(v: &Value) -> Result<Value, String> {
if wraps_array(v) {
let items = iterate(v)?.unwrap_or_default();
return Ok(with_host(|h| h.new_array(items)));
}
let entries = own_enum_entries(v)?;
Ok(with_host(|h| {
let mut m = indexmap::IndexMap::new();
for (k, val) in entries {
m.insert(k, val);
}
h.new_object(m)
}))
}
pub fn create(args: &[Value]) -> Result<Value, String> {
let target = args.first().cloned().unwrap_or(Value::Undef);
let handler = args.get(1).cloned().unwrap_or(Value::Undef);
let ok = |v: &Value| {
with_host(|h| matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v))
};
if !ok(&target) || !ok(&handler) {
return Err(host::type_error(
"Cannot create proxy with a non-object as target or handler",
));
}
Ok(with_host(|h| {
h.alloc(JsObj::Proxy {
target,
handler,
revoked: false,
})
}))
}
pub fn revocable(args: &[Value]) -> Result<Value, String> {
let proxy = create(args)?;
let idx = match proxy {
Value::Obj(i) => i,
_ => unreachable!("create returns a heap object"),
};
let revoke = with_host(|h| h.alloc(JsObj::Builtin(format!("@@prevoke:{idx}"))));
Ok(with_host(|h| {
let mut m = indexmap::IndexMap::new();
m.insert("proxy".to_string(), proxy);
m.insert("revoke".to_string(), revoke);
h.new_object(m)
}))
}
pub fn revoke(idx: u32) -> Value {
with_host(|h| {
if let Some(JsObj::Proxy { revoked, .. }) = h.get_mut(&Value::Obj(idx)) {
*revoked = true;
}
});
Value::Undef
}