Skip to main content

nodejs/
proxy.rs

1//! `Proxy` — the ECMAScript exotic object (10.5) whose essential internal
2//! methods are redirected to a handler's traps.
3//!
4//! A Proxy is not a shape node-js could fake with a property map: every one of
5//! its internal methods has to be diverted, so it is its own heap variant
6//! (`JsObj::Proxy`) and this module is the single place the diversion happens.
7//! The funnels the rest of the runtime already routes through —
8//! `builtins::get_property` / `set_property` / `has_property` /
9//! `delete_property` / `object_keys`, `host::invoke` / `construct_nt` — each
10//! call into here first; when the handler has no trap for the operation, the
11//! `no_trap` fallback re-runs the SAME funnel against the target, which is what
12//! makes `new Proxy(t, {})` observationally indistinguishable from `t`.
13//!
14//! Not implemented, deliberately, and recorded in BUGS.md rather than faked: the
15//! spec's trap-result *invariant* checks (10.5.x steps that throw when a trap
16//! contradicts a non-configurable/non-extensible target property). node-js
17//! reports the trap's answer as given. Every trap itself is real.
18
19use crate::host::{self, with_host, JsObj};
20use fusevm::Value;
21
22/// `(target, handler)` when `v` is a Proxy — revoked or not.
23pub fn parts(v: &Value) -> Option<(Value, Value)> {
24    with_host(|h| match h.get(v) {
25        Some(JsObj::Proxy {
26            target, handler, ..
27        }) => Some((target.clone(), handler.clone())),
28        _ => None,
29    })
30}
31
32/// Whether `v` is a Proxy whose `[[ProxyHandler]]` is still live.
33fn revoked(v: &Value) -> bool {
34    with_host(|h| matches!(h.get(v), Some(JsObj::Proxy { revoked, .. }) if *revoked))
35}
36
37/// The proxy chain's ultimate non-proxy target — what `Array.isArray`,
38/// `Object.prototype.toString` and `typeof` classify by (10.5.x defer those to
39/// `[[ProxyTarget]]`, and a proxy of a proxy defers again).
40pub fn ultimate_target(v: &Value) -> Option<Value> {
41    let mut cur = parts(v)?.0;
42    for _ in 0..100 {
43        match parts(&cur) {
44            Some((t, _)) => cur = t,
45            None => return Some(cur),
46        }
47    }
48    Some(cur)
49}
50
51/// V8's message for an operation attempted on a revoked proxy.
52fn revoked_err(op: &str) -> String {
53    host::type_error(&format!(
54        "Cannot perform '{op}' on a proxy that has been revoked"
55    ))
56}
57
58/// Resolve trap `name` on `v`'s handler.
59///
60/// `Ok(None)` means "not a proxy, or no such trap" — the caller runs its
61/// ordinary path (against the target, for the no-trap case). A revoked proxy
62/// and a non-callable trap both throw here, before any target work happens.
63fn trap(v: &Value, name: &str) -> Result<Option<(Value, Value, Value)>, String> {
64    let Some((target, handler)) = parts(v) else {
65        return Ok(None);
66    };
67    if revoked(v) {
68        return Err(revoked_err(name));
69    }
70    let t = crate::builtins::get_property(&handler, name)?;
71    if matches!(t, Value::Undef) || with_host(|h| h.is_null(&t)) {
72        return Ok(None);
73    }
74    if !with_host(|h| host::is_callable(h, &t)) {
75        return Err(host::type_error(&format!(
76            "'{}' returned for property '{name}' of object '#<Object>' is not a function",
77            with_host(|h| h.str_of(&t))
78        )));
79    }
80    Ok(Some((t, target, handler)))
81}
82
83/// The target of a proxy whose handler declines the operation (no trap), or
84/// `None` when `v` is not a proxy at all. Errors on a revoked proxy.
85fn no_trap(v: &Value, op: &str) -> Result<Option<Value>, String> {
86    match parts(v) {
87        None => Ok(None),
88        Some((target, _)) if !revoked(v) => Ok(Some(target)),
89        Some(_) => Err(revoked_err(op)),
90    }
91}
92
93/// An internal property key as the JS value a trap receives: the SYMBOL for a
94/// symbol-keyed property (`@@sym:7`, `@@iterator`), a string otherwise. A trap
95/// that inspects its key argument must see what the script wrote.
96pub fn key_value(k: &str) -> Value {
97    with_host(|h| {
98        if let Some(s) = h.symbol_of_key(k) {
99            return s;
100        }
101        match k.strip_prefix("@@") {
102            Some(name) if host::WELL_KNOWN_SYMBOLS.contains(&name) => h.well_known_symbol(name),
103            _ => h.new_str(k),
104        }
105    })
106}
107
108fn call(t: &Value, handler: &Value, args: Vec<Value>) -> Result<Value, String> {
109    host::invoke(t, args, Some(handler.clone()))
110}
111
112// ── the thirteen traps ───────────────────────────────────────────────────────
113
114/// `[[Get]]`. `Ok(None)` → not a proxy; the caller proceeds normally.
115pub fn get(v: &Value, key: &str, receiver: &Value) -> Result<Option<Value>, String> {
116    if let Some((t, target, handler)) = trap(v, "get")? {
117        let k = key_value(key);
118        return call(&t, &handler, vec![target, k, receiver.clone()]).map(Some);
119    }
120    match no_trap(v, "get")? {
121        Some(target) => crate::builtins::get_property_recv(&target, key, receiver).map(Some),
122        None => Ok(None),
123    }
124}
125
126/// `[[Set]]`. `Ok(true)` means the write was handled here.
127pub fn set(v: &Value, key: &str, val: &Value, receiver: &Value) -> Result<bool, String> {
128    if let Some((t, target, handler)) = trap(v, "set")? {
129        let k = key_value(key);
130        call(&t, &handler, vec![target, k, val.clone(), receiver.clone()])?;
131        return Ok(true);
132    }
133    match no_trap(v, "set")? {
134        Some(target) => {
135            crate::builtins::set_property_pub(&target, key, val.clone())?;
136            Ok(true)
137        }
138        None => Ok(false),
139    }
140}
141
142/// `[[HasProperty]]` (`key in proxy`).
143pub fn has(v: &Value, key: &str) -> Result<Option<bool>, String> {
144    if let Some((t, target, handler)) = trap(v, "has")? {
145        let k = key_value(key);
146        let r = call(&t, &handler, vec![target, k])?;
147        return Ok(Some(with_host(|h| h.truthy(&r))));
148    }
149    match no_trap(v, "has")? {
150        Some(target) => crate::builtins::has_property(&target, key).map(Some),
151        None => Ok(None),
152    }
153}
154
155/// `[[Delete]]`.
156pub fn delete(v: &Value, key: &str) -> Result<Option<bool>, String> {
157    if let Some((t, target, handler)) = trap(v, "deleteProperty")? {
158        let k = key_value(key);
159        let r = call(&t, &handler, vec![target, k])?;
160        return Ok(Some(with_host(|h| h.truthy(&r))));
161    }
162    match no_trap(v, "deleteProperty")? {
163        Some(target) => crate::builtins::delete_property(&target, key).map(Some),
164        None => Ok(None),
165    }
166}
167
168/// `[[OwnPropertyKeys]]`, as INTERNAL key strings (so a symbol key comes back as
169/// `@@sym:<id>` — the form the rest of the runtime indexes by).
170pub fn own_keys(v: &Value) -> Result<Option<Vec<String>>, String> {
171    if let Some((t, target, handler)) = trap(v, "ownKeys")? {
172        let r = call(&t, &handler, vec![target])?;
173        let items = with_host(|h| h.iter_vec(&r))?;
174        let mut out = Vec::with_capacity(items.len());
175        for k in items {
176            out.push(host::to_property_key(&k)?);
177        }
178        return Ok(Some(out));
179    }
180    match no_trap(v, "ownKeys")? {
181        Some(target) => {
182            let mut keys = with_host(|h| h.own_key_names(&target, false));
183            keys.extend(with_host(|h| {
184                h.own_symbol_keys(&target)
185                    .iter()
186                    .map(|s| h.property_key(s))
187                    .collect::<Vec<_>>()
188            }));
189            Ok(Some(keys))
190        }
191        None => Ok(None),
192    }
193}
194
195/// `[[GetOwnProperty]]` — the descriptor object (or `undefined`).
196pub fn get_own_descriptor(v: &Value, key: &str) -> Result<Option<Value>, String> {
197    if let Some((t, target, handler)) = trap(v, "getOwnPropertyDescriptor")? {
198        let k = key_value(key);
199        return call(&t, &handler, vec![target, k]).map(Some);
200    }
201    match no_trap(v, "getOwnPropertyDescriptor")? {
202        Some(target) => {
203            let k = key_value(key);
204            crate::builtins::own_descriptor_pub(&target, k).map(Some)
205        }
206        None => Ok(None),
207    }
208}
209
210/// `[[DefineOwnProperty]]`.
211pub fn define_property(v: &Value, key: &str, desc: &Value) -> Result<bool, String> {
212    if let Some((t, target, handler)) = trap(v, "defineProperty")? {
213        let k = key_value(key);
214        call(&t, &handler, vec![target, k, desc.clone()])?;
215        return Ok(true);
216    }
217    match no_trap(v, "defineProperty")? {
218        Some(target) => {
219            let k = key_value(key);
220            crate::builtins::define_property_pub(&target, k, desc.clone())?;
221            Ok(true)
222        }
223        None => Ok(false),
224    }
225}
226
227/// `[[GetPrototypeOf]]`.
228pub fn get_prototype_of(v: &Value) -> Result<Option<Value>, String> {
229    if let Some((t, target, handler)) = trap(v, "getPrototypeOf")? {
230        return call(&t, &handler, vec![target]).map(Some);
231    }
232    match no_trap(v, "getPrototypeOf")? {
233        Some(target) => Ok(Some(crate::builtins::prototype_of(&target))),
234        None => Ok(None),
235    }
236}
237
238/// `[[SetPrototypeOf]]`.
239pub fn set_prototype_of(v: &Value, proto: &Value) -> Result<bool, String> {
240    if let Some((t, target, handler)) = trap(v, "setPrototypeOf")? {
241        call(&t, &handler, vec![target, proto.clone()])?;
242        return Ok(true);
243    }
244    match no_trap(v, "setPrototypeOf")? {
245        Some(target) => {
246            with_host(|h| h.set_proto(&target, proto.clone()));
247            Ok(true)
248        }
249        None => Ok(false),
250    }
251}
252
253/// `[[IsExtensible]]`.
254pub fn is_extensible(v: &Value) -> Result<Option<bool>, String> {
255    if let Some((t, target, handler)) = trap(v, "isExtensible")? {
256        let r = call(&t, &handler, vec![target])?;
257        return Ok(Some(with_host(|h| h.truthy(&r))));
258    }
259    match no_trap(v, "isExtensible")? {
260        Some(target) => Ok(Some(with_host(|h| h.is_extensible(&target)))),
261        None => Ok(None),
262    }
263}
264
265/// `[[PreventExtensions]]`.
266pub fn prevent_extensions(v: &Value) -> Result<bool, String> {
267    if let Some((t, target, handler)) = trap(v, "preventExtensions")? {
268        call(&t, &handler, vec![target])?;
269        return Ok(true);
270    }
271    match no_trap(v, "preventExtensions")? {
272        Some(target) => {
273            with_host(|h| h.prevent_extensions(&target));
274            Ok(true)
275        }
276        None => Ok(false),
277    }
278}
279
280/// `[[Call]]`.
281pub fn apply(v: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Option<Value>, String> {
282    if let Some((t, target, handler)) = trap(v, "apply")? {
283        let this_arg = this.unwrap_or(Value::Undef);
284        let list = with_host(|h| h.new_array(args));
285        return call(&t, &handler, vec![target, this_arg, list]).map(Some);
286    }
287    match no_trap(v, "apply")? {
288        Some(target) => host::invoke(&target, args, this).map(Some),
289        None => Ok(None),
290    }
291}
292
293/// `[[Construct]]`.
294pub fn construct(v: &Value, args: Vec<Value>, new_target: &Value) -> Result<Option<Value>, String> {
295    if let Some((t, target, handler)) = trap(v, "construct")? {
296        let list = with_host(|h| h.new_array(args));
297        return call(&t, &handler, vec![target, list, new_target.clone()]).map(Some);
298    }
299    match no_trap(v, "construct")? {
300        Some(target) => host::construct_nt(&target, args, new_target.clone()).map(Some),
301        None => Ok(None),
302    }
303}
304
305// ── enumeration built on the traps ───────────────────────────────────────────
306
307/// The own keys of a proxy that are ENUMERABLE string keys — `Object.keys`,
308/// `for-in`'s own half, object spread and `JSON.stringify` all need this shape.
309/// 10.5.11 defines it as `ownKeys` filtered by each key's `[[GetOwnProperty]]`,
310/// so both traps really do run, in that order.
311pub fn own_enum_string_keys(v: &Value) -> Result<Vec<String>, String> {
312    let Some(keys) = own_keys(v)? else {
313        return Ok(Vec::new());
314    };
315    let mut out = Vec::new();
316    for k in keys {
317        if host::is_symbol_key(&k) {
318            continue;
319        }
320        let Some(d) = get_own_descriptor(v, &k)? else {
321            continue;
322        };
323        let enumerable = with_host(|h| match h.get(&d) {
324            Some(JsObj::Object(p)) => p.get("enumerable").map(|e| h.truthy(e)).unwrap_or(false),
325            _ => false,
326        });
327        if enumerable {
328            out.push(k);
329        }
330    }
331    Ok(out)
332}
333
334/// `(key, value)` for every own enumerable string key — spread / `Object.assign`
335/// / `Object.entries` / `JSON.stringify`. Each value is read through the `get`
336/// trap, as the spec's `CreateDataPropertyOrThrow(…, Get(from, key))` requires.
337pub fn own_enum_entries(v: &Value) -> Result<Vec<(String, Value)>, String> {
338    let keys = own_enum_string_keys(v)?;
339    let mut out = Vec::with_capacity(keys.len());
340    for k in keys {
341        let val = get(v, &k, v)?.unwrap_or(Value::Undef);
342        out.push((k, val));
343    }
344    Ok(out)
345}
346
347/// Whether the proxy chain bottoms out in an Array — the shape `IsArray` and
348/// `Array.prototype[Symbol.iterator]` both key off.
349fn wraps_array(v: &Value) -> bool {
350    match ultimate_target(v) {
351        Some(t) => with_host(|h| matches!(h.get(&t), Some(JsObj::Array(_)))),
352        None => false,
353    }
354}
355
356/// `[...proxy]` / `for (… of proxy)`. `Ok(None)` → not a proxy.
357///
358/// Three cases, in the order `GetIterator` reaches them:
359/// a user `Symbol.iterator` read THROUGH the `get` trap; an array target, whose
360/// `Array.prototype[Symbol.iterator]` observably does `Get(O, "length")` then
361/// `Get(O, i)` (so a `get` trap that lies about either is honored); and anything
362/// else (Map/Set/string/generator target), which iterates as the target does.
363pub fn iterate(v: &Value) -> Result<Option<Vec<Value>>, String> {
364    if parts(v).is_none() {
365        return Ok(None);
366    }
367    let array_backed = wraps_array(v);
368    let iter_fn = get(v, "@@iterator", v)?.unwrap_or(Value::Undef);
369    // node-js models `Array.prototype[Symbol.iterator]` as a thunk BOUND to the
370    // array it was read off, where the real method is generic over `this`. Read
371    // through a proxy, that thunk would walk the TARGET and ignore every answer
372    // the `get` trap gave — so an array-backed proxy still holding the default
373    // falls through to the length-driven walk, which is what the generic method
374    // observably does. A user-installed iterator is an ordinary function value
375    // and keeps the fast path.
376    let default_array_iter =
377        array_backed && with_host(|h| matches!(h.get(&iter_fn), Some(JsObj::BoundMethod { .. })));
378    if !default_array_iter && with_host(|h| host::is_callable(h, &iter_fn)) {
379        let iterator = host::invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
380        return host::drain_iterator(&iterator).map(Some);
381    }
382    if array_backed {
383        let len_v = get(v, "length", v)?.unwrap_or(Value::Undef);
384        let len = with_host(|h| h.to_number(&len_v));
385        let len = if len.is_finite() && len > 0.0 {
386            len as usize
387        } else {
388            0
389        };
390        let mut out = Vec::with_capacity(len);
391        for i in 0..len {
392            out.push(get(v, &i.to_string(), v)?.unwrap_or(Value::Undef));
393        }
394        return Ok(Some(out));
395    }
396    let target = no_trap(v, "get")?.expect("checked it is a proxy");
397    host::iter_all(&target).map(Some)
398}
399
400/// The plain value `JSON.stringify` serializes a proxy as. `SerializeJSONArray`
401/// and `SerializeJSONObject` both read every member through `[[Get]]`, so the
402/// snapshot is taken through the traps rather than off the target.
403pub fn json_snapshot(v: &Value) -> Result<Value, String> {
404    if wraps_array(v) {
405        let items = iterate(v)?.unwrap_or_default();
406        return Ok(with_host(|h| h.new_array(items)));
407    }
408    let entries = own_enum_entries(v)?;
409    Ok(with_host(|h| {
410        let mut m = indexmap::IndexMap::new();
411        for (k, val) in entries {
412            m.insert(k, val);
413        }
414        h.new_object(m)
415    }))
416}
417
418// ── construction ─────────────────────────────────────────────────────────────
419
420/// `new Proxy(target, handler)` (10.5.14 `ProxyCreate`).
421pub fn create(args: &[Value]) -> Result<Value, String> {
422    let target = args.first().cloned().unwrap_or(Value::Undef);
423    let handler = args.get(1).cloned().unwrap_or(Value::Undef);
424    let ok = |v: &Value| {
425        with_host(|h| matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v))
426    };
427    if !ok(&target) || !ok(&handler) {
428        return Err(host::type_error(
429            "Cannot create proxy with a non-object as target or handler",
430        ));
431    }
432    Ok(with_host(|h| {
433        h.alloc(JsObj::Proxy {
434            target,
435            handler,
436            revoked: false,
437        })
438    }))
439}
440
441/// `Proxy.revocable(target, handler)` → `{ proxy, revoke }`. The revoker is a
442/// builtin thunk keyed by the proxy's heap index, so calling it twice is the
443/// no-op the spec asks for rather than a second teardown.
444pub fn revocable(args: &[Value]) -> Result<Value, String> {
445    let proxy = create(args)?;
446    let idx = match proxy {
447        Value::Obj(i) => i,
448        _ => unreachable!("create returns a heap object"),
449    };
450    let revoke = with_host(|h| h.alloc(JsObj::Builtin(format!("@@prevoke:{idx}"))));
451    Ok(with_host(|h| {
452        let mut m = indexmap::IndexMap::new();
453        m.insert("proxy".to_string(), proxy);
454        m.insert("revoke".to_string(), revoke);
455        h.new_object(m)
456    }))
457}
458
459/// Run a `@@prevoke:<idx>` thunk: mark the proxy dead so every trap throws.
460///
461/// The target handle is KEPT rather than nulled as 10.5.15 step 5 words it,
462/// because `typeof` is fixed at creation by whether the target was callable and
463/// V8 still answers `'function'` for a revoked proxy of a function. Nothing can
464/// read the target through the proxy anymore — `revoked` is checked before any
465/// trap or fallback runs.
466pub fn revoke(idx: u32) -> Value {
467    with_host(|h| {
468        if let Some(JsObj::Proxy { revoked, .. }) = h.get_mut(&Value::Obj(idx)) {
469            *revoked = true;
470        }
471    });
472    Value::Undef
473}