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 async_hooks;
23pub mod buffer;
24pub mod child_process;
25pub mod cluster;
26pub mod console;
27pub mod crypto;
28pub mod date;
29pub mod dgram;
30pub mod diagnostics_channel;
31pub mod dns;
32pub mod domain;
33pub mod events;
34pub mod fetch;
35pub mod fs;
36pub mod fs_promises;
37pub mod http;
38pub mod http2;
39pub mod https;
40pub mod net;
41pub mod node_module;
42pub mod os;
43pub mod path;
44pub mod perf_hooks;
45pub mod process;
46pub mod punycode;
47pub mod querystring;
48pub mod readline;
49pub mod repl;
50pub mod stream;
51pub mod stream_consumers;
52pub mod stream_promises;
53pub mod stream_web;
54pub mod string_decoder;
55pub mod timers;
56pub mod tls;
57pub mod trace_events;
58pub mod tty;
59pub mod typedarray;
60pub mod url;
61pub mod url_legacy;
62pub mod util;
63pub mod util_types;
64pub mod v8;
65pub mod vm;
66pub mod worker_threads;
67pub mod zlib;
68
69/// Native-heavy core modules that node-js does not yet implement (TLS handshakes,
70/// HTTP/2 framing, OS worker threads sharing the thread-local heap, UDP sockets,
71/// V8 inspector, etc.). `require`ing them succeeds and yields a namespace so that
72/// programs which import-then-conditionally-use them still load; ACTUALLY calling
73/// a method throws `Error: <mod>.<method> is not implemented in node-js`. This is
74/// an honest not-yet-built surface, never a silent fake.
75pub const UNIMPLEMENTED_MODULES: &[&str] = &["inspector", "wasi"];
76
77/// True if `ns` is a known-but-unimplemented core module (see `UNIMPLEMENTED_MODULES`).
78pub fn is_unimplemented(ns: &str) -> bool {
79    UNIMPLEMENTED_MODULES.contains(&ns)
80}
81
82/// Canonical namespace name a `require(spec)` resolves to (after stripping an
83/// optional `node:` prefix), or `None` for an unsupported module.
84pub fn resolve(spec: &str) -> Option<&'static str> {
85    match spec.strip_prefix("node:").unwrap_or(spec) {
86        "fs" => Some("fs"),
87        "path" => Some("path"),
88        "os" => Some("os"),
89        "util" => Some("util"),
90        "assert" => Some("assert"),
91        "crypto" => Some("crypto"),
92        "buffer" => Some("buffer"),
93        "url" => Some("url"),
94        "process" => Some("process"),
95        "net" => Some("net"),
96        "http" => Some("http"),
97        "stream" => Some("stream"),
98        "tty" => Some("tty"),
99        // The `events` module's export IS the EventEmitter constructor, so
100        // `require('events')` yields the ctor namespace directly.
101        "events" => Some("EventEmitter"),
102        "string_decoder" => Some("string_decoder"),
103        "zlib" => Some("zlib"),
104        "querystring" => Some("querystring"),
105        "console" => Some("console"),
106        // `path/posix` is exactly our POSIX `path` (node-js targets a POSIX host,
107        // so `require('path') === path.posix`); `path/win32` is the separate
108        // backslash flavor. `assert/strict` is `assert` (already strict-based).
109        "path/posix" => Some("path"),
110        "path/win32" => Some("path/win32"),
111        // `sys` is the long-deprecated alias for `util`.
112        "sys" => Some("util"),
113        "assert/strict" => Some("assert"),
114        "child_process" => Some("child_process"),
115        "dns" => Some("dns"),
116        "punycode" => Some("punycode"),
117        "timers" => Some("timers"),
118        "timers/promises" => Some("timers/promises"),
119        "perf_hooks" => Some("perf_hooks"),
120        "async_hooks" => Some("async_hooks"),
121        "util/types" => Some("util/types"),
122        "diagnostics_channel" => Some("diagnostics_channel"),
123        "v8" => Some("v8"),
124        "readline" => Some("readline"),
125        "vm" => Some("vm"),
126        "fs/promises" => Some("fs/promises"),
127        "dgram" => Some("dgram"),
128        "dns/promises" => Some("dns/promises"),
129        "worker_threads" => Some("worker_threads"),
130        "tls" => Some("tls"),
131        "https" => Some("https"),
132        "repl" => Some("repl"),
133        "cluster" => Some("cluster"),
134        "domain" => Some("domain"),
135        "http2" => Some("http2"),
136        "trace_events" => Some("trace_events"),
137        "module" => Some("module"),
138        "stream/consumers" => Some("stream/consumers"),
139        "stream/promises" => Some("stream/promises"),
140        "stream/web" => Some("stream/web"),
141        other => UNIMPLEMENTED_MODULES.iter().copied().find(|&m| m == other),
142    }
143}
144
145/// True if `qualified` (`namespace.method`) is a stdlib method that
146/// `call_builtin_function` should route into `call` (extends `is_known_builtin`).
147pub fn is_method(qualified: &str) -> bool {
148    let Some((ns, m)) = qualified.split_once('.') else {
149        return qualified == "assert";
150    };
151    // Any method on an unimplemented namespace routes to `call`, which throws an
152    // honest "not implemented" error (so `mod.foo()` fails clearly rather than
153    // silently returning undefined).
154    is_unimplemented(ns) || namespace_methods(ns).contains(&m) || namespace_ctors(ns).contains(&m)
155}
156
157/// The callable members of builtin namespace `ns`. THE single table backing both
158/// `is_method` (does `ns.m` dispatch?) and `namespace_keys` (what does `for (k in
159/// ns)` yield?), so a method can never be callable-but-unenumerable or the reverse.
160pub fn namespace_methods(ns: &str) -> &'static [&'static str] {
161    match ns {
162        "fs" => fs::METHODS,
163        "path" | "path/win32" => path::METHODS,
164        "os" => os::METHODS,
165        "util" => util::METHODS,
166        "assert" | "assertStrict" => assert::METHODS,
167        "crypto" => crypto::METHODS,
168        "Buffer" => buffer::STATIC_METHODS,
169        "buffer" => buffer::MODULE_METHODS,
170        "Date" => date::STATIC_METHODS,
171        "Response" => fetch::RESPONSE_STATICS,
172        "AbortSignal" => fetch::ABORT_SIGNAL_STATICS,
173        n if typedarray::is_ctor(n) => typedarray::STATIC_METHODS,
174        "url" => url::MODULE_METHODS,
175        "net" => net::MODULE_METHODS,
176        "http" => http::MODULE_METHODS,
177        "stream" => stream::METHODS,
178        "worker_threads" => worker_threads::METHODS,
179        "zlib" => zlib::MODULE_METHODS,
180        "querystring" => querystring::METHODS,
181        "tty" => tty::METHODS,
182        "process" => process::METHODS,
183        "EventEmitter" => events::STATIC_METHODS,
184        "console" => console::METHODS,
185        "child_process" => child_process::METHODS,
186        "dns" => dns::METHODS,
187        "dns/promises" => dns::PROMISES_METHODS,
188        "punycode" => punycode::METHODS,
189        "timers" => timers::METHODS,
190        "timers/promises" => timers::PROMISES_METHODS,
191        "perf_hooks" | "performance" => perf_hooks::METHODS,
192        "async_hooks" => async_hooks::METHODS,
193        "AsyncResource" => async_hooks::RESOURCE_STATIC_METHODS,
194        "util/types" => util_types::METHODS,
195        "diagnostics_channel" => diagnostics_channel::METHODS,
196        "v8" => v8::METHODS,
197        "readline" => readline::METHODS,
198        "vm" => vm::METHODS,
199        "fs/promises" => fs_promises::METHODS,
200        "dgram" => dgram::MODULE_METHODS,
201        "tls" => tls::MODULE_METHODS,
202        "https" => https::MODULE_METHODS,
203        "repl" => repl::METHODS,
204        "cluster" => cluster::METHODS,
205        "domain" => domain::METHODS,
206        "http2" => http2::METHODS,
207        "trace_events" => trace_events::METHODS,
208        "module" => node_module::METHODS,
209        "Module" => node_module::MODULE_STATIC_METHODS,
210        "stream/consumers" => stream_consumers::METHODS,
211        "stream/promises" => stream_promises::METHODS,
212        _ => &[],
213    }
214}
215
216/// Class/constructor members a namespace re-exports as values rather than
217/// callable methods (`require('buffer').Buffer`, `require('url').URL`). They are
218/// enumerable own keys too, so `for (k in buffer)` sees `Buffer`.
219pub fn namespace_ctors(ns: &str) -> &'static [&'static str] {
220    match ns {
221        "buffer" => &["Buffer", "Blob", "File"],
222        "url" => &["URL", "URLSearchParams"],
223        "EventEmitter" => &["EventEmitter"],
224        "async_hooks" => &["AsyncLocalStorage", "AsyncResource"],
225        "string_decoder" => &["StringDecoder"],
226        "assert" => &["AssertionError"],
227        "console" => &["Console"],
228        "vm" => &["Script"],
229        "fs" => &["promises"],
230        // `stream/web` exports nothing BUT classes (`METHODS` is empty), so
231        // without this arm the namespace had no enumerable key at all: measured
232        // against node v26.7.0, `Object.keys(require('stream/web')).length` was
233        // 0 here and 18 there, even though every one of the classes resolved
234        // fine through `constant`. A namespace that answers property reads but
235        // enumerates empty breaks the copy-the-module pattern
236        // (`{ ...require('stream/web') }`, `for (k in web)`).
237        "stream/web" => stream_web::CLASSES,
238        _ => &[],
239    }
240}
241
242/// The enumerable own keys of the builtin namespace `ns` — what `for (key in ns)`
243/// and `Object.keys(ns)` yield. These are the members node-js ACTUALLY
244/// implements, not Node's full export list, so a package that copies a namespace
245/// key-by-key (safer-buffer clones `buffer` and `Buffer`) ends up with exactly the
246/// working set rather than an empty object.
247pub fn namespace_keys(ns: &str) -> Vec<String> {
248    // The `require.cache` view enumerates the resolved filenames it holds.
249    if ns == crate::builtins::REQUIRE_CACHE {
250        return crate::module::cache_keys();
251    }
252    let mut out: Vec<String> = namespace_ctors(ns).iter().map(|s| s.to_string()).collect();
253    for m in namespace_methods(ns) {
254        if !out.iter().any(|k| k == m) {
255            out.push((*m).to_string());
256        }
257    }
258    out
259}
260
261/// Dispatch a resolved stdlib builtin (`assert`, or `namespace.method`). Returns
262/// `None` if `name` is not a stdlib builtin (the caller falls through to the core
263/// builtin table).
264pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
265    if name == "assert" {
266        return Some(assert::assert_ok(args));
267    }
268    let (ns, m) = name.split_once('.')?;
269    Some(match ns {
270        "fs" => fs::call(m, args)?,
271        "path" => path::call(path::Flavor::Posix, m, args)?,
272        "path/win32" => path::call(path::Flavor::Win32, m, args)?,
273        "os" => os::call(m, args)?,
274        "util" => util::call(m, args)?,
275        "assert" => assert::call(m, args)?,
276        "assertStrict" => assert::strict_call(m, args)?,
277        "crypto" => crypto::call(m, args)?,
278        "Buffer" => buffer::static_call(m, args)?,
279        "buffer" if m == "Buffer" => Ok(with_host(|h| h.alloc(JsObj::Builtin("Buffer".into())))),
280        "buffer" => buffer::module_call(m, args)?,
281        "Date" => date::static_call(m, args)?,
282        "Response" | "AbortSignal" => fetch::static_call(ns, m, args)?,
283        n if typedarray::is_ctor(n) => typedarray::static_call(n, m, args)?,
284        "url" if m == "URL" => Ok(with_host(|h| h.alloc(JsObj::Builtin("URL".into())))),
285        "url" => url::call(m, args)?,
286        "net" => net::call(m, args)?,
287        "http" => http::call(m, args)?,
288        "stream" => stream::call(m, args)?,
289        "worker_threads" => worker_threads::call(m, args)?,
290        "zlib" => zlib::call(m, args)?,
291        "querystring" => querystring::call(m, args)?,
292        "tty" => tty::call(m, args)?,
293        "process" => process::call(m, args)?,
294        "EventEmitter" if m == "EventEmitter" => Ok(with_host(|h| {
295            h.alloc(JsObj::Builtin("EventEmitter".into()))
296        })),
297        "EventEmitter" => events::static_call(m, args)?,
298        "console" => console::call(m, args)?,
299        "child_process" => child_process::call(m, args)?,
300        "dns" => dns::call(m, args)?,
301        "punycode" => punycode::call(m, args)?,
302        "timers" => timers::call(m, args)?,
303        "timers/promises" => timers::promises_call(m, args)?,
304        "perf_hooks" | "performance" => perf_hooks::call(m, args)?,
305        "async_hooks" => async_hooks::call(m, args)?,
306        "AsyncResource" => async_hooks::static_call(m, args)?,
307        "util/types" => util_types::call(m, args)?,
308        "diagnostics_channel" => diagnostics_channel::call(m, args)?,
309        "v8" => v8::call(m, args)?,
310        "readline" => readline::call(m, args)?,
311        "vm" => vm::call(m, args)?,
312        "fs/promises" => fs_promises::call(m, args)?,
313        "dgram" => dgram::call(m, args)?,
314        // dns/promises: getServers/setServers/get|setDefaultResultOrder are shared
315        // sync fns; every other method maps to dns's `promise<Cap>` variant.
316        "dns/promises" => match m {
317            "getServers" | "setServers" | "getDefaultResultOrder" | "setDefaultResultOrder" => {
318                dns::call(m, args)?
319            }
320            _ => {
321                let mut pm = String::from("promise");
322                let mut cs = m.chars();
323                if let Some(c) = cs.next() {
324                    pm.extend(c.to_uppercase());
325                    pm.push_str(cs.as_str());
326                }
327                dns::call(&pm, args)?
328            }
329        },
330        "tls" => tls::call(m, args)?,
331        "https" => https::call(m, args)?,
332        "repl" => repl::call(m, args)?,
333        "cluster" => cluster::call(m, args)?,
334        "domain" => domain::call(m, args)?,
335        "http2" => http2::call(m, args)?,
336        "trace_events" => trace_events::call(m, args)?,
337        "module" => node_module::call(m, args)?,
338        "Module" => node_module::static_call(m, args)?,
339        "stream/consumers" => stream_consumers::call(m, args)?,
340        "stream/promises" => stream_promises::call(m, args)?,
341        _ if is_unimplemented(ns) => Err(format!("Error: {ns}.{m} is not implemented in node-js")),
342        _ => return None,
343    })
344}
345
346/// A non-function constant on a stdlib namespace (`path.sep`, `os.EOL`,
347/// `buffer.Buffer`, `url.URL`), reachable via `namespace_property`.
348pub fn constant(ns: &str, name: &str) -> Option<Value> {
349    match ns {
350        // Both flavors carry `.posix`/`.win32` cross-links, exactly as Node's
351        // `posix.win32 = win32.win32 = win32; posix.posix = win32.posix = posix`.
352        "path" | "path/win32" if name == "posix" => {
353            Some(with_host(|h| h.alloc(JsObj::Builtin("path".into()))))
354        }
355        "path" | "path/win32" if name == "win32" => {
356            Some(with_host(|h| h.alloc(JsObj::Builtin("path/win32".into()))))
357        }
358        "path" => path::constant(path::Flavor::Posix, name),
359        "path/win32" => path::constant(path::Flavor::Win32, name),
360        "os" => os::constant(name),
361        // `Buffer.poolSize` is a DATA property, not a method, so it belongs here
362        // rather than in `STATIC_METHODS` (which would make it read as a
363        // function). It was absent entirely: `Buffer.poolSize` was `undefined`
364        // where node v26.7.0 reports 65536. node-js allocates each Buffer on its
365        // own, so this is the documented constant, not a live allocator figure.
366        "Buffer" if name == "poolSize" => Some(Value::Float(65536.0)),
367        "buffer" if name == "Buffer" => {
368            Some(with_host(|h| h.alloc(JsObj::Builtin("Buffer".into()))))
369        }
370        "buffer" if matches!(name, "Blob" | "File") => {
371            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
372        }
373        "url" if name == "URL" => Some(with_host(|h| h.alloc(JsObj::Builtin("URL".into())))),
374        "net" => net::constant(name),
375        "tty" => tty::constant(name),
376        "repl" => repl::constant(name),
377        "readline" => readline::constant(name),
378        "diagnostics_channel" => diagnostics_channel::constant(name),
379        "v8" => v8::constant(name),
380        "console" if name == "Console" => {
381            Some(with_host(|h| h.alloc(JsObj::Builtin("Console".into()))))
382        }
383        "assert" if name == "AssertionError" => Some(with_host(|h| {
384            h.alloc(JsObj::Builtin("AssertionError".into()))
385        })),
386        "assert" if name == "strict" => Some(with_host(|h| {
387            h.alloc(JsObj::Builtin("assertStrict".into()))
388        })),
389        "stream" => stream::constant(name),
390        "http" => http::constant(name),
391        "string_decoder" if name == "StringDecoder" => Some(with_host(|h| {
392            h.alloc(JsObj::Builtin("StringDecoder".into()))
393        })),
394        "process" => process::constant(name),
395        "EventEmitter" if name == "EventEmitter" => Some(with_host(|h| {
396            h.alloc(JsObj::Builtin("EventEmitter".into()))
397        })),
398        "perf_hooks" | "performance" => perf_hooks::constant(name),
399        "dns" => dns::constant(name),
400        "punycode" => punycode::constant(name),
401        "async_hooks" if matches!(name, "AsyncLocalStorage" | "AsyncResource") => {
402            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
403        }
404        "vm" if name == "Script" => Some(with_host(|h| h.alloc(JsObj::Builtin("Script".into())))),
405        "url" if name == "URLSearchParams" => Some(with_host(|h| {
406            h.alloc(JsObj::Builtin("URLSearchParams".into()))
407        })),
408        "fs" if name == "promises" => {
409            Some(with_host(|h| h.alloc(JsObj::Builtin("fs/promises".into()))))
410        }
411        "worker_threads" => worker_threads::constant(name),
412        "https" => https::constant(name),
413        "cluster" => cluster::constant(name),
414        "domain" => domain::constant(name),
415        "http2" => http2::constant(name),
416        "module" => node_module::constant(name),
417        "Module" => node_module::static_constant(name),
418        "stream/web" => stream_web::constant(name),
419        // util.types / util.TextEncoder|TextDecoder / util.MIMEType|MIMEParams.
420        "util" => util::constant(name),
421        // crypto class-constructor exports (require('crypto').Sign etc.) — the
422        // instances are made by factory fns, but the ctor names must resolve.
423        "crypto"
424            if matches!(
425                name,
426                "Sign"
427                    | "Verify"
428                    | "KeyObject"
429                    | "DiffieHellman"
430                    | "ECDH"
431                    | "X509Certificate"
432                    | "Hash"
433                    | "Hmac"
434                    | "Cipheriv"
435                    | "Decipheriv"
436            ) =>
437        {
438            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
439        }
440        _ => None,
441    }
442}
443
444/// Construct a stdlib class instance (`new URL(...)`, `new EventEmitter()`, and
445/// `new Buffer(...)` legacy), reachable from `construct_builtin`. `None` if `name`
446/// is not a stdlib constructor.
447pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
448    match name {
449        "URL" => Some(url::construct(args)),
450        "EventEmitter" => Some(Ok(events::new_emitter())),
451        // `new Buffer(x)` and the deprecated call form `Buffer(x)` are the same
452        // operation, and it is NOT simply `Buffer.from`: a NUMBER allocates that
453        // many zero bytes, where `Buffer.from(3)` is a TypeError in Node.
454        // Measured on node v26.7.0, `new Buffer(3)` and `Buffer(3)` are both
455        // `<Buffer 00 00 00>` (zero-filled since the `Buffer.alloc` semantics
456        // landed), while `new Buffer('ab')` and `new Buffer([1,2])` behave as
457        // `from`. Routing everything through `from` made `new Buffer(3)` one byte
458        // long.
459        "Buffer" => {
460            let numeric = matches!(args.first(), Some(Value::Int(_)) | Some(Value::Float(_)))
461                && args.len() == 1;
462            let m = if numeric { "alloc" } else { "from" };
463            Some(buffer::static_call(m, args).unwrap_or(Ok(Value::Undef)))
464        }
465        "Date" => Some(date::construct(args)),
466        "StringDecoder" => Some(string_decoder::construct(args)),
467        "WeakRef" => Some(typedarray::construct_weakref(args)),
468        "FinalizationRegistry" => Some(typedarray::construct_finalization_registry(args)),
469        n if fetch::is_class(n) => fetch::construct(n, args),
470        "TextEncoder" => Some(typedarray::construct_text_encoder()),
471        "TextDecoder" => Some(typedarray::construct_text_decoder(args)),
472        n if typedarray::is_ctor(n) => Some(typedarray::construct(n, args)),
473        n if stream::is_class(n) => Some(Ok(stream::construct(n, args))),
474        "AsyncLocalStorage" | "AsyncResource" => async_hooks::construct(name, args),
475        "Script" => Some(vm::construct(args)),
476        "URLSearchParams" => Some(url::construct_search_params(args)),
477        "Worker" => Some(worker_threads::construct_worker(args)),
478        "Domain" => Some(domain::construct(args)),
479        "Tracing" => Some(trace_events::construct(args)),
480        "Blob" => Some(buffer::construct_blob(args)),
481        "File" => Some(buffer::construct_file(args)),
482        "AssertionError" => Some(Ok(assert::construct_assertion_error(args))),
483        "X509Certificate" => Some(crypto::construct_x509(args)),
484        "MIMEType" => Some(util::construct_mime_type(args)),
485        "MIMEParams" => Some(util::construct_mime_params(args)),
486        "Resolver" => Some(Ok(dns::construct_resolver(args))),
487        "ReadStream" | "WriteStream" => Some(Ok(tty::construct(name, args))),
488        "MessageChannel" => Some(worker_threads::construct_message_channel(args)),
489        "BroadcastChannel" => Some(worker_threads::construct_broadcast_channel(args)),
490        "PerformanceObserver" => Some(perf_hooks::construct(name, args)),
491        "REPLServer" | "Recoverable" => Some(repl::construct(name, args)),
492        "Interface" => Some(readline::construct(args)),
493        "Console" => Some(console::construct(args)),
494        "Serializer" | "DefaultSerializer" | "Deserializer" | "DefaultDeserializer" => {
495            Some(v8::construct(name, args))
496        }
497        // net/http constructors: their `construct` already returns Option<Result>.
498        "Socket" | "Stream" | "Server" | "SocketAddress" | "BlockList" => {
499            net::construct(name, args)
500        }
501        "Agent" | "http.Server" => http::construct(name, args),
502        // stream/web WHATWG classes (its `construct` returns Option<Result>).
503        n if stream_web::is_class(n) => stream_web::construct(n, args),
504        _ => None,
505    }
506}
507
508/// The hidden `@@native` instance tag of `recv` (`"Buffer"`/`"Hash"`/
509/// `"EventEmitter"`/`"URL"`), or `None` for a non-native object.
510pub fn native_tag(recv: &Value) -> Option<String> {
511    with_host(|h| match h.get(recv) {
512        Some(JsObj::Object(p)) => p.get("@@native").map(|v| h.str_of(v)),
513        _ => None,
514    })
515}
516
517/// Native instance tags whose `instance_call` implements `toJSON()`, which
518/// `JSON.stringify` must invoke before serializing the value. (`instance_has_method`
519/// only covers tags with a declared method table; `Date` dispatches directly.)
520pub fn has_to_json(tag: &str) -> bool {
521    matches!(tag, "Buffer" | "Date" | "URL" | "MIMEType" | "MIMEParams")
522}
523
524/// Whether `name` is a method of a native instance tagged `tag`. Used by
525/// `get_property` so a method *read* (`server.listen.apply(...)`, the express
526/// listen path) yields a bound method rather than `undefined` — the method is
527/// still dispatched through `instance_call` when the bound method is invoked.
528pub fn instance_has_method(tag: &str, name: &str) -> bool {
529    let (base, emitter) = instance_method_lists(tag);
530    base.contains(&name) || emitter.contains(&name)
531}
532
533/// The method names a native instance tagged `tag` carries, as
534/// `(its own list, the EventEmitter surface it also gets or empty)`.
535///
536/// Split out of [`instance_has_method`] so the same table can be *enumerated*,
537/// not only queried: `host::ensure_ctor_proto` builds a native constructor's
538/// real `.prototype` object from it. A predicate alone would have forced a
539/// second, hand-maintained list of the same names — the drift that put
540/// `listeners` on nine dispatchers and not on the three that run.
541pub fn instance_method_lists(tag: &str) -> (&'static [&'static str], &'static [&'static str]) {
542    // Shared EventEmitter surface for the emitter-backed instances. Read from
543    // `events::METHODS` so what an instance ADVERTISES here can never drift from
544    // what the dispatchers actually delegate.
545    const EMITTER: &[&str] = events::METHODS;
546    let base: &[&str] = match tag {
547        "Timeout" => timers::TIMEOUT_METHODS,
548        "Immediate" => timers::IMMEDIATE_METHODS,
549        "Server" => &["listen", "close", "address"],
550        "Socket" => &[
551            "write",
552            "end",
553            "destroy",
554            "pause",
555            "resume",
556            "setEncoding",
557            "setKeepAlive",
558            "setNoDelay",
559            "setTimeout",
560            "ref",
561            "unref",
562            "connect",
563        ],
564        "ServerResponse" => &[
565            "writeHead",
566            "setHeader",
567            "getHeader",
568            "getHeaderNames",
569            "getHeaders",
570            "hasHeader",
571            "removeHeader",
572            "write",
573            "end",
574            "flushHeaders",
575        ],
576        "IncomingMessage" => &["pause", "resume", "setEncoding", "destroy"],
577        "Buffer" => buffer::INSTANCE_METHODS,
578        "Date" => date::INSTANCE_METHODS,
579        "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" | "Stream" => &[
580            "read",
581            "write",
582            "end",
583            "pipe",
584            "pause",
585            "resume",
586            "setEncoding",
587            "destroy",
588            "push",
589        ],
590        "URL" => &["toString", "toJSON"],
591        "AsyncLocalStorage" => async_hooks::ALS_METHODS,
592        "AsyncHook" => async_hooks::HOOK_METHODS,
593        "AsyncResource" => async_hooks::RESOURCE_METHODS,
594        "Channel" => &["subscribe", "unsubscribe", "publish"],
595        "WriteStream" => &[
596            "write",
597            "end",
598            "on",
599            "once",
600            "removeListener",
601            "cork",
602            "uncork",
603            "setEncoding",
604        ],
605        // `Hash` and `Hmac` answer the same two methods (both route to
606        // `crypto::hashlike_call`). Only `Hmac` was listed, so
607        // `ensure_ctor_proto("Hash")` found nothing and `crypto.Hash.prototype`
608        // read `undefined` — the ES5-subclassing hole this table exists to
609        // close, still open for one of the two constructors it documents.
610        "Hash" | "Hmac" => &["update", "digest"],
611        "StringDecoder" => string_decoder::INSTANCE_METHODS,
612        "Interface" => readline::INTERFACE_METHODS,
613        "Script" => vm::SCRIPT_METHODS,
614        "URLSearchParams" => url::SEARCH_PARAMS_METHODS,
615        "UdpSocket" => dgram::SOCKET_METHODS,
616        "Worker" => worker_threads::WORKER_METHODS,
617        "MessagePort" => worker_threads::PORT_METHODS,
618        "TLSServer" => tls::SERVER_METHODS,
619        "TLSSocket" => tls::SOCKET_METHODS,
620        "HTTPSServerResponse" => https::RESPONSE_METHODS,
621        "HTTPSClientRequest" => https::CLIENT_REQUEST_METHODS,
622        "REPLServer" => repl::REPLSERVER_METHODS,
623        "ClusterWorker" => cluster::WORKER_METHODS,
624        "Domain" => domain::DOMAIN_METHODS,
625        "Tracing" => trace_events::TRACING_METHODS,
626        "Http2Server" => http2::SERVER_METHODS,
627        "Http2Stream" => http2::STREAM_METHODS,
628        "Http2Session" => http2::SESSION_METHODS,
629        "Cipheriv" | "Decipheriv" => &["update", "final", "setAutoPadding"],
630        "BlockList" => net::BLOCKLIST_METHODS,
631        "ClientRequest" => http::CLIENT_REQUEST_METHODS,
632        "Agent" => &["destroy", "getName"],
633        "Blob" | "File" => buffer::BLOB_METHODS,
634        "ReadStream" => tty::READ_STREAM_METHODS,
635        "Dirent" => fs::DIRENT_METHODS,
636        "Dir" => fs::DIR_METHODS,
637        "FSReadStream" => fs::READ_STREAM_METHODS,
638        "FSWriteStream" => fs::WRITE_STREAM_METHODS,
639        "Resolver" => dns::RESOLVER_METHODS,
640        "Histogram" => perf_hooks::HISTOGRAM_METHODS,
641        "PerformanceObserver" => perf_hooks::PERFORMANCE_OBSERVER_METHODS,
642        "PerformanceObserverEntryList" => perf_hooks::OBSERVER_ENTRY_LIST_METHODS,
643        "BroadcastChannel" => worker_threads::BROADCAST_CHANNEL_METHODS,
644        "TracingChannel" => diagnostics_channel::TRACING_CHANNEL_METHODS,
645        "Serializer" => v8::SERIALIZER_METHODS,
646        "Deserializer" => v8::DESERIALIZER_METHODS,
647        "Console" => console::CONSOLE_METHODS,
648        "ChildProcess" => child_process::CHILD_PROCESS_METHODS,
649        "Sign" => &["update", "sign"],
650        "Verify" => &["update", "verify"],
651        "KeyObject" => &["export", "equals"],
652        "DiffieHellman" => &[
653            "generateKeys",
654            "computeSecret",
655            "getPrime",
656            "getGenerator",
657            "getPublicKey",
658            "getPrivateKey",
659            "setPublicKey",
660            "setPrivateKey",
661        ],
662        "ECDH" => &[
663            "generateKeys",
664            "computeSecret",
665            "getPublicKey",
666            "getPrivateKey",
667            "setPrivateKey",
668        ],
669        "X509Certificate" => &["toString"],
670        "FinalizationRegistry" => &["register", "unregister"],
671        "MIMEType" => util::MIME_TYPE_METHODS,
672        "MIMEParams" => util::MIME_PARAMS_METHODS,
673        t if fetch::is_class(t) => fetch::methods_for(t),
674        t if stream_web::is_class(t) => stream_web::methods_for(t),
675        _ => &[],
676    };
677    let is_emitter = matches!(
678        tag,
679        "Server"
680            | "Socket"
681            | "ServerResponse"
682            | "IncomingMessage"
683            | "EventEmitter"
684            | "Readable"
685            | "Writable"
686            | "Duplex"
687            | "Transform"
688            | "PassThrough"
689            | "Stream"
690            | "UdpSocket"
691            | "Worker"
692            | "MessagePort"
693            | "TLSServer"
694            | "TLSSocket"
695            | "HTTPSServerResponse"
696            | "HTTPSClientRequest"
697            | "ClusterWorker"
698            | "Domain"
699            | "Http2Server"
700            | "Http2Stream"
701            | "Http2Session"
702            | "ClientRequest"
703            | "FSReadStream"
704            | "FSWriteStream"
705            | "ChildProcess"
706    );
707    (base, if is_emitter { EMITTER } else { &[] })
708}
709
710/// Dispatch a method call on a native stdlib instance (`recv` carries a
711/// `@@native` tag). Called from `host::call_method` before the generic object
712/// method resolution.
713pub fn instance_call(
714    tag: &str,
715    recv: &Value,
716    method: &str,
717    args: Vec<Value>,
718) -> Result<Value, String> {
719    match tag {
720        "Buffer" => buffer::instance_call(recv, method, &args),
721        "Timeout" | "Immediate" => timers::instance_call(recv, method, &args),
722        "Date" => date::instance_call(recv, method, &args),
723        "StringDecoder" => string_decoder::instance_call(recv, method, &args),
724        "WeakRef" => typedarray::weakref_call(recv, method),
725        "FinalizationRegistry" => typedarray::finalization_registry_call(recv, method, &args),
726        "TextEncoder" => typedarray::text_encoder_call(recv, method, &args),
727        "TextDecoder" => typedarray::text_decoder_call(recv, method, &args),
728        "TypedArray" => typedarray::instance_call(recv, method, &args),
729        t if fetch::is_class(t) => fetch::instance_call(t, recv, method, &args),
730        "Hash" => crypto::instance_call(recv, method, &args),
731        "Hmac" => crypto::hmac_instance_call(recv, method, &args),
732        "Interface" => readline::instance_call(recv, method, args),
733        "Script" => vm::instance_call(recv, method, args),
734        "URLSearchParams" => url::search_params_call(recv, method, &args),
735        "UdpSocket" => dgram::instance_call(recv, method, args),
736        "Worker" | "MessagePort" | "BroadcastChannel" => {
737            worker_threads::instance_call(tag, recv, method, args)
738        }
739        "TLSServer" | "TLSSocket" => tls::instance_call(tag, recv, method, args),
740        "HTTPSServerResponse" | "HTTPSClientRequest" => {
741            https::instance_call(tag, recv, method, args)
742        }
743        "REPLServer" => repl::instance_call(recv, method, args),
744        "ClusterWorker" => cluster::instance_call(recv, method, args),
745        "Domain" => domain::instance_call(recv, method, args),
746        "Tracing" => trace_events::instance_call(recv, method, args),
747        "Http2Server" | "Http2Stream" | "Http2Session" => {
748            http2::instance_call(tag, recv, method, args)
749        }
750        "EventEmitter" => events::instance_call(recv, method, args),
751        "URL" => url::instance_call(recv, method, &args),
752        "Stats" => fs::stats_call(recv, method),
753        "Dirent" => fs::dirent_call(recv, method),
754        "Dir" => fs::dir_call(recv, method, args),
755        "FSReadStream" => fs::read_stream_call(recv, method, args),
756        "FSWriteStream" => fs::write_stream_call(recv, method, args),
757        "Server" | "Socket" | "BlockList" => net::instance_call(tag, recv, method, args),
758        "IncomingMessage" | "ServerResponse" | "ClientRequest" | "Agent" => {
759            http::instance_call(tag, recv, method, args)
760        }
761        "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" | "Stream" => {
762            stream::instance_call(tag, recv, method, args)
763        }
764        "Cipheriv" | "Decipheriv" => crypto::cipher_instance_call(tag, recv, method, &args),
765        "Sign" | "Verify" => crypto::sign_verify_instance_call(tag, recv, method, &args),
766        "KeyObject" => crypto::key_object_instance_call(recv, method, &args),
767        "DiffieHellman" => crypto::dh_instance_call(recv, method, &args),
768        "ECDH" => crypto::ecdh_instance_call(recv, method, &args),
769        "X509Certificate" => crypto::x509_instance_call(recv, method, &args),
770        "MIMEType" => util::mime_type_instance_call(recv, method, &args),
771        "MIMEParams" => util::mime_params_instance_call(recv, method, &args),
772        "Blob" | "File" => buffer::blob_call(recv, method, &args),
773        "ReadStream" => tty::instance_call(recv, method, &args),
774        "Resolver" => dns::resolver_instance_call(recv, method, args),
775        "Histogram" => perf_hooks::histogram_instance_call(recv, method, &args),
776        "PerformanceObserver" => perf_hooks::observer_instance_call(recv, method, &args),
777        "PerformanceObserverEntryList" => perf_hooks::entry_list_instance_call(recv, method, &args),
778        "TracingChannel" => diagnostics_channel::tracing_instance_call(recv, method, &args),
779        "Serializer" | "Deserializer" => v8::instance_call(tag, recv, method, args),
780        "Console" => console::instance_call(recv, method, args),
781        "ChildProcess" => child_process::instance_call(recv, method, args),
782        t if stream_web::is_class(t) => stream_web::instance_call(t, recv, method, args),
783        "AsyncLocalStorage" | "AsyncHook" | "AsyncResource" => {
784            async_hooks::instance_call(tag, recv, method, args)
785        }
786        "Channel" => diagnostics_channel::instance_call(recv, method, &args),
787        "WriteStream" => process::stream_instance_call(recv, method, &args),
788        _ => Err(crate::host::type_error(&format!(
789            "{method} is not a function"
790        ))),
791    }
792}
793
794// ── shared helpers ──────────────────────────────────────────────────────────
795
796/// The `Received …` tail Node appends to an `ERR_INVALID_ARG_TYPE` message
797/// (`internal/errors.js` `determineSpecificType`): `null`/`undefined` verbatim,
798/// a primitive as `type <typeof> (<inspected>)`, an object as
799/// `an instance of <Ctor>`.
800pub(crate) fn received_desc(v: &Value) -> String {
801    with_host(|h| {
802        if matches!(v, Value::Undef) {
803            return "undefined".to_string();
804        }
805        if h.is_null(v) {
806            return "null".to_string();
807        }
808        let ty = h.type_of(v);
809        if ty == "object" || ty == "function" {
810            // `ctor_name` is empty for the builtin shapes (they carry no user
811            // class), so fall back to the intrinsic constructor name.
812            let name = match h.ctor_name(v) {
813                n if !n.is_empty() => n,
814                _ => match h.get(v) {
815                    Some(JsObj::Array(_)) => "Array".into(),
816                    Some(JsObj::Map { .. }) => "Map".into(),
817                    Some(JsObj::Set { .. }) => "Set".into(),
818                    Some(JsObj::Promise { .. }) => "Promise".into(),
819                    Some(JsObj::RegExp(_)) => "RegExp".into(),
820                    Some(JsObj::Object(p)) => match p.get("@@native") {
821                        Some(t) => h.str_of(t),
822                        None => "Object".into(),
823                    },
824                    _ => "Object".into(),
825                },
826            };
827            return format!("an instance of {name}");
828        }
829        let shown = match ty {
830            "string" => format!("'{}'", h.str_of(v)),
831            "bigint" => format!("{}n", h.str_of(v)),
832            "number" if matches!(v, Value::Float(f) if *f == 0.0 && f.is_sign_negative()) => {
833                "-0".to_string()
834            }
835            _ => h.str_of(v),
836        };
837        format!("type {ty} ({shown})")
838    })
839}
840
841/// ToString of `args[i]` (empty string if absent).
842pub(crate) fn arg_str(args: &[Value], i: usize) -> String {
843    with_host(|h| args.get(i).map(|v| h.str_of(v)).unwrap_or_default())
844}
845
846/// ToNumber of `args[i]` (`NaN` if absent).
847pub(crate) fn arg_num(args: &[Value], i: usize) -> f64 {
848    with_host(|h| args.get(i).map(|v| h.to_number(v)).unwrap_or(f64::NAN))
849}
850
851/// Lowercase hex encoding of `bytes`.
852pub(crate) fn to_hex(bytes: &[u8]) -> String {
853    let mut s = String::with_capacity(bytes.len() * 2);
854    for b in bytes {
855        s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
856        s.push(char::from_digit((b & 0xf) as u32, 16).unwrap());
857    }
858    s
859}
860
861/// Decode a hex string to bytes (ignoring a trailing odd nibble, like Node).
862pub(crate) fn from_hex(s: &str) -> Vec<u8> {
863    let digits: Vec<u8> = s
864        .bytes()
865        .filter_map(|c| (c as char).to_digit(16).map(|d| d as u8))
866        .collect();
867    digits
868        .chunks(2)
869        .filter(|c| c.len() == 2)
870        .map(|c| (c[0] << 4) | c[1])
871        .collect()
872}
873
874const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
875
876/// Standard base64 encoding (with `=` padding) of `bytes`.
877pub(crate) fn to_base64(bytes: &[u8]) -> String {
878    let mut out = String::new();
879    for chunk in bytes.chunks(3) {
880        let b = [
881            chunk[0],
882            *chunk.get(1).unwrap_or(&0),
883            *chunk.get(2).unwrap_or(&0),
884        ];
885        let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | (b[2] as u32);
886        out.push(B64[((n >> 18) & 63) as usize] as char);
887        out.push(B64[((n >> 12) & 63) as usize] as char);
888        out.push(if chunk.len() > 1 {
889            B64[((n >> 6) & 63) as usize] as char
890        } else {
891            '='
892        });
893        out.push(if chunk.len() > 2 {
894            B64[(n & 63) as usize] as char
895        } else {
896            '='
897        });
898    }
899    out
900}
901
902/// URL-safe base64 (RFC 4648 §5) of `bytes`: `+/` become `-_` and the `=`
903/// padding is dropped. This is `buf.toString('base64url')`, which is a distinct
904/// encoding from `'base64'` — not an alias. Node emits `Buffer.from([251,255,
905/// 190,1]).toString('base64url')` as `-_--AQ` where `'base64'` gives `+/++AQ==`.
906pub(crate) fn to_base64url(bytes: &[u8]) -> String {
907    to_base64(bytes)
908        .chars()
909        .filter(|c| *c != '=')
910        .map(|c| match c {
911            '+' => '-',
912            '/' => '_',
913            c => c,
914        })
915        .collect()
916}
917
918/// Decode a base64 string to bytes (ignores whitespace and padding).
919///
920/// BOTH alphabets are accepted, in either direction: node decodes `-_` under
921/// `'base64'` and `+/` under `'base64url'` (measured — `Buffer.from('-_-_',
922/// 'base64').toString('hex')` and `Buffer.from('+/+/','base64url')
923/// .toString('hex')` are both `fbffbf` on v26.7.0), so the decoder does not need
924/// to know which name it was reached by. Refusing the URL-safe characters here
925/// silently produced an EMPTY buffer, because an unrecognized character is
926/// dropped rather than rejected.
927pub(crate) fn from_base64(s: &str) -> Vec<u8> {
928    let rev = |c: u8| -> Option<u32> {
929        let c = match c {
930            b'-' => b'+',
931            b'_' => b'/',
932            c => c,
933        };
934        B64.iter().position(|&x| x == c).map(|p| p as u32)
935    };
936    let vals: Vec<u32> = s.bytes().filter_map(rev).collect();
937    let mut out = Vec::new();
938    for chunk in vals.chunks(4) {
939        if chunk.len() < 2 {
940            break;
941        }
942        let n = (chunk[0] << 18)
943            | (chunk[1] << 12)
944            | (chunk.get(2).copied().unwrap_or(0) << 6)
945            | chunk.get(3).copied().unwrap_or(0);
946        out.push((n >> 16) as u8);
947        if chunk.len() > 2 {
948            out.push((n >> 8) as u8);
949        }
950        if chunk.len() > 3 {
951            out.push(n as u8);
952        }
953    }
954    out
955}