Skip to main content

nodejs/stdlib/
mod.rs

1//! Node.js core modules implemented natively for node-js.
2//!
3//! A `require(spec)` (see `builtins::call_builtin_function`) resolves a supported
4//! module to a `JsObj::Builtin("<module>")` namespace value — exactly the shape
5//! of the built-in `console`/`Math` namespaces — so `mod.method(...)` dispatches
6//! through `host::call_method` → `builtins::call_builtin_function("<module>.<method>")`
7//! → `stdlib::call`, and `const { method } = require('mod')` reads the method as a
8//! first-class `Builtin("mod.method")` via `namespace_property`.
9//!
10//! Every stdlib function is free-standing and acquires the thread-local `JsHost`
11//! through `with_host` only around allocations (and releases it before any
12//! re-entrant `host::invoke`), so callbacks (`fs` async, `EventEmitter.emit`,
13//! `assert.throws`) never double-borrow the host. Stateful instances (`Buffer`,
14//! crypto `Hash`, `EventEmitter`, `URL`) are plain objects carrying a hidden
15//! `@@native` tag (filtered from enumeration/display like `@@iterator`); their
16//! methods route through `instance_call` from `host::call_method`.
17
18use crate::host::{with_host, JsObj};
19use fusevm::Value;
20
21pub mod assert;
22pub mod assert_diff;
23pub mod async_hooks;
24pub mod buffer;
25pub mod child_process;
26pub mod cluster;
27pub mod console;
28pub mod constants;
29pub mod crypto;
30pub mod date;
31pub mod dgram;
32pub mod diagnostics_channel;
33pub mod dns;
34pub mod domain;
35pub mod events;
36pub mod fetch;
37pub mod fs;
38pub mod fs_promises;
39pub mod http;
40pub mod http2;
41pub mod https;
42pub mod iterator;
43pub mod net;
44pub mod node_module;
45pub mod os;
46pub mod path;
47pub mod perf_hooks;
48pub mod process;
49pub mod punycode;
50pub mod querystring;
51pub mod readline;
52pub mod repl;
53pub mod stream;
54pub mod stream_consumers;
55pub mod stream_promises;
56pub mod stream_web;
57pub mod string_decoder;
58pub mod timers;
59pub mod tls;
60pub mod trace_events;
61pub mod tty;
62pub mod typedarray;
63pub mod url;
64pub mod url_legacy;
65pub mod util;
66pub mod util_types;
67pub mod v8;
68pub mod vm;
69pub mod worker_threads;
70pub mod zlib;
71
72/// Native-heavy core modules that node-js does not yet implement (TLS handshakes,
73/// HTTP/2 framing, OS worker threads sharing the thread-local heap, UDP sockets,
74/// V8 inspector, etc.). `require`ing them succeeds and yields a namespace so that
75/// programs which import-then-conditionally-use them still load; ACTUALLY calling
76/// a method throws `Error: <mod>.<method> is not implemented in node-js`. This is
77/// an honest not-yet-built surface, never a silent fake.
78pub const UNIMPLEMENTED_MODULES: &[&str] = &["inspector", "wasi"];
79
80/// True if `ns` is a known-but-unimplemented core module (see `UNIMPLEMENTED_MODULES`).
81pub fn is_unimplemented(ns: &str) -> bool {
82    UNIMPLEMENTED_MODULES.contains(&ns)
83}
84
85/// Canonical namespace name a `require(spec)` resolves to (after stripping an
86/// optional `node:` prefix), or `None` for an unsupported module.
87/// Core modules whose export is a plain DATA value rather than a namespace of
88/// methods. `require('constants')` is the only one: every member is a number,
89/// so it is built as a real object instead of a `Builtin` handle, whose members
90/// are dispatchable methods by construction.
91pub fn data_module(spec: &str) -> Option<Value> {
92    match spec.strip_prefix("node:").unwrap_or(spec) {
93        "constants" => Some(constants::object(&constants::flat())),
94        _ => None,
95    }
96}
97
98/// Whether `spec` names a core module of any kind — a method namespace or a
99/// data module. This is what `require.resolve` answers with the bare specifier.
100pub fn is_core(spec: &str) -> bool {
101    resolve(spec).is_some() || matches!(spec.strip_prefix("node:").unwrap_or(spec), "constants")
102}
103
104pub fn resolve(spec: &str) -> Option<&'static str> {
105    match spec.strip_prefix("node:").unwrap_or(spec) {
106        "fs" => Some("fs"),
107        "path" => Some("path"),
108        "os" => Some("os"),
109        "util" => Some("util"),
110        "assert" => Some("assert"),
111        "crypto" => Some("crypto"),
112        "buffer" => Some("buffer"),
113        "url" => Some("url"),
114        "process" => Some("process"),
115        "net" => Some("net"),
116        "http" => Some("http"),
117        // `require('stream')` IS the `Stream` constructor — `stream.Stream ===
118        // stream`, and libraries still subclass it the ES5 way with
119        // `Stream.call(this)` + `Object.create(Stream.prototype)`. It used to
120        // resolve to a plain namespace, so `typeof` was `object`, `.prototype`
121        // was `undefined`, and that subclassing pattern threw.
122        "stream" => Some("Stream"),
123        "tty" => Some("tty"),
124        // The `events` module's export IS the EventEmitter constructor, so
125        // `require('events')` yields the ctor namespace directly.
126        "events" => Some("EventEmitter"),
127        "string_decoder" => Some("string_decoder"),
128        "zlib" => Some("zlib"),
129        "querystring" => Some("querystring"),
130        "console" => Some("console"),
131        // `path/posix` is exactly our POSIX `path` (node-js targets a POSIX host,
132        // so `require('path') === path.posix`); `path/win32` is the separate
133        // backslash flavor. `assert/strict` is `assert` (already strict-based).
134        "path/posix" => Some("path"),
135        "path/win32" => Some("path/win32"),
136        // `sys` is the long-deprecated alias for `util`.
137        "sys" => Some("util"),
138        // `require('assert/strict')` IS the strict namespace, so its `equal`
139        // and `deepEqual` are the strict comparisons. Pointing it at the plain
140        // `assert` made `require('assert/strict').equal(1, '1')` pass. The
141        // strict namespace already existed for `assert.strict.*`.
142        "assert/strict" => Some("assertStrict"),
143        "child_process" => Some("child_process"),
144        "dns" => Some("dns"),
145        "punycode" => Some("punycode"),
146        "timers" => Some("timers"),
147        "timers/promises" => Some("timers/promises"),
148        "perf_hooks" => Some("perf_hooks"),
149        "async_hooks" => Some("async_hooks"),
150        "util/types" => Some("util/types"),
151        "diagnostics_channel" => Some("diagnostics_channel"),
152        "v8" => Some("v8"),
153        "readline" => Some("readline"),
154        "readline/promises" => Some("readline/promises"),
155        "vm" => Some("vm"),
156        "fs/promises" => Some("fs/promises"),
157        "dgram" => Some("dgram"),
158        "dns/promises" => Some("dns/promises"),
159        "worker_threads" => Some("worker_threads"),
160        "tls" => Some("tls"),
161        "https" => Some("https"),
162        "repl" => Some("repl"),
163        "cluster" => Some("cluster"),
164        "domain" => Some("domain"),
165        "http2" => Some("http2"),
166        "trace_events" => Some("trace_events"),
167        "module" => Some("module"),
168        "stream/consumers" => Some("stream/consumers"),
169        "stream/promises" => Some("stream/promises"),
170        "stream/web" => Some("stream/web"),
171        other => UNIMPLEMENTED_MODULES.iter().copied().find(|&m| m == other),
172    }
173}
174
175/// True if `qualified` (`namespace.method`) is a stdlib method that
176/// `call_builtin_function` should route into `call` (extends `is_known_builtin`).
177pub fn is_method(qualified: &str) -> bool {
178    let Some((ns, m)) = qualified.split_once('.') else {
179        return qualified == "assert";
180    };
181    // Any method on an unimplemented namespace routes to `call`, which throws an
182    // honest "not implemented" error (so `mod.foo()` fails clearly rather than
183    // silently returning undefined).
184    is_unimplemented(ns) || namespace_methods(ns).contains(&m) || namespace_ctors(ns).contains(&m)
185}
186
187/// The callable members of builtin namespace `ns`. THE single table backing both
188/// `is_method` (does `ns.m` dispatch?) and `namespace_keys` (what does `for (k in
189/// ns)` yield?), so a method can never be callable-but-unenumerable or the reverse.
190pub fn namespace_methods(ns: &str) -> &'static [&'static str] {
191    match ns {
192        "fs" => fs::METHODS,
193        "path" | "path/win32" => path::METHODS,
194        "os" => os::METHODS,
195        "util" => util::METHODS,
196        "assert" | "assertStrict" => assert::METHODS,
197        "crypto" => crypto::METHODS,
198        "webcrypto" => crypto::WEBCRYPTO_METHODS,
199        "SubtleCrypto" => crypto::SUBTLE_METHODS,
200        "Buffer" => buffer::STATIC_METHODS,
201        "buffer" => buffer::MODULE_METHODS,
202        "Date" => date::STATIC_METHODS,
203        "Response" => fetch::RESPONSE_STATICS,
204        "AbortSignal" => fetch::ABORT_SIGNAL_STATICS,
205        "Iterator" => iterator::STATIC_METHODS,
206        n if typedarray::is_ctor(n) => typedarray::static_methods(n),
207        "URL" => url::STATIC_METHODS,
208        "url" => url::MODULE_METHODS,
209        "net" => net::MODULE_METHODS,
210        "http" => http::MODULE_METHODS,
211        "stream" | "Stream" => stream::METHODS,
212        n if stream::is_class(n) => stream::STATIC_METHODS,
213        "worker_threads" => worker_threads::METHODS,
214        "zlib" => zlib::MODULE_METHODS,
215        "querystring" => querystring::METHODS,
216        "tty" => tty::METHODS,
217        "process" => process::METHODS,
218        "EventEmitter" => events::STATIC_METHODS,
219        "console" => console::METHODS,
220        "child_process" => child_process::METHODS,
221        "dns" => dns::METHODS,
222        "dns/promises" => dns::PROMISES_METHODS,
223        "punycode" => punycode::METHODS,
224        "timers" => timers::METHODS,
225        "timers/promises" => timers::PROMISES_METHODS,
226        "perf_hooks" | "performance" => perf_hooks::METHODS,
227        "async_hooks" => async_hooks::METHODS,
228        "AsyncResource" => async_hooks::RESOURCE_STATIC_METHODS,
229        "util/types" => util_types::METHODS,
230        "diagnostics_channel" => diagnostics_channel::METHODS,
231        "v8" => v8::METHODS,
232        "readline" => readline::METHODS,
233        "readline/promises" => readline::PROMISES_METHODS,
234        "vm" => vm::METHODS,
235        "fs/promises" => fs_promises::METHODS,
236        "dgram" => dgram::MODULE_METHODS,
237        "tls" => tls::MODULE_METHODS,
238        "https" => https::MODULE_METHODS,
239        "repl" => repl::METHODS,
240        "cluster" => cluster::METHODS,
241        "domain" => domain::METHODS,
242        "http2" => http2::METHODS,
243        "trace_events" => trace_events::METHODS,
244        "module" => node_module::METHODS,
245        "Module" => node_module::MODULE_STATIC_METHODS,
246        "stream/consumers" => stream_consumers::METHODS,
247        "stream/promises" => stream_promises::METHODS,
248        _ => &[],
249    }
250}
251
252/// Class/constructor members a namespace re-exports as values rather than
253/// callable methods (`require('buffer').Buffer`, `require('url').URL`). They are
254/// enumerable own keys too, so `for (k in buffer)` sees `Buffer`.
255pub fn namespace_ctors(ns: &str) -> &'static [&'static str] {
256    match ns {
257        "buffer" => &["Buffer", "Blob", "File"],
258        "stream" | "Stream" => stream::CLASSES,
259        "url" => &["URL", "URLSearchParams"],
260        "EventEmitter" => &["EventEmitter"],
261        "async_hooks" => &["AsyncLocalStorage", "AsyncResource"],
262        "string_decoder" => &["StringDecoder"],
263        "assert" => &["AssertionError"],
264        "console" => &["Console"],
265        "vm" => &["Script"],
266        "fs" => &["promises"],
267        // `stream/web` exports nothing BUT classes (`METHODS` is empty), so
268        // without this arm the namespace had no enumerable key at all: measured
269        // against node v26.7.0, `Object.keys(require('stream/web')).length` was
270        // 0 here and 18 there, even though every one of the classes resolved
271        // fine through `constant`. A namespace that answers property reads but
272        // enumerates empty breaks the copy-the-module pattern
273        // (`{ ...require('stream/web') }`, `for (k in web)`).
274        "stream/web" => stream_web::CLASSES,
275        _ => &[],
276    }
277}
278
279/// The enumerable own keys of the builtin namespace `ns` — what `for (key in ns)`
280/// and `Object.keys(ns)` yield. These are the members node-js ACTUALLY
281/// implements, not Node's full export list, so a package that copies a namespace
282/// key-by-key (safer-buffer clones `buffer` and `Buffer`) ends up with exactly the
283/// working set rather than an empty object.
284pub fn namespace_keys(ns: &str) -> Vec<String> {
285    // The `require.cache` view enumerates the resolved filenames it holds.
286    if ns == crate::builtins::REQUIRE_CACHE {
287        return crate::module::cache_keys();
288    }
289    let mut out: Vec<String> = namespace_ctors(ns).iter().map(|s| s.to_string()).collect();
290    for m in namespace_methods(ns) {
291        if !out.iter().any(|k| k == m) {
292            out.push((*m).to_string());
293        }
294    }
295    out
296}
297
298/// Dispatch a resolved stdlib builtin (`assert`, or `namespace.method`). Returns
299/// `None` if `name` is not a stdlib builtin (the caller falls through to the core
300/// builtin table).
301pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
302    if name == "assert" {
303        return Some(assert::assert_ok(args));
304    }
305    let (ns, m) = name.split_once('.')?;
306    Some(match ns {
307        "fs" => fs::call(m, args)?,
308        "path" => path::call(path::Flavor::Posix, m, args)?,
309        "path/win32" => path::call(path::Flavor::Win32, m, args)?,
310        "os" => os::call(m, args)?,
311        "util" => util::call(m, args)?,
312        "assert" => assert::call(m, args)?,
313        "assertStrict" => assert::strict_call(m, args)?,
314        "crypto" => crypto::call(m, args)?,
315        // `webcrypto`'s two random helpers are the same implementations the
316        // node-flavoured `crypto` module exposes.
317        "webcrypto" => crypto::call(m, args)?,
318        "SubtleCrypto" => crypto::subtle_call(m, args)?,
319        "Buffer" => buffer::static_call(m, args)?,
320        "buffer" if m == "Buffer" => Ok(with_host(|h| h.alloc(JsObj::Builtin("Buffer".into())))),
321        "buffer" => buffer::module_call(m, args)?,
322        "Date" => date::static_call(m, args)?,
323        "Response" | "AbortSignal" => fetch::static_call(ns, m, args)?,
324        "Iterator" => iterator::static_call(m, args)?,
325        n if typedarray::is_ctor(n) => typedarray::static_call(n, m, args)?,
326        "URL" => url::static_call(m, args)?,
327        "url" if m == "URL" => Ok(with_host(|h| h.alloc(JsObj::Builtin("URL".into())))),
328        "url" => url::call(m, args)?,
329        "net" => net::call(m, args)?,
330        "http" => http::call(m, args)?,
331        "stream" | "Stream" => stream::call(m, args)?,
332        "worker_threads" => worker_threads::call(m, args)?,
333        "zlib" => zlib::call(m, args)?,
334        "querystring" => querystring::call(m, args)?,
335        "tty" => tty::call(m, args)?,
336        "process" => process::call(m, args)?,
337        "EventEmitter" if m == "EventEmitter" => Ok(with_host(|h| {
338            h.alloc(JsObj::Builtin("EventEmitter".into()))
339        })),
340        "EventEmitter" => events::static_call(m, args)?,
341        n if stream::is_class(n) => stream::static_call(n, m, args)?,
342        "console" => console::call(m, args)?,
343        "child_process" => child_process::call(m, args)?,
344        "dns" => dns::call(m, args)?,
345        "punycode" => punycode::call(m, args)?,
346        "timers" => timers::call(m, args)?,
347        "timers/promises" => timers::promises_call(m, args)?,
348        "perf_hooks" | "performance" => perf_hooks::call(m, args)?,
349        "async_hooks" => async_hooks::call(m, args)?,
350        "AsyncResource" => async_hooks::static_call(m, args)?,
351        "util/types" => util_types::call(m, args)?,
352        "diagnostics_channel" => diagnostics_channel::call(m, args)?,
353        "v8" => v8::call(m, args)?,
354        "readline" => readline::call(m, args)?,
355        "readline/promises" => readline::promises_call(m, args)?,
356        "vm" => vm::call(m, args)?,
357        "fs/promises" => fs_promises::call(m, args)?,
358        "dgram" => dgram::call(m, args)?,
359        // dns/promises: getServers/setServers/get|setDefaultResultOrder are shared
360        // sync fns; every other method maps to dns's `promise<Cap>` variant.
361        "dns/promises" => match m {
362            "getServers" | "setServers" | "getDefaultResultOrder" | "setDefaultResultOrder" => {
363                dns::call(m, args)?
364            }
365            _ => {
366                let mut pm = String::from("promise");
367                let mut cs = m.chars();
368                if let Some(c) = cs.next() {
369                    pm.extend(c.to_uppercase());
370                    pm.push_str(cs.as_str());
371                }
372                dns::call(&pm, args)?
373            }
374        },
375        "tls" => tls::call(m, args)?,
376        "https" => https::call(m, args)?,
377        "repl" => repl::call(m, args)?,
378        "cluster" => cluster::call(m, args)?,
379        "domain" => domain::call(m, args)?,
380        "http2" => http2::call(m, args)?,
381        "trace_events" => trace_events::call(m, args)?,
382        "module" => node_module::call(m, args)?,
383        "Module" => node_module::static_call(m, args)?,
384        "stream/consumers" => stream_consumers::call(m, args)?,
385        "stream/promises" => stream_promises::call(m, args)?,
386        _ if is_unimplemented(ns) => Err(format!("Error: {ns}.{m} is not implemented in node-js")),
387        _ => return None,
388    })
389}
390
391/// A non-function constant on a stdlib namespace (`path.sep`, `os.EOL`,
392/// `buffer.Buffer`, `url.URL`), reachable via `namespace_property`.
393pub fn constant(ns: &str, name: &str) -> Option<Value> {
394    match ns {
395        // Both flavors carry `.posix`/`.win32` cross-links, exactly as Node's
396        // `posix.win32 = win32.win32 = win32; posix.posix = win32.posix = posix`.
397        "path" | "path/win32" if name == "posix" => {
398            Some(with_host(|h| h.alloc(JsObj::Builtin("path".into()))))
399        }
400        "path" | "path/win32" if name == "win32" => {
401            Some(with_host(|h| h.alloc(JsObj::Builtin("path/win32".into()))))
402        }
403        "path" => path::constant(path::Flavor::Posix, name),
404        "path/win32" => path::constant(path::Flavor::Win32, name),
405        "os" => os::constant(name),
406        // `fs.constants` and `crypto.constants` were absent, so
407        // `fs.constants.O_RDONLY` and `crypto.constants.RSA_PKCS1_PADDING` —
408        // which libraries pass straight through to `open` and to RSA — read as
409        // `undefined`.
410        "fs" | "fs/promises" if name == "constants" => Some(constants::object(&constants::fs())),
411        "crypto" if name == "constants" => Some(constants::object(&constants::crypto())),
412        // `crypto.webcrypto` is the WHATWG surface; `globalThis.crypto` is the
413        // same object. Only its two random helpers and `subtle.digest` are
414        // implemented — see `crypto::WEBCRYPTO_METHODS`.
415        "crypto" if name == "webcrypto" => {
416            Some(with_host(|h| h.alloc(JsObj::Builtin("webcrypto".into()))))
417        }
418        "webcrypto" if name == "subtle" => Some(with_host(|h| {
419            h.alloc(JsObj::Builtin("SubtleCrypto".into()))
420        })),
421        // `EventEmitter.defaultMaxListeners` is a DATA property, so it belongs
422        // here rather than among the static methods (which would make it read
423        // as a function). It was absent: node reports 10.
424        "EventEmitter" | "events" if name == "defaultMaxListeners" => Some(Value::Float(10.0)),
425        // `Buffer.poolSize` is a DATA property, not a method, so it belongs here
426        // rather than in `STATIC_METHODS` (which would make it read as a
427        // function). It was absent entirely: `Buffer.poolSize` was `undefined`
428        // where node v26.7.0 reports 65536. node-js allocates each Buffer on its
429        // own, so this is the documented constant, not a live allocator figure.
430        "Buffer" if name == "poolSize" => Some(Value::Float(65536.0)),
431        // `Uint8Array.BYTES_PER_ELEMENT` is a property of the CONSTRUCTOR as
432        // well as of every instance (23.2.6.2 / 23.2.5.1); only the instance
433        // carried it, so the constructor read `undefined` — the form the
434        // `byteLength = n * Ctor.BYTES_PER_ELEMENT` idiom uses.
435        n if typedarray::is_ctor(n) && name == "BYTES_PER_ELEMENT" => {
436            // `ArrayBuffer`/`DataView` are element-less and report nothing.
437            typedarray::ELEMENT_KINDS
438                .contains(&n)
439                .then(|| Value::Float(typedarray::bytes_per_element(n) as f64))
440        }
441        "buffer" if name == "Buffer" => {
442            Some(with_host(|h| h.alloc(JsObj::Builtin("Buffer".into()))))
443        }
444        "buffer" if matches!(name, "Blob" | "File") => {
445            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
446        }
447        "url" if name == "URL" => Some(with_host(|h| h.alloc(JsObj::Builtin("URL".into())))),
448        "net" => net::constant(name),
449        "tty" => tty::constant(name),
450        "repl" => repl::constant(name),
451        // `readline.promises` is the same module reached as a property, so the
452        // cross-link is checked before the shared constant table.
453        "readline" if name == "promises" => Some(with_host(|h| {
454            h.alloc(JsObj::Builtin("readline/promises".into()))
455        })),
456        "readline" | "readline/promises" => readline::constant(name),
457        "diagnostics_channel" => diagnostics_channel::constant(name),
458        "v8" => v8::constant(name),
459        "console" if name == "Console" => {
460            Some(with_host(|h| h.alloc(JsObj::Builtin("Console".into()))))
461        }
462        "assert" if name == "AssertionError" => Some(with_host(|h| {
463            h.alloc(JsObj::Builtin("AssertionError".into()))
464        })),
465        "assert" if name == "strict" => Some(with_host(|h| {
466            h.alloc(JsObj::Builtin("assertStrict".into()))
467        })),
468        // Every stream class's `.prototype` is the REAL prototype object the
469        // hierarchy hangs off, not a `Builtin("X.prototype")` handle — an ES5
470        // subclass reads it and passes it to `Object.create`, and
471        // `getPrototypeOf(Readable.prototype)` has to reach `Stream.prototype`.
472        n if stream::is_class(n) && name == "prototype" => with_host(|h| h.ensure_ctor_proto(n)),
473        "stream" | "Stream" => stream::constant(name),
474        "http" => http::constant(name),
475        "string_decoder" if name == "StringDecoder" => Some(with_host(|h| {
476            h.alloc(JsObj::Builtin("StringDecoder".into()))
477        })),
478        "process" => process::constant(name),
479        "EventEmitter" if name == "EventEmitter" => Some(with_host(|h| {
480            h.alloc(JsObj::Builtin("EventEmitter".into()))
481        })),
482        "perf_hooks" | "performance" => perf_hooks::constant(name),
483        "dns" => dns::constant(name),
484        "punycode" => punycode::constant(name),
485        "async_hooks" if matches!(name, "AsyncLocalStorage" | "AsyncResource") => {
486            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
487        }
488        "vm" if name == "Script" => Some(with_host(|h| h.alloc(JsObj::Builtin("Script".into())))),
489        "url" if name == "URLSearchParams" => Some(with_host(|h| {
490            h.alloc(JsObj::Builtin("URLSearchParams".into()))
491        })),
492        "fs" if name == "promises" => {
493            Some(with_host(|h| h.alloc(JsObj::Builtin("fs/promises".into()))))
494        }
495        "worker_threads" => worker_threads::constant(name),
496        "https" => https::constant(name),
497        "cluster" => cluster::constant(name),
498        "domain" => domain::constant(name),
499        "http2" => http2::constant(name),
500        "module" => node_module::constant(name),
501        "Module" => node_module::static_constant(name),
502        "stream/web" => stream_web::constant(name),
503        // util.types / util.TextEncoder|TextDecoder / util.MIMEType|MIMEParams.
504        "util" => util::constant(name),
505        // crypto class-constructor exports (require('crypto').Sign etc.) — the
506        // instances are made by factory fns, but the ctor names must resolve.
507        "crypto"
508            if matches!(
509                name,
510                "Sign"
511                    | "Verify"
512                    | "KeyObject"
513                    | "DiffieHellman"
514                    | "ECDH"
515                    | "X509Certificate"
516                    | "Hash"
517                    | "Hmac"
518                    | "Cipheriv"
519                    | "Decipheriv"
520            ) =>
521        {
522            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
523        }
524        _ => None,
525    }
526}
527
528/// Construct a stdlib class instance (`new URL(...)`, `new EventEmitter()`, and
529/// `new Buffer(...)` legacy), reachable from `construct_builtin`. `None` if `name`
530/// is not a stdlib constructor.
531pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
532    match name {
533        "URL" => Some(url::construct(args)),
534        "EventEmitter" => Some(Ok(events::new_emitter())),
535        // `new Buffer(x)` and the deprecated call form `Buffer(x)` are the same
536        // operation, and it is NOT simply `Buffer.from`: a NUMBER allocates that
537        // many zero bytes, where `Buffer.from(3)` is a TypeError in Node.
538        // Measured on node v26.7.0, `new Buffer(3)` and `Buffer(3)` are both
539        // `<Buffer 00 00 00>` (zero-filled since the `Buffer.alloc` semantics
540        // landed), while `new Buffer('ab')` and `new Buffer([1,2])` behave as
541        // `from`. Routing everything through `from` made `new Buffer(3)` one byte
542        // long.
543        "Buffer" => {
544            let numeric = matches!(args.first(), Some(Value::Int(_)) | Some(Value::Float(_)))
545                && args.len() == 1;
546            let m = if numeric { "alloc" } else { "from" };
547            Some(buffer::static_call(m, args).unwrap_or(Ok(Value::Undef)))
548        }
549        "Date" => Some(date::construct(args)),
550        "StringDecoder" => Some(string_decoder::construct(args)),
551        "WeakRef" => Some(typedarray::construct_weakref(args)),
552        "FinalizationRegistry" => Some(typedarray::construct_finalization_registry(args)),
553        n if fetch::is_class(n) => fetch::construct(n, args),
554        "TextEncoder" => Some(typedarray::construct_text_encoder()),
555        "TextDecoder" => Some(typedarray::construct_text_decoder(args)),
556        "DataView" => Some(typedarray::construct_dataview(args)),
557        n if typedarray::is_ctor(n) => Some(typedarray::construct(n, args)),
558        n if stream::is_class(n) => Some(Ok(stream::construct(n, args))),
559        "AsyncLocalStorage" | "AsyncResource" => async_hooks::construct(name, args),
560        "Script" => Some(vm::construct(args)),
561        "URLSearchParams" => Some(url::construct_search_params(args)),
562        "Worker" => Some(worker_threads::construct_worker(args)),
563        "Domain" => Some(domain::construct(args)),
564        "Tracing" => Some(trace_events::construct(args)),
565        "Blob" => Some(buffer::construct_blob(args)),
566        "File" => Some(buffer::construct_file(args)),
567        "AssertionError" => Some(Ok(assert::construct_assertion_error(args))),
568        "X509Certificate" => Some(crypto::construct_x509(args)),
569        "MIMEType" => Some(util::construct_mime_type(args)),
570        "MIMEParams" => Some(util::construct_mime_params(args)),
571        "Resolver" => Some(Ok(dns::construct_resolver(args))),
572        "ReadStream" | "WriteStream" => Some(Ok(tty::construct(name, args))),
573        "MessageChannel" => Some(worker_threads::construct_message_channel(args)),
574        "BroadcastChannel" => Some(worker_threads::construct_broadcast_channel(args)),
575        "PerformanceObserver" => Some(perf_hooks::construct(name, args)),
576        "REPLServer" | "Recoverable" => Some(repl::construct(name, args)),
577        "Interface" => Some(readline::construct(args)),
578        "Console" => Some(console::construct(args)),
579        "Serializer" | "DefaultSerializer" | "Deserializer" | "DefaultDeserializer" => {
580            Some(v8::construct(name, args))
581        }
582        // net/http constructors: their `construct` already returns Option<Result>.
583        "Socket" | "Stream" | "Server" | "SocketAddress" | "BlockList" => {
584            net::construct(name, args)
585        }
586        "Agent" | "http.Server" => http::construct(name, args),
587        // stream/web WHATWG classes (its `construct` returns Option<Result>).
588        n if stream_web::is_class(n) => stream_web::construct(n, args),
589        _ => None,
590    }
591}
592
593/// The hidden `@@native` instance tag of `recv` (`"Buffer"`/`"Hash"`/
594/// `"EventEmitter"`/`"URL"`), or `None` for a non-native object.
595pub fn native_tag(recv: &Value) -> Option<String> {
596    with_host(|h| match h.get(recv) {
597        Some(JsObj::Object(p)) => p.get("@@native").map(|v| h.str_of(v)),
598        _ => None,
599    })
600}
601
602/// Native instance tags whose `instance_call` implements `toJSON()`, which
603/// `JSON.stringify` must invoke before serializing the value. (`instance_has_method`
604/// only covers tags with a declared method table; `Date` dispatches directly.)
605pub fn has_to_json(tag: &str) -> bool {
606    matches!(tag, "Buffer" | "Date" | "URL" | "MIMEType" | "MIMEParams")
607}
608
609/// Whether `name` is a method of a native instance tagged `tag`. Used by
610/// `get_property` so a method *read* (`server.listen.apply(...)`, the express
611/// listen path) yields a bound method rather than `undefined` — the method is
612/// still dispatched through `instance_call` when the bound method is invoked.
613pub fn instance_has_method(tag: &str, name: &str) -> bool {
614    // A class INHERITS its parent's methods: `equals` is on `KeyObject` and a
615    // `SecretKeyObject` answers it. Asking only the leaf's own list reported
616    // `false` for a method the prototype chain really carries.
617    let mut ctor = Some(tag);
618    while let Some(t) = ctor {
619        let (base, emitter) = instance_method_lists(t);
620        if base.contains(&name) || emitter.contains(&name) {
621            return true;
622        }
623        ctor = native_parent(t);
624    }
625    false
626}
627
628/// The method names a native instance tagged `tag` carries, as
629/// `(its own list, the EventEmitter surface it also gets or empty)`.
630///
631/// Split out of [`instance_has_method`] so the same table can be *enumerated*,
632/// not only queried: `host::ensure_ctor_proto` builds a native constructor's
633/// real `.prototype` object from it. A predicate alone would have forced a
634/// second, hand-maintained list of the same names — the drift that put
635/// `listeners` on nine dispatchers and not on the three that run.
636/// The native class whose prototype `ctor`'s prototype inherits from, if any.
637///
638/// Only the stream hierarchy has one: node's is
639/// `Readable/Writable/Duplex/Transform/PassThrough → Stream → EventEmitter`,
640/// and every prototype used to hang straight off `Object.prototype` instead, so
641/// `new Readable() instanceof Stream` and `instanceof EventEmitter` both read
642/// false and an ES5 subclass built on `Object.create(Stream.prototype)`
643/// inherited nothing.
644pub fn native_parent(ctor: &str) -> Option<&'static str> {
645    match ctor {
646        "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" => Some("Stream"),
647        "Stream" => Some("EventEmitter"),
648        // `crypto.createSecretKey` and `generateKeyPair` hand back the LEAF
649        // classes, not `KeyObject`: node's chain is
650        // `SecretKeyObject → KeyObject` and
651        // `PublicKeyObject → AsymmetricKeyObject → KeyObject`, which is why
652        // `symmetricKeySize` exists on one and `asymmetricKeyType` on the other.
653        // Everything was tagged `KeyObject`, so `key.constructor.name` read
654        // `KeyObject` and neither accessor had a home.
655        "SecretKeyObject" | "AsymmetricKeyObject" => Some("KeyObject"),
656        "PublicKeyObject" | "PrivateKeyObject" => Some("AsymmetricKeyObject"),
657        _ => None,
658    }
659}
660
661/// The ACCESSOR properties a native class's prototype carries, as
662/// `(name, has_setter)`, and the `Symbol.toStringTag` it stamps (empty for
663/// none).
664///
665/// Node keeps these OFF the instance: `Object.keys(new AbortController())` is
666/// empty and `JSON.stringify` of one is `{}`, because `signal` is a getter on
667/// `AbortController.prototype`. Storing them as own data properties — what this
668/// did — leaked them into every enumeration, every spread and every
669/// serialization of an object that merely held one.
670///
671/// The getter reads the instance's hidden `@@<name>` slot, so a construction
672/// site stores the value there rather than under the public name.
673/// Run whatever a class has to do after one of its accessor SETTERS stored a
674/// value — for a `URL`, rewriting the fields derived from the one just written.
675///
676/// Without this the setter was a plain slot write: `u.protocol = 'https:'` read
677/// back as `https:` while `u.href` still showed `http://…`, so the object
678/// disagreed with itself.
679/// Whether a class's prototype MEMBERS are enumerable, as node defines them.
680///
681/// Most are: `for (const k in new URL('http://a/'))` walks `href`, `origin` and
682/// the rest, because a WebIDL interface's members are enumerable and node
683/// defines its own classes the same way. The exceptions are the ones written as
684/// ES classes — `vm.Script` and the `KeyObject` family — whose methods are
685/// non-enumerable like any class method's.
686///
687/// `constructor` is never enumerable, in either kind.
688/// Methods a class defines AFTER its accessors.
689///
690/// Prototype members enumerate in definition order, and node defines
691/// `URL.prototype` as `toString`, then every accessor, then `toJSON` — so a
692/// `for-in` over a URL ends with `toJSON` rather than starting with it.
693pub fn instance_late_methods(tag: &str) -> &'static [&'static str] {
694    match tag {
695        "URL" => &["toJSON"],
696        _ => &[],
697    }
698}
699
700pub fn instance_members_enumerable(tag: &str) -> bool {
701    !matches!(
702        tag,
703        "Script"
704            | "KeyObject"
705            | "SecretKeyObject"
706            | "AsymmetricKeyObject"
707            | "PublicKeyObject"
708            | "PrivateKeyObject"
709    )
710}
711
712pub fn instance_accessor_written(tag: &str, key: &str, recv: &Value) {
713    if tag == "URL" {
714        // `href` is the whole URL, not a field of it, and `host` is two fields.
715        match key {
716            "href" => url::reparse(recv),
717            "host" => url::split_host(recv),
718            _ => url::refresh(recv),
719        }
720    }
721}
722
723pub fn instance_accessors(tag: &str) -> (&'static [(&'static str, bool)], &'static str) {
724    match tag {
725        "AbortController" => (&[("signal", false)], "AbortController"),
726        "AbortSignal" => (
727            &[("aborted", false), ("reason", false), ("onabort", true)],
728            "AbortSignal",
729        ),
730        "KeyObject" => (&[("type", false)], "KeyObject"),
731        "SecretKeyObject" => (&[("symmetricKeySize", false)], ""),
732        "AsymmetricKeyObject" => (
733            &[
734                ("asymmetricKeyType", false),
735                ("asymmetricKeyDetails", false),
736            ],
737            "",
738        ),
739        "TextEncoder" => (&[("encoding", false)], "TextEncoder"),
740        // `origin` and `searchParams` are the two a caller cannot assign.
741        "URL" => (
742            &[
743                ("href", true),
744                ("origin", false),
745                ("protocol", true),
746                ("username", true),
747                ("password", true),
748                ("host", true),
749                ("hostname", true),
750                ("port", true),
751                ("pathname", true),
752                ("search", true),
753                ("searchParams", false),
754                ("hash", true),
755            ],
756            "URL",
757        ),
758        "TextDecoder" => (
759            &[("encoding", false), ("fatal", false), ("ignoreBOM", false)],
760            "TextDecoder",
761        ),
762        _ => (&[], ""),
763    }
764}
765
766pub fn instance_method_lists(tag: &str) -> (&'static [&'static str], &'static [&'static str]) {
767    // Shared EventEmitter surface for the emitter-backed instances. Read from
768    // `events::METHODS` so what an instance ADVERTISES here can never drift from
769    // what the dispatchers actually delegate.
770    const EMITTER: &[&str] = events::METHODS;
771    let base: &[&str] = match tag {
772        "Timeout" => timers::TIMEOUT_METHODS,
773        "IntervalIterator" => timers::INTERVAL_METHODS,
774        "CollectionIterator" => &["next", "@@iterator"],
775        "IteratorHelper" => iterator::METHODS,
776        "Immediate" => timers::IMMEDIATE_METHODS,
777        "Server" => &["listen", "close", "address"],
778        "Socket" => &[
779            "write",
780            "end",
781            "destroy",
782            "pause",
783            "resume",
784            "setEncoding",
785            "setKeepAlive",
786            "setNoDelay",
787            "setTimeout",
788            "ref",
789            "unref",
790            "connect",
791        ],
792        "ServerResponse" => &[
793            "writeHead",
794            "setHeader",
795            "getHeader",
796            "getHeaderNames",
797            "getHeaders",
798            "hasHeader",
799            "removeHeader",
800            "write",
801            "end",
802            "flushHeaders",
803        ],
804        "IncomingMessage" => &["pause", "resume", "setEncoding", "destroy"],
805        "Buffer" => buffer::INSTANCE_METHODS,
806        "DataView" => typedarray::DATAVIEW_METHODS,
807        "ArrayBuffer" => &["slice", "resize", "transfer", "transferToFixedLength"],
808        "Date" => date::INSTANCE_METHODS,
809        "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" | "Stream" => &[
810            "read",
811            "write",
812            "end",
813            "pipe",
814            "pause",
815            "resume",
816            "setEncoding",
817            "destroy",
818            "push",
819        ],
820        // `toJSON` comes AFTER the accessors — see `instance_late_methods`.
821        "URL" => &["toString"],
822        "AsyncLocalStorage" => async_hooks::ALS_METHODS,
823        "AsyncHook" => async_hooks::HOOK_METHODS,
824        "AsyncResource" => async_hooks::RESOURCE_METHODS,
825        "Channel" => &["subscribe", "unsubscribe", "publish"],
826        "WriteStream" => &[
827            "write",
828            "end",
829            "on",
830            "once",
831            "removeListener",
832            "cork",
833            "uncork",
834            "setEncoding",
835        ],
836        // `Hash` and `Hmac` answer the same two methods (both route to
837        // `crypto::hashlike_call`). Only `Hmac` was listed, so
838        // `ensure_ctor_proto("Hash")` found nothing and `crypto.Hash.prototype`
839        // read `undefined` — the ES5-subclassing hole this table exists to
840        // close, still open for one of the two constructors it documents.
841        // `copy` is Hash-only — an Hmac cannot be forked.
842        "Hash" => &["update", "digest", "copy"],
843        "Hmac" => &["update", "digest"],
844        "StringDecoder" => string_decoder::INSTANCE_METHODS,
845        "Interface" => readline::INTERFACE_METHODS,
846        "Script" => vm::SCRIPT_METHODS,
847        "URLSearchParams" => url::SEARCH_PARAMS_METHODS,
848        "UdpSocket" => dgram::SOCKET_METHODS,
849        "Worker" => worker_threads::WORKER_METHODS,
850        "MessagePort" => worker_threads::PORT_METHODS,
851        "TLSServer" => tls::SERVER_METHODS,
852        "TLSSocket" => tls::SOCKET_METHODS,
853        "HTTPSServerResponse" => https::RESPONSE_METHODS,
854        "HTTPSClientRequest" => https::CLIENT_REQUEST_METHODS,
855        "REPLServer" => repl::REPLSERVER_METHODS,
856        "ClusterWorker" => cluster::WORKER_METHODS,
857        "Domain" => domain::DOMAIN_METHODS,
858        "Tracing" => trace_events::TRACING_METHODS,
859        "Http2Server" => http2::SERVER_METHODS,
860        "Http2Stream" => http2::STREAM_METHODS,
861        "Http2Session" => http2::SESSION_METHODS,
862        "Cipheriv" | "Decipheriv" => &["update", "final", "setAutoPadding"],
863        "BlockList" => net::BLOCKLIST_METHODS,
864        "ClientRequest" => http::CLIENT_REQUEST_METHODS,
865        "Agent" => &["destroy", "getName"],
866        "Blob" | "File" => buffer::BLOB_METHODS,
867        "ReadStream" => tty::READ_STREAM_METHODS,
868        "Dirent" => fs::DIRENT_METHODS,
869        "Dir" => fs::DIR_METHODS,
870        "FSReadStream" => fs::READ_STREAM_METHODS,
871        "FSWriteStream" => fs::WRITE_STREAM_METHODS,
872        "Resolver" => dns::RESOLVER_METHODS,
873        "Histogram" => perf_hooks::HISTOGRAM_METHODS,
874        "PerformanceObserver" => perf_hooks::PERFORMANCE_OBSERVER_METHODS,
875        "PerformanceObserverEntryList" => perf_hooks::OBSERVER_ENTRY_LIST_METHODS,
876        "BroadcastChannel" => worker_threads::BROADCAST_CHANNEL_METHODS,
877        "TracingChannel" => diagnostics_channel::TRACING_CHANNEL_METHODS,
878        "Serializer" => v8::SERIALIZER_METHODS,
879        "Deserializer" => v8::DESERIALIZER_METHODS,
880        "Console" => console::CONSOLE_METHODS,
881        "ChildProcess" => child_process::CHILD_PROCESS_METHODS,
882        "Sign" => &["update", "sign"],
883        "Verify" => &["update", "verify"],
884        "KeyObject" => &["equals", "toCryptoKey"],
885        // `export` is on each LEAF, since what it exports differs.
886        "SecretKeyObject" | "PublicKeyObject" | "PrivateKeyObject" => &["export"],
887        "DiffieHellman" => &[
888            "generateKeys",
889            "computeSecret",
890            "getPrime",
891            "getGenerator",
892            "getPublicKey",
893            "getPrivateKey",
894            "setPublicKey",
895            "setPrivateKey",
896        ],
897        "ECDH" => &[
898            "generateKeys",
899            "computeSecret",
900            "getPublicKey",
901            "getPrivateKey",
902            "setPrivateKey",
903        ],
904        "X509Certificate" => &["toString"],
905        "FinalizationRegistry" => &["register", "unregister"],
906        "MIMEType" => util::MIME_TYPE_METHODS,
907        "MIMEParams" => util::MIME_PARAMS_METHODS,
908        t if fetch::is_class(t) => fetch::methods_for(t),
909        t if stream_web::is_class(t) => stream_web::methods_for(t),
910        _ => &[],
911    };
912    let is_emitter = matches!(
913        tag,
914        "Server"
915            | "Socket"
916            | "ServerResponse"
917            | "IncomingMessage"
918            | "EventEmitter"
919            | "Readable"
920            | "Writable"
921            | "Duplex"
922            | "Transform"
923            | "PassThrough"
924            | "Stream"
925            | "UdpSocket"
926            | "Worker"
927            | "MessagePort"
928            | "TLSServer"
929            | "TLSSocket"
930            | "HTTPSServerResponse"
931            | "HTTPSClientRequest"
932            | "ClusterWorker"
933            | "Domain"
934            | "Http2Server"
935            | "Http2Stream"
936            | "Http2Session"
937            | "ClientRequest"
938            | "FSReadStream"
939            | "FSWriteStream"
940            | "ChildProcess"
941    );
942    (base, if is_emitter { EMITTER } else { &[] })
943}
944
945/// Dispatch a method call on a native stdlib instance (`recv` carries a
946/// `@@native` tag). Called from `host::call_method` before the generic object
947/// method resolution.
948pub fn instance_call(
949    tag: &str,
950    recv: &Value,
951    method: &str,
952    args: Vec<Value>,
953) -> Result<Value, String> {
954    match tag {
955        "Buffer" => buffer::instance_call(recv, method, &args),
956        "Timeout" | "Immediate" => timers::instance_call(recv, method, &args),
957        "IntervalIterator" => timers::interval_call(recv, method, &args),
958        "CollectionIterator" => match method {
959            "next" => crate::builtins::collection_iterator_next(recv),
960            "@@iterator" => Ok(recv.clone()),
961            m if iterator::is_helper(m) => iterator::call(recv, m, &args),
962            _ => Err(crate::host::type_error(&format!(
963                "mapIterator.{method} is not a function"
964            ))),
965        },
966        "IteratorHelper" => match method {
967            "next" => iterator::helper_next(recv),
968            "@@iterator" => Ok(recv.clone()),
969            // Abandoning a helper marks it exhausted AND closes its source, so
970            // a later `next` is a done step and the generator underneath runs
971            // its `finally`.
972            "return" => Ok(iterator::helper_return(recv)),
973            m if iterator::is_helper(m) => iterator::call(recv, m, &args),
974            _ => Err(crate::host::type_error(&format!(
975                "{method} is not a function"
976            ))),
977        },
978        "Date" => date::instance_call(recv, method, &args),
979        "StringDecoder" => string_decoder::instance_call(recv, method, &args),
980        "WeakRef" => typedarray::weakref_call(recv, method),
981        "FinalizationRegistry" => typedarray::finalization_registry_call(recv, method, &args),
982        "TextEncoder" => typedarray::text_encoder_call(recv, method, &args),
983        "TextDecoder" => typedarray::text_decoder_call(recv, method, &args),
984        "TypedArray" => typedarray::instance_call(recv, method, &args),
985        "DataView" => typedarray::dataview_call(recv, method, &args),
986        // An `ArrayBuffer`'s only instance method is `slice`, which copies the
987        // byte range into a fresh buffer.
988        "ArrayBuffer" if method == "slice" => {
989            if typedarray::is_detached(recv) {
990                return Err(typedarray::detached_error(
991                    "ArrayBuffer.prototype",
992                    "slice",
993                    true,
994                ));
995            }
996            Ok(typedarray::buffer_slice(recv, &args))
997        }
998        "ArrayBuffer" if method == "resize" => typedarray::buffer_resize(recv, &args),
999        "ArrayBuffer" if method == "transfer" => typedarray::buffer_transfer(recv, &args, false),
1000        "ArrayBuffer" if method == "transferToFixedLength" => {
1001            typedarray::buffer_transfer(recv, &args, true)
1002        }
1003        t if fetch::is_class(t) => fetch::instance_call(t, recv, method, &args),
1004        "Hash" => crypto::instance_call(recv, method, &args),
1005        "Hmac" => crypto::hmac_instance_call(recv, method, &args),
1006        "Interface" => readline::instance_call(recv, method, args),
1007        "Script" => vm::instance_call(recv, method, args),
1008        "URLSearchParams" => url::search_params_call(recv, method, &args),
1009        "UdpSocket" => dgram::instance_call(recv, method, args),
1010        "Worker" | "MessagePort" | "BroadcastChannel" => {
1011            worker_threads::instance_call(tag, recv, method, args)
1012        }
1013        "TLSServer" | "TLSSocket" => tls::instance_call(tag, recv, method, args),
1014        "HTTPSServerResponse" | "HTTPSClientRequest" => {
1015            https::instance_call(tag, recv, method, args)
1016        }
1017        "REPLServer" => repl::instance_call(recv, method, args),
1018        "ClusterWorker" => cluster::instance_call(recv, method, args),
1019        "Domain" => domain::instance_call(recv, method, args),
1020        "Tracing" => trace_events::instance_call(recv, method, args),
1021        "Http2Server" | "Http2Stream" | "Http2Session" => {
1022            http2::instance_call(tag, recv, method, args)
1023        }
1024        "EventEmitter" => events::instance_call(recv, method, args),
1025        "URL" => url::instance_call(recv, method, &args),
1026        "Stats" => fs::stats_call(recv, method),
1027        "Dirent" => fs::dirent_call(recv, method),
1028        "Dir" => fs::dir_call(recv, method, args),
1029        "FSReadStream" => fs::read_stream_call(recv, method, args),
1030        "FSWriteStream" => fs::write_stream_call(recv, method, args),
1031        "Server" | "Socket" | "BlockList" => net::instance_call(tag, recv, method, args),
1032        "IncomingMessage" | "ServerResponse" | "ClientRequest" | "Agent" => {
1033            http::instance_call(tag, recv, method, args)
1034        }
1035        "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" | "Stream" => {
1036            stream::instance_call(tag, recv, method, args)
1037        }
1038        "Cipheriv" | "Decipheriv" => crypto::cipher_instance_call(tag, recv, method, &args),
1039        "Sign" | "Verify" => crypto::sign_verify_instance_call(tag, recv, method, &args),
1040        "KeyObject"
1041        | "SecretKeyObject"
1042        | "AsymmetricKeyObject"
1043        | "PublicKeyObject"
1044        | "PrivateKeyObject" => crypto::key_object_instance_call(recv, method, &args),
1045        "DiffieHellman" => crypto::dh_instance_call(recv, method, &args),
1046        "ECDH" => crypto::ecdh_instance_call(recv, method, &args),
1047        "X509Certificate" => crypto::x509_instance_call(recv, method, &args),
1048        "MIMEType" => util::mime_type_instance_call(recv, method, &args),
1049        "MIMEParams" => util::mime_params_instance_call(recv, method, &args),
1050        "Blob" | "File" => buffer::blob_call(recv, method, &args),
1051        "ReadStream" => tty::instance_call(recv, method, &args),
1052        "Resolver" => dns::resolver_instance_call(recv, method, args),
1053        "Histogram" => perf_hooks::histogram_instance_call(recv, method, &args),
1054        "PerformanceObserver" => perf_hooks::observer_instance_call(recv, method, &args),
1055        "PerformanceObserverEntryList" => perf_hooks::entry_list_instance_call(recv, method, &args),
1056        "TracingChannel" => diagnostics_channel::tracing_instance_call(recv, method, &args),
1057        "Serializer" | "Deserializer" => v8::instance_call(tag, recv, method, args),
1058        "Console" => console::instance_call(recv, method, args),
1059        "ChildProcess" => child_process::instance_call(recv, method, args),
1060        t if stream_web::is_class(t) => stream_web::instance_call(t, recv, method, args),
1061        "AsyncLocalStorage" | "AsyncHook" | "AsyncResource" => {
1062            async_hooks::instance_call(tag, recv, method, args)
1063        }
1064        "Channel" => diagnostics_channel::instance_call(recv, method, &args),
1065        "WriteStream" => process::stream_instance_call(recv, method, &args),
1066        _ => Err(crate::host::type_error(&format!(
1067            "{method} is not a function"
1068        ))),
1069    }
1070}
1071
1072// ── shared helpers ──────────────────────────────────────────────────────────
1073
1074/// The `Received …` tail Node appends to an `ERR_INVALID_ARG_TYPE` message
1075/// (`internal/errors.js` `determineSpecificType`): `null`/`undefined` verbatim,
1076/// a primitive as `type <typeof> (<inspected>)`, a function as
1077/// `function <name>`, an object as `an instance of <Ctor>`.
1078pub(crate) fn received_desc(v: &Value) -> String {
1079    with_host(|h| {
1080        if matches!(v, Value::Undef) {
1081            return "undefined".to_string();
1082        }
1083        if h.is_null(v) {
1084            return "null".to_string();
1085        }
1086        let ty = h.type_of(v);
1087        // A callable is named by its own `.name`, never by its constructor:
1088        // `determineSpecificType` reports `function foo` (and `function ` for an
1089        // anonymous one), where this used to say `an instance of Object`.
1090        if ty == "function" {
1091            return format!("function {}", h.callable_name(v));
1092        }
1093        if ty == "object" {
1094            // `ctor_name` is empty for the builtin shapes (they carry no user
1095            // class), so fall back to the intrinsic constructor name.
1096            let name = match h.ctor_name(v) {
1097                n if !n.is_empty() => n,
1098                _ => match h.get(v) {
1099                    Some(JsObj::Array(_)) => "Array".into(),
1100                    Some(JsObj::Map { .. }) => "Map".into(),
1101                    Some(JsObj::Set { .. }) => "Set".into(),
1102                    Some(JsObj::Promise { .. }) => "Promise".into(),
1103                    Some(JsObj::RegExp(_)) => "RegExp".into(),
1104                    Some(JsObj::Object(p)) => match p.get("@@native") {
1105                        Some(t) => h.str_of(t),
1106                        None => "Object".into(),
1107                    },
1108                    _ => "Object".into(),
1109                },
1110            };
1111            return format!("an instance of {name}");
1112        }
1113        let shown = match ty {
1114            "string" => format!("'{}'", h.str_of(v)),
1115            "bigint" => format!("{}n", h.str_of(v)),
1116            "number" if matches!(v, Value::Float(f) if *f == 0.0 && f.is_sign_negative()) => {
1117                "-0".to_string()
1118            }
1119            _ => h.str_of(v),
1120        };
1121        format!("type {ty} ({shown})")
1122    })
1123}
1124
1125/// ToString of `args[i]` (empty string if absent).
1126pub(crate) fn arg_str(args: &[Value], i: usize) -> String {
1127    with_host(|h| args.get(i).map(|v| h.str_of(v)).unwrap_or_default())
1128}
1129
1130/// ToNumber of `args[i]` (`NaN` if absent).
1131pub(crate) fn arg_num(args: &[Value], i: usize) -> f64 {
1132    with_host(|h| args.get(i).map(|v| h.to_number(v)).unwrap_or(f64::NAN))
1133}
1134
1135/// Lowercase hex encoding of `bytes`.
1136pub(crate) fn to_hex(bytes: &[u8]) -> String {
1137    let mut s = String::with_capacity(bytes.len() * 2);
1138    for b in bytes {
1139        s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
1140        s.push(char::from_digit((b & 0xf) as u32, 16).unwrap());
1141    }
1142    s
1143}
1144
1145/// Decode a hex string to bytes (ignoring a trailing odd nibble, like Node).
1146pub(crate) fn from_hex(s: &str) -> Vec<u8> {
1147    let digits: Vec<u8> = s
1148        .bytes()
1149        .filter_map(|c| (c as char).to_digit(16).map(|d| d as u8))
1150        .collect();
1151    digits
1152        .chunks(2)
1153        .filter(|c| c.len() == 2)
1154        .map(|c| (c[0] << 4) | c[1])
1155        .collect()
1156}
1157
1158const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1159
1160/// Standard base64 encoding (with `=` padding) of `bytes`.
1161pub(crate) fn to_base64(bytes: &[u8]) -> String {
1162    let mut out = String::new();
1163    for chunk in bytes.chunks(3) {
1164        let b = [
1165            chunk[0],
1166            *chunk.get(1).unwrap_or(&0),
1167            *chunk.get(2).unwrap_or(&0),
1168        ];
1169        let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | (b[2] as u32);
1170        out.push(B64[((n >> 18) & 63) as usize] as char);
1171        out.push(B64[((n >> 12) & 63) as usize] as char);
1172        out.push(if chunk.len() > 1 {
1173            B64[((n >> 6) & 63) as usize] as char
1174        } else {
1175            '='
1176        });
1177        out.push(if chunk.len() > 2 {
1178            B64[(n & 63) as usize] as char
1179        } else {
1180            '='
1181        });
1182    }
1183    out
1184}
1185
1186/// URL-safe base64 (RFC 4648 §5) of `bytes`: `+/` become `-_` and the `=`
1187/// padding is dropped. This is `buf.toString('base64url')`, which is a distinct
1188/// encoding from `'base64'` — not an alias. Node emits `Buffer.from([251,255,
1189/// 190,1]).toString('base64url')` as `-_--AQ` where `'base64'` gives `+/++AQ==`.
1190pub(crate) fn to_base64url(bytes: &[u8]) -> String {
1191    to_base64(bytes)
1192        .chars()
1193        .filter(|c| *c != '=')
1194        .map(|c| match c {
1195            '+' => '-',
1196            '/' => '_',
1197            c => c,
1198        })
1199        .collect()
1200}
1201
1202/// Decode a base64 string to bytes (ignores whitespace and padding).
1203///
1204/// BOTH alphabets are accepted, in either direction: node decodes `-_` under
1205/// `'base64'` and `+/` under `'base64url'` (measured — `Buffer.from('-_-_',
1206/// 'base64').toString('hex')` and `Buffer.from('+/+/','base64url')
1207/// .toString('hex')` are both `fbffbf` on v26.7.0), so the decoder does not need
1208/// to know which name it was reached by. Refusing the URL-safe characters here
1209/// silently produced an EMPTY buffer, because an unrecognized character is
1210/// dropped rather than rejected.
1211pub(crate) fn from_base64(s: &str) -> Vec<u8> {
1212    let rev = |c: u8| -> Option<u32> {
1213        let c = match c {
1214            b'-' => b'+',
1215            b'_' => b'/',
1216            c => c,
1217        };
1218        B64.iter().position(|&x| x == c).map(|p| p as u32)
1219    };
1220    let vals: Vec<u32> = s.bytes().filter_map(rev).collect();
1221    let mut out = Vec::new();
1222    for chunk in vals.chunks(4) {
1223        if chunk.len() < 2 {
1224            break;
1225        }
1226        let n = (chunk[0] << 18)
1227            | (chunk[1] << 12)
1228            | (chunk.get(2).copied().unwrap_or(0) << 6)
1229            | chunk.get(3).copied().unwrap_or(0);
1230        out.push((n >> 16) as u8);
1231        if chunk.len() > 2 {
1232            out.push((n >> 8) as u8);
1233        }
1234        if chunk.len() > 3 {
1235            out.push(n as u8);
1236        }
1237    }
1238    out
1239}