Skip to main content

nodejs/stdlib/
util.rs

1//! Node `util` module: `format`, `inspect`, and a subset of `util.types`.
2
3use crate::host::{with_host, JsObj};
4use fusevm::Value;
5use indexmap::IndexMap;
6
7pub const METHODS: &[&str] = &[
8    "format",
9    "formatWithOptions",
10    "inspect",
11    "deprecate",
12    "inherits",
13    "types",
14    "types.isMap",
15    "types.isSet",
16    "types.isPromise",
17    "types.isDate",
18    "types.isRegExp",
19    "types.isNativeError",
20    "types.isAsyncFunction",
21    "isDeepStrictEqual",
22    "isArray",
23    "debuglog",
24    "stripVTControlCharacters",
25    "toUSVString",
26    "getSystemErrorName",
27    "getSystemErrorMessage",
28    "getSystemErrorMap",
29    "styleText",
30    "parseArgs",
31    "promisify",
32    "callbackify",
33    "parseEnv",
34    "debug",
35];
36
37/// Non-function `util` exports (`require('util').TextEncoder`, `.MIMEType`, …).
38/// Each resolves to a `Builtin("<name>")` the parent `construct`s via `new`.
39pub fn constant(name: &str) -> Option<Value> {
40    match name {
41        "types" => Some(with_host(|h| h.alloc(JsObj::Builtin("util/types".into())))),
42        "TextEncoder" | "TextDecoder" | "MIMEType" | "MIMEParams" => {
43            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
44        }
45        _ => None,
46    }
47}
48
49pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
50    if let Some(pred) = method.strip_prefix("types.") {
51        return Some(Ok(Value::Bool(type_predicate(pred, args.first()))));
52    }
53    Some(match method {
54        "format" => {
55            // `format` re-enters `with_host` internally, so build the string first
56            // and only then allocate — never nest the borrow.
57            let s = format(args);
58            Ok(with_host(|h| h.new_str(s)))
59        }
60        // `formatWithOptions(inspectOptions, fmt, ...args)`: the options object
61        // only tunes `inspect` styling, which we do not vary — drop it and format
62        // the remaining arguments exactly like `format`.
63        "formatWithOptions" => {
64            let s = format(args.get(1..).unwrap_or(&[]));
65            Ok(with_host(|h| h.new_str(s)))
66        }
67        "inspect" => {
68            // Honor an options `{ depth: N | null }` (default 2; null = unlimited).
69            let depth = match args.get(1) {
70                Some(opts) => match crate::builtins::get_property(opts, "depth") {
71                    Ok(Value::Undef) => 2,
72                    Ok(v) if with_host(|h| h.is_null(&v)) => usize::MAX,
73                    Ok(v) => {
74                        let n = with_host(|h| h.to_number(&v));
75                        if n.is_finite() && n >= 0.0 {
76                            n as usize
77                        } else {
78                            usize::MAX
79                        }
80                    }
81                    Err(_) => 2,
82                },
83                None => 2,
84            };
85            crate::host::set_inspect_max_depth(depth);
86            let out = with_host(|h| {
87                let s = h.inspect(&args.first().cloned().unwrap_or(Value::Undef));
88                h.new_str(s)
89            });
90            crate::host::set_inspect_max_depth(2);
91            Ok(out)
92        }
93        // `deprecate(fn, msg)`: return a callable that behaves like `fn`. The
94        // house rule is no deprecation nags, so no warning is emitted — the
95        // original function is handed back unchanged.
96        "deprecate" => Ok(args.first().cloned().unwrap_or(Value::Undef)),
97        // `inherits(ctor, superCtor)`: modern Node semantics — set `ctor.super_`
98        // and re-link `ctor.prototype`'s `[[Prototype]]` to `superCtor.prototype`
99        // (methods already on `ctor.prototype` are preserved).
100        "inherits" => Ok(inherits(args)),
101        // Bare `util.types` accessed then called is not meaningful; return the
102        // namespace value so a stray call is a harmless undefined.
103        "types" => Ok(Value::Undef),
104        "isDeepStrictEqual" => Ok(Value::Bool(super::assert::deep_equal(
105            &args.first().cloned().unwrap_or(Value::Undef),
106            &args.get(1).cloned().unwrap_or(Value::Undef),
107            true,
108        ))),
109        // `util.isArray === Array.isArray` (a legacy alias Node still ships).
110        "isArray" => Ok(Value::Bool(with_host(|h| {
111            matches!(
112                h.get(&args.first().cloned().unwrap_or(Value::Undef)),
113                Some(JsObj::Array(_))
114            )
115        }))),
116        // `stripVTControlCharacters(str)`: remove ANSI/VT escape sequences.
117        "stripVTControlCharacters" => {
118            let stripped = strip_vt(&super::arg_str(args, 0));
119            Ok(with_host(|h| h.new_str(stripped)))
120        }
121        // `toUSVString(str)`: replace unpaired surrogates with U+FFFD. Strings are
122        // already well-formed UTF-8 here (no lone surrogates survive), so the input
123        // round-trips unchanged.
124        "toUSVString" => Ok(with_host(|h| {
125            let s = h.str_of(&args.first().cloned().unwrap_or(Value::Undef));
126            h.new_str(s)
127        })),
128        "getSystemErrorName" => {
129            let e = errno_of(super::arg_num(args, 0));
130            let name = errno_name(e)
131                .map(str::to_string)
132                .unwrap_or_else(|| format!("Unknown system error {e}"));
133            Ok(with_host(|h| h.new_str(name)))
134        }
135        "getSystemErrorMessage" => {
136            let e = errno_of(super::arg_num(args, 0));
137            let msg = errno_message(e)
138                .map(str::to_string)
139                .unwrap_or_else(|| format!("Unknown system error {e}"));
140            Ok(with_host(|h| h.new_str(msg)))
141        }
142        "getSystemErrorMap" => Ok(system_error_map()),
143        "styleText" => return Some(style_text(args)),
144        "parseArgs" => return Some(parse_args(args)),
145        "debuglog" => return Some(debuglog(args)),
146        "promisify" => return Some(promisify(args)),
147        "callbackify" => return Some(callbackify(args)),
148        // `util.parseEnv(content)` → an object of the parsed dotenv assignments.
149        "parseEnv" => Ok(parse_env(&super::arg_str(args, 0))),
150        // `util.debug === util.debuglog` (a documented alias).
151        "debug" => return Some(debuglog(args)),
152        _ => return None,
153    })
154}
155
156/// `util.inherits(ctor, superCtor)`.
157fn inherits(args: &[Value]) -> Value {
158    let ctor = args.first().cloned().unwrap_or(Value::Undef);
159    let sup = args.get(1).cloned().unwrap_or(Value::Undef);
160    with_host(|h| {
161        // Get-or-create `superCtor.prototype` and store it, so a later
162        // `superCtor.prototype` read returns the same object identity (`===`).
163        let sup_proto = h.fn_prop(&sup, "prototype").unwrap_or_else(|| {
164            let mut props = indexmap::IndexMap::new();
165            props.insert("constructor".to_string(), sup.clone());
166            let p = h.new_object(props);
167            h.set_fn_prop(&sup, "prototype", p.clone());
168            p
169        });
170        // Get-or-create `ctor.prototype` (with a `constructor` back-link).
171        let ctor_proto = h.fn_prop(&ctor, "prototype").unwrap_or_else(|| {
172            let mut props = indexmap::IndexMap::new();
173            props.insert("constructor".to_string(), ctor.clone());
174            let p = h.new_object(props);
175            h.set_fn_prop(&ctor, "prototype", p.clone());
176            p
177        });
178        h.set_proto(&ctor_proto, sup_proto);
179        h.set_fn_prop(&ctor, "super_", sup);
180    });
181    Value::Undef
182}
183
184fn type_predicate(pred: &str, v: Option<&Value>) -> bool {
185    let Some(v) = v else { return false };
186    with_host(|h| match pred {
187        "isMap" => matches!(h.get(v), Some(JsObj::Map { weak: false, .. })),
188        "isSet" => matches!(h.get(v), Some(JsObj::Set { weak: false, .. })),
189        "isPromise" => matches!(h.get(v), Some(JsObj::Promise { .. })),
190        _ => false,
191    })
192}
193
194/// `util.format(fmt, ...args)` — printf-style substitution (`%s %d %i %f %j %o %O
195/// %c %%`) with any leftover arguments appended space-separated.
196pub fn format(args: &[Value]) -> String {
197    if args.is_empty() {
198        return String::new();
199    }
200    // Node: a single argument is returned as-is (no specifier processing) —
201    // `util.format("100%% done")` === "100%% done".
202    if args.len() == 1 {
203        return with_host(|h| h.console_format(&args[0]));
204    }
205    let fmt = with_host(|h| h.str_of(&args[0]));
206    // A non-string first argument: inspect everything, space-joined.
207    if !matches!(args[0], Value::Str(_))
208        && !with_host(|h| matches!(h.get(&args[0]), Some(JsObj::Str(_))))
209    {
210        return with_host(|h| {
211            args.iter()
212                .map(|a| h.console_format(a))
213                .collect::<Vec<_>>()
214                .join(" ")
215        });
216    }
217
218    let mut out = String::new();
219    let mut ai = 1usize;
220    let mut chars = fmt.chars().peekable();
221    while let Some(c) = chars.next() {
222        if c != '%' {
223            out.push(c);
224            continue;
225        }
226        let Some(&spec) = chars.peek() else {
227            out.push('%');
228            break;
229        };
230        if spec == '%' {
231            out.push('%');
232            chars.next();
233            continue;
234        }
235        if !matches!(spec, 's' | 'd' | 'i' | 'f' | 'j' | 'o' | 'O' | 'c') || ai >= args.len() {
236            out.push('%');
237            continue;
238        }
239        chars.next();
240        let arg = &args[ai];
241        ai += 1;
242        match spec {
243            // Node's %s renders a BigInt with the trailing `n` (unlike String()).
244            's' => {
245                let s = with_host(|h| match h.get(arg) {
246                    Some(JsObj::BigInt(b)) => format!("{b}n"),
247                    _ => h.str_of(arg),
248                });
249                out.push_str(&s);
250            }
251            'd' | 'i' => {
252                let n = with_host(|h| h.to_number(arg));
253                out.push_str(&if n.is_nan() {
254                    "NaN".into()
255                } else {
256                    (n.trunc() as i64).to_string()
257                });
258            }
259            'f' => out.push_str(&crate::host::fmt_number(with_host(|h| h.to_number(arg)))),
260            'j' => {
261                let s = crate::builtins::call_builtin_function("JSON.stringify", vec![arg.clone()])
262                    .ok()
263                    .map(|v| with_host(|h| h.str_of(&v)))
264                    .unwrap_or_else(|| "undefined".into());
265                out.push_str(&s);
266            }
267            'o' | 'O' => out.push_str(&with_host(|h| h.inspect(arg))),
268            'c' => {} // CSS directive: consumes the arg, emits nothing.
269            _ => {}
270        }
271    }
272    // Append remaining arguments.
273    for a in &args[ai..] {
274        out.push(' ');
275        out.push_str(&with_host(|h| h.console_format(a)));
276    }
277    out
278}
279
280// ── promisify / callbackify ──────────────────────────────────────────────────
281// These build a REAL JS closure by compiling a factory expression and invoking it
282// with the wrapped function — the same re-entrant nested-run path `vm.runInThisContext`
283// uses — so the returned value is an ordinary user function (`typeof === "function"`).
284
285/// Compile a single JS expression and run it on the current host, returning its
286/// completion value. Re-entrant-safe (mirrors `vm::run_code`).
287fn run_completion(src: &str) -> Result<Value, String> {
288    let prog = crate::compile_completion(src)?;
289    let chunk = crate::load_merged(prog);
290    crate::host::run_chunk_on(chunk)
291}
292
293const PROMISIFY_SRC: &str = "(function(original){\n\
294  return function(){\n\
295    var self = this;\n\
296    var args = Array.prototype.slice.call(arguments);\n\
297    return new Promise(function(resolve, reject){\n\
298      args.push(function(err, value){ if (err) reject(err); else resolve(value); });\n\
299      original.apply(self, args);\n\
300    });\n\
301  };\n\
302})";
303
304const CALLBACKIFY_SRC: &str = "(function(original){\n\
305  return function(){\n\
306    var self = this;\n\
307    var args = Array.prototype.slice.call(arguments);\n\
308    var cb = args.pop();\n\
309    Promise.resolve(original.apply(self, args)).then(\n\
310      function(value){ cb.call(self, null, value); },\n\
311      function(err){ cb.call(self, err || new Error('Promise was rejected with a falsy value')); }\n\
312    );\n\
313  };\n\
314})";
315
316/// `util.promisify(fn)` → a function returning a Promise that resolves with the
317/// callback's value (rejecting on its error argument).
318fn promisify(args: &[Value]) -> Result<Value, String> {
319    let orig = args.first().cloned().unwrap_or(Value::Undef);
320    if !with_host(|h| crate::host::is_callable(h, &orig)) {
321        return Err(
322            "TypeError [ERR_INVALID_ARG_TYPE]: The \"original\" argument must be of type function"
323                .into(),
324        );
325    }
326    let factory = run_completion(PROMISIFY_SRC)?;
327    crate::host::invoke(&factory, vec![orig], None)
328}
329
330/// `util.callbackify(fn)` → a function taking a trailing `(err, value)` callback,
331/// invoked from the async function's resolved/rejected result.
332fn callbackify(args: &[Value]) -> Result<Value, String> {
333    let orig = args.first().cloned().unwrap_or(Value::Undef);
334    if !with_host(|h| crate::host::is_callable(h, &orig)) {
335        return Err(
336            "TypeError [ERR_INVALID_ARG_TYPE]: The \"original\" argument must be of type function"
337                .into(),
338        );
339    }
340    let factory = run_completion(CALLBACKIFY_SRC)?;
341    crate::host::invoke(&factory, vec![orig], None)
342}
343
344// ── debuglog ─────────────────────────────────────────────────────────────────
345
346const DEBUGLOG_ENABLED_SRC: &str = "(function(prefix){\n\
347  var util = require('util');\n\
348  return function(){\n\
349    console.error(prefix + ' ' + util.format.apply(null, arguments));\n\
350  };\n\
351})";
352
353/// `util.debuglog(section)` → a logging function gated by the `NODE_DEBUG` env var.
354/// When the section is not enabled, a no-op function is returned (Node's contract).
355fn debuglog(args: &[Value]) -> Result<Value, String> {
356    let section = super::arg_str(args, 0);
357    if debuglog_enabled(&section) {
358        let prefix = format!("{} {}:", section.to_uppercase(), std::process::id());
359        let factory = run_completion(DEBUGLOG_ENABLED_SRC)?;
360        let pfx = with_host(|h| h.new_str(prefix));
361        crate::host::invoke(&factory, vec![pfx], None)
362    } else {
363        run_completion("(function(){})")
364    }
365}
366
367/// Whether `NODE_DEBUG` enables `section` (comma/space-separated, case-insensitive,
368/// `*` wildcards allowed — matching Node's env parsing).
369fn debuglog_enabled(section: &str) -> bool {
370    let Ok(env) = std::env::var("NODE_DEBUG") else {
371        return false;
372    };
373    let sec = section.to_uppercase();
374    env.split(|c: char| c == ',' || c.is_whitespace())
375        .filter(|s| !s.is_empty())
376        .any(|pat| {
377            let pat = pat.to_uppercase();
378            if pat.contains('*') {
379                wildcard_match(&pat, &sec)
380            } else {
381                pat == sec
382            }
383        })
384}
385
386/// Minimal glob match (`*` = any run) for `NODE_DEBUG` section patterns.
387fn wildcard_match(pat: &str, s: &str) -> bool {
388    let parts: Vec<&str> = pat.split('*').collect();
389    if parts.len() == 1 {
390        return pat == s;
391    }
392    let mut pos = 0usize;
393    for (i, part) in parts.iter().enumerate() {
394        if part.is_empty() {
395            continue;
396        }
397        if i == 0 {
398            if !s[pos..].starts_with(part) {
399                return false;
400            }
401            pos += part.len();
402        } else if i == parts.len() - 1 {
403            return s[pos..].ends_with(part);
404        } else if let Some(idx) = s[pos..].find(part) {
405            pos += idx + part.len();
406        } else {
407            return false;
408        }
409    }
410    true
411}
412
413// ── stripVTControlCharacters ─────────────────────────────────────────────────
414
415/// Remove ANSI/VT escape sequences: two-char escapes, CSI (`ESC [ … final`),
416/// and OSC (`ESC ] … BEL|ST`), including the C1 CSI introducer `›`.
417fn strip_vt(s: &str) -> String {
418    let mut out = String::with_capacity(s.len());
419    let mut chars = s.chars().peekable();
420    while let Some(c) = chars.next() {
421        if c == '\u{9b}' {
422            // C1 CSI: consume params/intermediates until a final byte.
423            while let Some(&n) = chars.peek() {
424                chars.next();
425                if ('\u{40}'..='\u{7e}').contains(&n) {
426                    break;
427                }
428            }
429            continue;
430        }
431        if c != '\u{1b}' {
432            out.push(c);
433            continue;
434        }
435        match chars.peek() {
436            Some('[') => {
437                chars.next();
438                while let Some(&n) = chars.peek() {
439                    chars.next();
440                    if ('\u{40}'..='\u{7e}').contains(&n) {
441                        break;
442                    }
443                }
444            }
445            Some(']') => {
446                chars.next();
447                while let Some(&n) = chars.peek() {
448                    if n == '\u{7}' {
449                        chars.next();
450                        break;
451                    }
452                    if n == '\u{1b}' {
453                        chars.next();
454                        if chars.peek() == Some(&'\\') {
455                            chars.next();
456                        }
457                        break;
458                    }
459                    chars.next();
460                }
461            }
462            Some(_) => {
463                chars.next();
464            }
465            None => {}
466        }
467    }
468    out
469}
470
471// ── styleText ────────────────────────────────────────────────────────────────
472
473/// The format name(s) passed to `styleText` (a string or an array of strings).
474fn style_names(v: &Value) -> Vec<String> {
475    with_host(|h| match h.get(v) {
476        Some(JsObj::Array(items)) => items.iter().map(|x| h.str_of(x)).collect(),
477        _ => vec![h.str_of(v)],
478    })
479}
480
481/// `(open, close)` SGR parameter numbers for a `util.inspect.colors` name.
482fn style_codes(name: &str) -> Option<(u16, u16)> {
483    Some(match name {
484        "reset" => (0, 0),
485        "bold" => (1, 22),
486        "dim" => (2, 22),
487        "italic" => (3, 23),
488        "underline" => (4, 24),
489        "blink" => (5, 25),
490        "inverse" => (7, 27),
491        "hidden" => (8, 28),
492        "strikethrough" => (9, 29),
493        "doubleunderline" => (21, 24),
494        "overline" => (53, 55),
495        "black" => (30, 39),
496        "red" => (31, 39),
497        "green" => (32, 39),
498        "yellow" => (33, 39),
499        "blue" => (34, 39),
500        "magenta" => (35, 39),
501        "cyan" => (36, 39),
502        "white" => (37, 39),
503        "gray" | "grey" | "blackBright" => (90, 39),
504        "redBright" => (91, 39),
505        "greenBright" => (92, 39),
506        "yellowBright" => (93, 39),
507        "blueBright" => (94, 39),
508        "magentaBright" => (95, 39),
509        "cyanBright" => (96, 39),
510        "whiteBright" => (97, 39),
511        "bgBlack" => (40, 49),
512        "bgRed" => (41, 49),
513        "bgGreen" => (42, 49),
514        "bgYellow" => (43, 49),
515        "bgBlue" => (44, 49),
516        "bgMagenta" => (45, 49),
517        "bgCyan" => (46, 49),
518        "bgWhite" => (47, 49),
519        "bgGray" | "bgGrey" | "bgBlackBright" => (100, 49),
520        "bgRedBright" => (101, 49),
521        "bgGreenBright" => (102, 49),
522        "bgYellowBright" => (103, 49),
523        "bgBlueBright" => (104, 49),
524        "bgMagentaBright" => (105, 49),
525        "bgCyanBright" => (106, 49),
526        "bgWhiteBright" => (107, 49),
527        _ => return None,
528    })
529}
530
531/// `util.styleText(format, text)` — wrap `text` in the SGR codes for `format`
532/// (a color/modifier name or an array of them). `"none"` is a no-op passthrough.
533fn style_text(args: &[Value]) -> Result<Value, String> {
534    let fmt = args.first().cloned().unwrap_or(Value::Undef);
535    let names = style_names(&fmt);
536    let mut result = super::arg_str(args, 1);
537    for name in &names {
538        if name == "none" {
539            continue;
540        }
541        let (open, close) = style_codes(name).ok_or_else(|| {
542            format!(
543                "TypeError [ERR_INVALID_ARG_VALUE]: The argument 'format' must be a valid \
544                 util.inspect.colors key. Received '{name}'"
545            )
546        })?;
547        result = format!("\u{1b}[{open}m{result}\u{1b}[{close}m");
548    }
549    Ok(with_host(|h| h.new_str(result)))
550}
551
552// ── getSystemError{Name,Message,Map} ─────────────────────────────────────────
553// Node's system error numbers are negative libuv errnos. We resolve them through
554// the platform's own `libc` constants so macOS/Linux each report their native
555// numbering, and pair each with libuv's canonical (lowercase) message string.
556
557/// `(name, platform errno, libuv message)` for the common POSIX errors. Only
558/// constants present on every supported Unix target are listed (cross-platform).
559const ERRNO_TABLE: &[(&str, i32, &str)] = &[
560    ("E2BIG", libc::E2BIG, "argument list too long"),
561    ("EACCES", libc::EACCES, "permission denied"),
562    ("EADDRINUSE", libc::EADDRINUSE, "address already in use"),
563    (
564        "EADDRNOTAVAIL",
565        libc::EADDRNOTAVAIL,
566        "address not available",
567    ),
568    (
569        "EAFNOSUPPORT",
570        libc::EAFNOSUPPORT,
571        "address family not supported",
572    ),
573    ("EAGAIN", libc::EAGAIN, "resource temporarily unavailable"),
574    ("EALREADY", libc::EALREADY, "connection already in progress"),
575    ("EBADF", libc::EBADF, "bad file descriptor"),
576    ("EBUSY", libc::EBUSY, "resource busy or locked"),
577    ("ECANCELED", libc::ECANCELED, "operation canceled"),
578    (
579        "ECONNABORTED",
580        libc::ECONNABORTED,
581        "software caused connection abort",
582    ),
583    ("ECONNREFUSED", libc::ECONNREFUSED, "connection refused"),
584    ("ECONNRESET", libc::ECONNRESET, "connection reset by peer"),
585    (
586        "EDESTADDRREQ",
587        libc::EDESTADDRREQ,
588        "destination address required",
589    ),
590    ("EEXIST", libc::EEXIST, "file already exists"),
591    (
592        "EFAULT",
593        libc::EFAULT,
594        "bad address in system call argument",
595    ),
596    ("EFBIG", libc::EFBIG, "file too large"),
597    ("EHOSTDOWN", libc::EHOSTDOWN, "host is down"),
598    ("EHOSTUNREACH", libc::EHOSTUNREACH, "host is unreachable"),
599    ("EINTR", libc::EINTR, "interrupted system call"),
600    ("EINVAL", libc::EINVAL, "invalid argument"),
601    ("EIO", libc::EIO, "i/o error"),
602    ("EISCONN", libc::EISCONN, "socket is already connected"),
603    ("EISDIR", libc::EISDIR, "illegal operation on a directory"),
604    ("ELOOP", libc::ELOOP, "too many symbolic links encountered"),
605    ("EMFILE", libc::EMFILE, "too many open files"),
606    ("EMLINK", libc::EMLINK, "too many links"),
607    ("EMSGSIZE", libc::EMSGSIZE, "message too long"),
608    ("ENAMETOOLONG", libc::ENAMETOOLONG, "name too long"),
609    ("ENETDOWN", libc::ENETDOWN, "network is down"),
610    ("ENETUNREACH", libc::ENETUNREACH, "network is unreachable"),
611    ("ENFILE", libc::ENFILE, "file table overflow"),
612    ("ENOBUFS", libc::ENOBUFS, "no buffer space available"),
613    ("ENODEV", libc::ENODEV, "no such device"),
614    ("ENOENT", libc::ENOENT, "no such file or directory"),
615    ("ENOMEM", libc::ENOMEM, "not enough memory"),
616    ("ENOPROTOOPT", libc::ENOPROTOOPT, "protocol not available"),
617    ("ENOSPC", libc::ENOSPC, "no space left on device"),
618    ("ENOSYS", libc::ENOSYS, "function not implemented"),
619    ("ENOTCONN", libc::ENOTCONN, "socket is not connected"),
620    ("ENOTDIR", libc::ENOTDIR, "not a directory"),
621    ("ENOTEMPTY", libc::ENOTEMPTY, "directory not empty"),
622    ("ENOTSOCK", libc::ENOTSOCK, "socket operation on non-socket"),
623    ("ENXIO", libc::ENXIO, "no such device or address"),
624    (
625        "EOPNOTSUPP",
626        libc::EOPNOTSUPP,
627        "operation not supported on socket",
628    ),
629    (
630        "EOVERFLOW",
631        libc::EOVERFLOW,
632        "value too large for defined data type",
633    ),
634    ("EPERM", libc::EPERM, "operation not permitted"),
635    ("EPIPE", libc::EPIPE, "broken pipe"),
636    ("EPROTO", libc::EPROTO, "protocol error"),
637    (
638        "EPROTONOSUPPORT",
639        libc::EPROTONOSUPPORT,
640        "protocol not supported",
641    ),
642    (
643        "EPROTOTYPE",
644        libc::EPROTOTYPE,
645        "protocol wrong type for socket",
646    ),
647    ("ERANGE", libc::ERANGE, "result too large"),
648    ("EROFS", libc::EROFS, "read-only file system"),
649    (
650        "ESHUTDOWN",
651        libc::ESHUTDOWN,
652        "cannot send after transport endpoint shutdown",
653    ),
654    ("ESPIPE", libc::ESPIPE, "invalid seek"),
655    ("ESRCH", libc::ESRCH, "no such process"),
656    ("ETIMEDOUT", libc::ETIMEDOUT, "connection timed out"),
657    ("ETXTBSY", libc::ETXTBSY, "text file is busy"),
658    ("EXDEV", libc::EXDEV, "cross-device link not permitted"),
659];
660
661/// Normalize a `getSystemError*` argument (a negative libuv errno) to a positive
662/// platform errno for table lookup.
663fn errno_of(err: f64) -> i32 {
664    if err < 0.0 {
665        (-err) as i32
666    } else {
667        err as i32
668    }
669}
670
671fn errno_name(e: i32) -> Option<&'static str> {
672    ERRNO_TABLE
673        .iter()
674        .find(|(_, code, _)| *code == e)
675        .map(|(n, _, _)| *n)
676}
677
678fn errno_message(e: i32) -> Option<&'static str> {
679    ERRNO_TABLE
680        .iter()
681        .find(|(_, code, _)| *code == e)
682        .map(|(_, _, m)| *m)
683}
684
685/// `util.getSystemErrorMap()` → a `Map` of negative errno → `[name, message]`.
686fn system_error_map() -> Value {
687    with_host(|h| {
688        let mut entries = indexmap::IndexMap::new();
689        for (name, code, msg) in ERRNO_TABLE {
690            let key_val = Value::Float(-(*code as f64));
691            let name_v = h.new_str(*name);
692            let msg_v = h.new_str(*msg);
693            let pair = h.new_array(vec![name_v, msg_v]);
694            let key = crate::host::map_key(h, &key_val);
695            entries.insert(key, (key_val, pair));
696        }
697        h.alloc(JsObj::Map {
698            entries,
699            weak: false,
700        })
701    })
702}
703
704// ── parseArgs ────────────────────────────────────────────────────────────────
705
706/// A parsed option value (or list, when `multiple` is set).
707enum Slot {
708    Bool(bool),
709    Str(String),
710    ListBool(Vec<bool>),
711    ListStr(Vec<String>),
712}
713
714/// The declared config for one option.
715struct OptCfg {
716    long: String,
717    is_string: bool,
718    multiple: bool,
719    short: Option<String>,
720    default: Option<Value>,
721}
722
723/// `util.parseArgs(config)` → `{ values, positionals }`. Implements the documented
724/// short/long/`--`/`=`/grouped-short algorithm. `config.tokens` is not emitted.
725fn parse_args(config_args: &[Value]) -> Result<Value, String> {
726    let config = config_args.first().cloned().unwrap_or(Value::Undef);
727    let tokens = read_arg_tokens(&config);
728    let strict = read_bool_prop(&config, "strict", true);
729    let allow_positionals = read_bool_prop(&config, "allowPositionals", false);
730    let allow_negative = read_bool_prop(&config, "allowNegative", false);
731    let opts = read_options(&config);
732
733    let lookup_long = |name: &str| opts.iter().find(|o| o.long == name);
734    let lookup_short = |c: &str| opts.iter().find(|o| o.short.as_deref() == Some(c));
735    let is_bool_long = |name: &str| opts.iter().any(|o| o.long == name && !o.is_string);
736
737    let mut values: IndexMap<String, Slot> = IndexMap::new();
738    let mut positionals: Vec<String> = Vec::new();
739
740    let mut i = 0usize;
741    while i < tokens.len() {
742        let tok = tokens[i].clone();
743        if tok == "--" {
744            for t in &tokens[i + 1..] {
745                positionals.push(t.clone());
746            }
747            break;
748        }
749        if let Some(rest) = tok.strip_prefix("--") {
750            let (raw_name, inline) = match rest.split_once('=') {
751                Some((n, v)) => (n.to_string(), Some(v.to_string())),
752                None => (rest.to_string(), None),
753            };
754            // `--no-foo` negation for boolean options.
755            let (name, negate) = match raw_name.strip_prefix("no-") {
756                Some(base) if allow_negative && is_bool_long(base) => (base.to_string(), true),
757                _ => (raw_name, false),
758            };
759            match lookup_long(&name) {
760                None if strict => {
761                    return Err(format!(
762                        "Error [ERR_PARSE_ARGS_UNKNOWN_OPTION]: Unknown option '--{name}'"
763                    ))
764                }
765                None => {
766                    // Lenient: record as a boolean flag.
767                    store(&mut values, &name, Slot::Bool(true), false);
768                }
769                Some(cfg) if cfg.is_string => {
770                    let val = match inline {
771                        Some(v) => v,
772                        None => {
773                            i += 1;
774                            tokens.get(i).cloned().ok_or_else(|| {
775                                format!(
776                                    "Error [ERR_PARSE_ARGS_INVALID_OPTION_VALUE]: \
777                                     Option '--{name} <value>' argument missing"
778                                )
779                            })?
780                        }
781                    };
782                    store(&mut values, &cfg.long, Slot::Str(val), cfg.multiple);
783                }
784                Some(cfg) => {
785                    if inline.is_some() && strict {
786                        return Err(format!(
787                            "Error [ERR_PARSE_ARGS_INVALID_OPTION_VALUE]: \
788                             Option '--{}' does not take an argument",
789                            cfg.long
790                        ));
791                    }
792                    store(&mut values, &cfg.long, Slot::Bool(!negate), cfg.multiple);
793                }
794            }
795        } else if tok.len() > 1 && tok.starts_with('-') {
796            let chars: Vec<char> = tok[1..].chars().collect();
797            let mut ci = 0usize;
798            while ci < chars.len() {
799                let short = chars[ci].to_string();
800                match lookup_short(&short) {
801                    None if strict => {
802                        return Err(format!(
803                            "Error [ERR_PARSE_ARGS_UNKNOWN_OPTION]: Unknown option '-{short}'"
804                        ))
805                    }
806                    None => {
807                        store(&mut values, &short, Slot::Bool(true), false);
808                        ci += 1;
809                    }
810                    Some(cfg) if cfg.is_string => {
811                        let remainder: String = chars[ci + 1..].iter().collect();
812                        let val = if !remainder.is_empty() {
813                            remainder
814                        } else {
815                            i += 1;
816                            tokens.get(i).cloned().ok_or_else(|| {
817                                format!(
818                                    "Error [ERR_PARSE_ARGS_INVALID_OPTION_VALUE]: \
819                                     Option '-{short}, --{} <value>' argument missing",
820                                    cfg.long
821                                )
822                            })?
823                        };
824                        store(&mut values, &cfg.long, Slot::Str(val), cfg.multiple);
825                        break;
826                    }
827                    Some(cfg) => {
828                        store(&mut values, &cfg.long, Slot::Bool(true), cfg.multiple);
829                        ci += 1;
830                    }
831                }
832            }
833        } else {
834            if !allow_positionals && strict {
835                return Err(format!(
836                    "Error [ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL]: \
837                     Unexpected argument '{tok}'. This command does not take positional arguments"
838                ));
839            }
840            positionals.push(tok);
841        }
842        i += 1;
843    }
844
845    // Apply declared defaults for options that were never provided.
846    for cfg in &opts {
847        if !values.contains_key(&cfg.long) {
848            if let Some(def) = &cfg.default {
849                store_default(&mut values, cfg, def.clone());
850            }
851        }
852    }
853
854    Ok(build_parse_result(values, positionals))
855}
856
857/// Insert/append a parsed value under `name`, honoring `multiple`.
858fn store(values: &mut IndexMap<String, Slot>, name: &str, slot: Slot, multiple: bool) {
859    if !multiple {
860        values.insert(name.to_string(), slot);
861        return;
862    }
863    match values.get_mut(name) {
864        Some(Slot::ListBool(v)) => {
865            if let Slot::Bool(b) = slot {
866                v.push(b);
867            }
868        }
869        Some(Slot::ListStr(v)) => {
870            if let Slot::Str(s) = slot {
871                v.push(s);
872            }
873        }
874        _ => {
875            let init = match slot {
876                Slot::Bool(b) => Slot::ListBool(vec![b]),
877                Slot::Str(s) => Slot::ListStr(vec![s]),
878                other => other,
879            };
880            values.insert(name.to_string(), init);
881        }
882    }
883}
884
885/// Seed a default value (already a JS `Value`) for an unset option.
886fn store_default(values: &mut IndexMap<String, Slot>, cfg: &OptCfg, def: Value) {
887    // A `multiple` default is expected to be an array; a scalar default is stored
888    // directly. We coerce through the option's declared type.
889    let slot = with_host(|h| match h.get(&def) {
890        Some(JsObj::Array(items)) => {
891            if cfg.is_string {
892                Slot::ListStr(items.iter().map(|v| h.str_of(v)).collect())
893            } else {
894                Slot::ListBool(items.iter().map(|v| h.truthy(v)).collect())
895            }
896        }
897        _ if cfg.is_string => Slot::Str(h.str_of(&def)),
898        _ => Slot::Bool(h.truthy(&def)),
899    });
900    values.insert(cfg.long.clone(), slot);
901}
902
903/// Materialize `{ values, positionals }` in a single host borrow.
904fn build_parse_result(values: IndexMap<String, Slot>, positionals: Vec<String>) -> Value {
905    with_host(|h| {
906        let mut vobj = IndexMap::new();
907        for (k, slot) in values {
908            let v = match slot {
909                Slot::Bool(b) => Value::Bool(b),
910                Slot::Str(s) => h.new_str(s),
911                Slot::ListBool(items) => {
912                    let arr = items.into_iter().map(Value::Bool).collect();
913                    h.new_array(arr)
914                }
915                Slot::ListStr(items) => {
916                    let arr = items.into_iter().map(|s| h.new_str(s)).collect();
917                    h.new_array(arr)
918                }
919            };
920            vobj.insert(k, v);
921        }
922        let values_v = h.new_object(vobj);
923        let pos: Vec<Value> = positionals.into_iter().map(|s| h.new_str(s)).collect();
924        let positionals_v = h.new_array(pos);
925        let mut out = IndexMap::new();
926        out.insert("values".to_string(), values_v);
927        out.insert("positionals".to_string(), positionals_v);
928        h.new_object(out)
929    })
930}
931
932/// `config.args` as strings, or `process.argv.slice(2)` (the runtime's own tail).
933fn read_arg_tokens(config: &Value) -> Vec<String> {
934    let arr = crate::builtins::get_property(config, "args").unwrap_or(Value::Undef);
935    let from_config = with_host(|h| match h.get(&arr) {
936        Some(JsObj::Array(items)) => Some(items.iter().map(|v| h.str_of(v)).collect::<Vec<_>>()),
937        _ => None,
938    });
939    from_config.unwrap_or_else(|| std::env::args().skip(2).collect())
940}
941
942/// Read a boolean config property, defaulting when absent/undefined.
943fn read_bool_prop(config: &Value, name: &str, default: bool) -> bool {
944    match crate::builtins::get_property(config, name) {
945        Ok(Value::Undef) | Err(_) => default,
946        Ok(v) => with_host(|h| h.truthy(&v)),
947    }
948}
949
950/// Read `config.options` into an ordered list of `OptCfg`.
951fn read_options(config: &Value) -> Vec<OptCfg> {
952    let options = crate::builtins::get_property(config, "options").unwrap_or(Value::Undef);
953    let keys: Vec<String> = with_host(|h| match h.get(&options) {
954        Some(JsObj::Object(m)) => m.keys().filter(|k| !k.starts_with("@@")).cloned().collect(),
955        _ => Vec::new(),
956    });
957    keys.into_iter()
958        .map(|long| {
959            let spec = crate::builtins::get_property(&options, &long).unwrap_or(Value::Undef);
960            let type_str = crate::builtins::get_property(&spec, "type")
961                .ok()
962                .map(|v| with_host(|h| h.str_of(&v)))
963                .unwrap_or_default();
964            let short = match crate::builtins::get_property(&spec, "short") {
965                Ok(Value::Undef) | Err(_) => None,
966                Ok(v) => Some(with_host(|h| h.str_of(&v))),
967            };
968            let multiple = read_bool_prop(&spec, "multiple", false);
969            let default = match crate::builtins::get_property(&spec, "default") {
970                Ok(Value::Undef) | Err(_) => None,
971                Ok(v) => Some(v),
972            };
973            OptCfg {
974                long,
975                is_string: type_str == "string",
976                multiple,
977                short,
978                default,
979            }
980        })
981        .collect()
982}
983
984// ── parseEnv ─────────────────────────────────────────────────────────────────
985// Faithful port of Node's C++ `Dotenv::ParseContent` (src/node_dotenv.cc): CRLF
986// normalization, `#`/blank-line skipping, `export ` prefix stripping, single /
987// double / backtick quotes (`\n` expanded only inside double quotes), unquoted
988// inline `#` comments, unterminated quotes, and last-wins on duplicate keys.
989// Node emits the keys sorted, so we `sort_keys` before materializing.
990
991/// Trim ASCII whitespace (space, tab, newline) from both ends, matching Node's
992/// `trim_spaces` (which trims only `" \t\n"`).
993fn env_trim(s: &str) -> &str {
994    s.trim_matches(|c: char| c == ' ' || c == '\t' || c == '\n')
995}
996
997/// `util.parseEnv(content)` → an object of parsed `KEY=VALUE` assignments.
998fn parse_env(input: &str) -> Value {
999    let lines = input.replace('\r', "");
1000    let mut pairs: IndexMap<String, String> = IndexMap::new();
1001    let mut content: &str = env_trim(&lines);
1002
1003    while !content.is_empty() {
1004        let first = content.as_bytes()[0];
1005        // Skip blank lines and full-line comments.
1006        if first == b'\n' || first == b'#' {
1007            match content.find('\n') {
1008                Some(nl) => content = &content[nl + 1..],
1009                None => content = "",
1010            }
1011            continue;
1012        }
1013        // Next `=` or newline: a newline first means the line has no assignment.
1014        let Some(eq_or_nl) = content.find(['=', '\n']) else {
1015            break;
1016        };
1017        if content.as_bytes()[eq_or_nl] == b'\n' {
1018            content = env_trim(&content[eq_or_nl + 1..]);
1019            continue;
1020        }
1021        // Key up to `=`.
1022        let mut key = env_trim(&content[..eq_or_nl]);
1023        content = &content[eq_or_nl + 1..];
1024        // `KEY=` (empty value).
1025        if content.is_empty() || content.as_bytes()[0] == b'\n' {
1026            pairs.insert(key.to_string(), String::new());
1027            continue;
1028        }
1029        content = env_trim(content);
1030        // Skip empty keys (`=value`, `"   "=value`).
1031        if key.is_empty() {
1032            continue;
1033        }
1034        // `export ` prefix.
1035        if let Some(rest) = key.strip_prefix("export ") {
1036            key = env_trim(rest);
1037        }
1038        if content.is_empty() {
1039            pairs.insert(key.to_string(), String::new());
1040            break;
1041        }
1042        let vfirst = content.as_bytes()[0];
1043        // Double-quoted value: expand literal `\n`, may span raw newlines.
1044        if vfirst == b'"' {
1045            if let Some(rel) = content[1..].find('"') {
1046                let closing = rel + 1;
1047                let value = content[1..closing].replace("\\n", "\n");
1048                pairs.insert(key.to_string(), value);
1049                match content[closing + 1..].find('\n') {
1050                    Some(nl) => content = &content[closing + 1 + nl + 1..],
1051                    None => content = "",
1052                }
1053                continue;
1054            }
1055            // No closing quote — fall through to the generic quote handler.
1056        }
1057        // Single / double / backtick quoted value (no escape expansion).
1058        if vfirst == b'\'' || vfirst == b'"' || vfirst == b'`' {
1059            match content[1..].find(vfirst as char) {
1060                None => match content.find('\n') {
1061                    Some(nl) => {
1062                        pairs.insert(key.to_string(), content[..nl].to_string());
1063                        content = &content[nl + 1..];
1064                    }
1065                    None => {
1066                        pairs.insert(key.to_string(), content.to_string());
1067                        break;
1068                    }
1069                },
1070                Some(rel) => {
1071                    let closing = rel + 1;
1072                    pairs.insert(key.to_string(), content[1..closing].to_string());
1073                    match content[closing + 1..].find('\n') {
1074                        Some(nl) => content = &content[closing + 1 + nl + 1..],
1075                        None => content = "",
1076                    }
1077                    continue;
1078                }
1079            }
1080        } else {
1081            // Unquoted value: up to the newline, `#` starts an inline comment, trim.
1082            let (raw, next) = match content.find('\n') {
1083                Some(nl) => (&content[..nl], &content[nl + 1..]),
1084                None => (content, ""),
1085            };
1086            let value = match raw.find('#') {
1087                Some(h) => &raw[..h],
1088                None => raw,
1089            };
1090            pairs.insert(key.to_string(), env_trim(value).to_string());
1091            content = next;
1092        }
1093        content = env_trim(content);
1094    }
1095
1096    pairs.sort_keys();
1097    with_host(|h| {
1098        let mut m = IndexMap::new();
1099        for (k, v) in pairs {
1100            let val = h.new_str(v);
1101            m.insert(k, val);
1102        }
1103        h.new_object(m)
1104    })
1105}
1106
1107// ── MIMEType / MIMEParams ────────────────────────────────────────────────────
1108// Faithful port of Node's `lib/internal/mime.js`. A `MIMEType` is a native object
1109// tagged `@@native = "MIMEType"` with `type`/`subtype`/`essence` data properties
1110// and a `params` `MIMEParams` instance; a `MIMEParams` is tagged `@@native =
1111// "MIMEParams"` with its ordered unique `(name, value)` pairs in a hidden
1112// `@@pairs` array (mirrors the URLSearchParams representation).
1113
1114/// An HTTP token code point (`NOT_HTTP_TOKEN_CODE_POINT` inverse).
1115fn is_token_char(c: char) -> bool {
1116    c.is_ascii_alphanumeric()
1117        || matches!(
1118            c,
1119            '!' | '#'
1120                | '$'
1121                | '%'
1122                | '&'
1123                | '\''
1124                | '*'
1125                | '+'
1126                | '-'
1127                | '.'
1128                | '^'
1129                | '_'
1130                | '`'
1131                | '|'
1132                | '~'
1133        )
1134}
1135
1136/// An HTTP quoted-string code point (`NOT_HTTP_QUOTED_STRING_CODE_POINT` inverse):
1137/// tab, printable ASCII, and Latin-1 supplement.
1138fn is_quoted_string_char(c: char) -> bool {
1139    c == '\t' || ('\u{20}'..='\u{7e}').contains(&c) || ('\u{80}'..='\u{ff}').contains(&c)
1140}
1141
1142/// HTTP whitespace (`\r \n \t space`).
1143fn is_http_ws(c: char) -> bool {
1144    matches!(c, '\r' | '\n' | '\t' | ' ')
1145}
1146
1147/// Lowercase only ASCII `A-Z`, leaving other code points intact (Node's
1148/// `toASCIILower`).
1149fn ascii_lower(s: &str) -> String {
1150    s.to_ascii_lowercase()
1151}
1152
1153/// Build the `TypeError [ERR_INVALID_MIME_SYNTAX]` message (with the offending
1154/// index when known).
1155fn mime_syntax_err(part: &str, s: &str, index: Option<usize>) -> String {
1156    match index {
1157        Some(i) => format!(
1158            "TypeError [ERR_INVALID_MIME_SYNTAX]: The MIME syntax for a {part} in \"{s}\" is invalid at {i}"
1159        ),
1160        None => format!(
1161            "TypeError [ERR_INVALID_MIME_SYNTAX]: The MIME syntax for a {part} in \"{s}\" is invalid"
1162        ),
1163    }
1164}
1165
1166/// Parse `type/subtype` off the front of a MIME string; returns
1167/// `(type, subtype, remaining-params-string)`.
1168fn parse_type_and_subtype(s: &str) -> Result<(String, String, String), String> {
1169    let chars: Vec<char> = s.chars().collect();
1170    let n = chars.len();
1171    // Skip leading HTTP whitespace.
1172    let mut pos = 0;
1173    while pos < n && is_http_ws(chars[pos]) {
1174        pos += 1;
1175    }
1176    // Read the type up to `/`.
1177    let type_end = (pos..n).find(|&i| chars[i] == '/');
1178    let trimmed_type: String = match type_end {
1179        Some(e) => chars[pos..e].iter().collect(),
1180        None => chars[pos..].iter().collect(),
1181    };
1182    let type_invalid = trimmed_type.chars().position(|c| !is_token_char(c));
1183    if trimmed_type.is_empty() || type_invalid.is_some() || type_end.is_none() {
1184        return Err(mime_syntax_err("type", s, type_invalid));
1185    }
1186    let type_end = type_end.unwrap();
1187    pos = type_end + 1;
1188    let mime_type = ascii_lower(&trimmed_type);
1189    // Read the subtype up to `;`.
1190    let sub_end = (pos..n).find(|&i| chars[i] == ';');
1191    let raw_subtype: &[char] = match sub_end {
1192        Some(e) => &chars[pos..e],
1193        None => &chars[pos..],
1194    };
1195    let mut new_pos = pos + raw_subtype.len();
1196    if sub_end.is_some() {
1197        new_pos += 1;
1198    }
1199    // Trim trailing HTTP whitespace from the subtype only.
1200    let mut end = raw_subtype.len();
1201    while end > 0 && is_http_ws(raw_subtype[end - 1]) {
1202        end -= 1;
1203    }
1204    let trimmed_subtype: String = raw_subtype[..end].iter().collect();
1205    let sub_invalid = trimmed_subtype.chars().position(|c| !is_token_char(c));
1206    if trimmed_subtype.is_empty() || sub_invalid.is_some() {
1207        return Err(mime_syntax_err("subtype", s, sub_invalid));
1208    }
1209    let subtype = ascii_lower(&trimmed_subtype);
1210    let params: String = chars[new_pos.min(n)..].iter().collect();
1211    Ok((mime_type, subtype, params))
1212}
1213
1214/// Scan a quoted parameter value starting just after the opening `"` (Node's
1215/// `QUOTED_VALUE_PATTERN`). Returns `(matched_len, lone_backslash, closing_quote)`.
1216fn scan_quoted(chars: &[char], start: usize) -> (usize, bool, bool) {
1217    let n = chars.len();
1218    let mut i = start;
1219    let mut lone_backslash = false;
1220    let mut closing_quote = false;
1221    while i < n {
1222        match chars[i] {
1223            '\\' => {
1224                if i + 1 >= n {
1225                    lone_backslash = true;
1226                    i += 1;
1227                    break;
1228                }
1229                i += 2;
1230            }
1231            '"' => {
1232                closing_quote = true;
1233                i += 1;
1234                break;
1235            }
1236            _ => i += 1,
1237        }
1238    }
1239    (i - start, lone_backslash, closing_quote)
1240}
1241
1242/// Remove single `\` escapes (Node's `removeBackslashes`).
1243fn remove_backslashes(s: &[char]) -> String {
1244    let n = s.len();
1245    if n == 0 {
1246        return String::new();
1247    }
1248    let mut ret = String::new();
1249    let mut i = 0usize;
1250    while i < n - 1 {
1251        if s[i] == '\\' {
1252            i += 1;
1253            ret.push(s[i]);
1254        } else {
1255            ret.push(s[i]);
1256        }
1257        i += 1;
1258    }
1259    if i == n - 1 {
1260        ret.push(s[i]);
1261    }
1262    ret
1263}
1264
1265/// Parse a MIME parameter string into ordered, unique `(name, value)` pairs.
1266fn parse_mime_params(s: &str) -> Vec<(String, String)> {
1267    let chars: Vec<char> = s.chars().collect();
1268    let n = chars.len();
1269    // The source ends where trailing whitespace begins.
1270    let mut end_of_source = n;
1271    while end_of_source > 0 && is_http_ws(chars[end_of_source - 1]) {
1272        end_of_source -= 1;
1273    }
1274    let mut out: Vec<(String, String)> = Vec::new();
1275    let mut position = 0usize;
1276    while position < end_of_source {
1277        // Skip whitespace before the parameter name.
1278        while position < n && is_http_ws(chars[position]) {
1279            position += 1;
1280        }
1281        // Read the name up to `;`, `=`, or end.
1282        let mut after = position;
1283        while after < n && chars[after] != ';' && chars[after] != '=' {
1284            after += 1;
1285        }
1286        let name = ascii_lower(&chars[position..after].iter().collect::<String>());
1287        position = after;
1288        if position < end_of_source {
1289            let ch = chars[position];
1290            position += 1;
1291            // A `;` terminator means a value-less parameter — ignore it.
1292            if ch == ';' {
1293                continue;
1294            }
1295        }
1296        if position >= end_of_source {
1297            break;
1298        }
1299        let value = if chars[position] == '"' {
1300            // Quoted-string value.
1301            position += 1;
1302            let (matched_len, lone_backslash, closing_quote) = scan_quoted(&chars, position);
1303            let matched = &chars[position..position + matched_len];
1304            position += matched_len;
1305            let inside: &[char] = if lone_backslash || closing_quote {
1306                &matched[..matched.len().saturating_sub(1)]
1307            } else {
1308                matched
1309            };
1310            let mut v = remove_backslashes(inside);
1311            if lone_backslash {
1312                v.push('\\');
1313            }
1314            v
1315        } else {
1316            // Bare value up to `;`, trailing whitespace trimmed.
1317            let value_end = (position..n).find(|&i| chars[i] == ';').unwrap_or(n);
1318            let raw = &chars[position..value_end];
1319            position += raw.len();
1320            let mut end = raw.len();
1321            while end > 0 && is_http_ws(raw[end - 1]) {
1322                end -= 1;
1323            }
1324            let trimmed: String = raw[..end].iter().collect();
1325            if trimmed.is_empty() {
1326                // Node `continue`s here without the trailing `position++`, leaving
1327                // `position` on the `;` so the next iteration consumes it.
1328                continue;
1329            }
1330            trimmed
1331        };
1332        // Keep only valid, non-duplicate parameters (first value wins).
1333        let name_ok = !name.is_empty() && name.chars().all(is_token_char);
1334        let value_ok = value.chars().all(is_quoted_string_char);
1335        if name_ok && value_ok && !out.iter().any(|(k, _)| *k == name) {
1336            out.push((name, value));
1337        }
1338        position += 1;
1339    }
1340    out
1341}
1342
1343/// Serialize a parameter value: bare if it is a valid token, else a quoted string
1344/// with `"`/`\` escaped (Node's `encode`).
1345fn encode_param_value(value: &str) -> String {
1346    if value.is_empty() {
1347        return "\"\"".to_string();
1348    }
1349    if value.chars().all(is_token_char) {
1350        return value.to_string();
1351    }
1352    let mut escaped = String::with_capacity(value.len() + 2);
1353    for c in value.chars() {
1354        if c == '"' || c == '\\' {
1355            escaped.push('\\');
1356        }
1357        escaped.push(c);
1358    }
1359    format!("\"{escaped}\"")
1360}
1361
1362/// Serialize ordered pairs as `name=value;name2=value2` (MIMEParams `toString`).
1363fn serialize_mime_params(pairs: &[(String, String)]) -> String {
1364    let mut ret = String::new();
1365    for (k, v) in pairs {
1366        if !ret.is_empty() {
1367            ret.push(';');
1368        }
1369        ret.push_str(k);
1370        ret.push('=');
1371        ret.push_str(&encode_param_value(v));
1372    }
1373    ret
1374}
1375
1376/// Method names dispatched through `mime_params_instance_call` (for
1377/// `instance_has_method` wiring; `@@iterator` powers `for..of` / spread).
1378pub const MIME_PARAMS_METHODS: &[&str] = &[
1379    "get",
1380    "set",
1381    "has",
1382    "delete",
1383    "entries",
1384    "keys",
1385    "values",
1386    "toString",
1387    "toJSON",
1388    "@@iterator",
1389];
1390
1391/// Method names dispatched through `mime_type_instance_call`.
1392pub const MIME_TYPE_METHODS: &[&str] = &["toString", "toJSON"];
1393
1394/// Build a `MIMEParams` native object from ordered pairs.
1395fn make_mime_params(pairs: &[(String, String)]) -> Value {
1396    with_host(|h| {
1397        let items: Vec<Value> = pairs
1398            .iter()
1399            .map(|(k, v)| {
1400                let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
1401                h.new_array(kv)
1402            })
1403            .collect();
1404        let arr = h.new_array(items);
1405        let mut m = IndexMap::new();
1406        m.insert("@@native".into(), h.new_str("MIMEParams"));
1407        m.insert("@@pairs".into(), arr);
1408        h.new_object(m)
1409    })
1410}
1411
1412/// Read the ordered `(name, value)` pairs out of a `MIMEParams`.
1413fn mime_pairs_of(recv: &Value) -> Vec<(String, String)> {
1414    with_host(|h| {
1415        let items: Vec<Value> = match h.get(recv) {
1416            Some(JsObj::Object(p)) => match p.get("@@pairs").and_then(|a| h.get(a)) {
1417                Some(JsObj::Array(items)) => items.clone(),
1418                _ => Vec::new(),
1419            },
1420            _ => Vec::new(),
1421        };
1422        items
1423            .iter()
1424            .map(|it| match h.get(it) {
1425                Some(JsObj::Array(kv)) => {
1426                    let kv = kv.clone();
1427                    let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
1428                    let v = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
1429                    (k, v)
1430                }
1431                _ => (h.str_of(it), String::new()),
1432            })
1433            .collect()
1434    })
1435}
1436
1437/// Overwrite a `MIMEParams`' backing `@@pairs` array.
1438fn set_mime_pairs(recv: &Value, pairs: &[(String, String)]) {
1439    with_host(|h| {
1440        let items: Vec<Value> = pairs
1441            .iter()
1442            .map(|(k, v)| {
1443                let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
1444                h.new_array(kv)
1445            })
1446            .collect();
1447        let arr = h.new_array(items);
1448        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1449            p.insert("@@pairs".into(), arr);
1450        }
1451    });
1452}
1453
1454/// `new util.MIMEParams()` — an empty parameter set (Node's constructor takes no
1455/// arguments).
1456pub fn construct_mime_params(_args: &[Value]) -> Result<Value, String> {
1457    Ok(make_mime_params(&[]))
1458}
1459
1460/// `MIMEParams` instance methods.
1461pub fn mime_params_instance_call(
1462    recv: &Value,
1463    method: &str,
1464    args: &[Value],
1465) -> Result<Value, String> {
1466    match method {
1467        "get" => {
1468            let name = super::arg_str(args, 0);
1469            match mime_pairs_of(recv).into_iter().find(|(k, _)| *k == name) {
1470                Some((_, v)) => Ok(with_host(|h| h.new_str(v))),
1471                None => Ok(with_host(|h| h.null())),
1472            }
1473        }
1474        "has" => {
1475            let name = super::arg_str(args, 0);
1476            Ok(Value::Bool(
1477                mime_pairs_of(recv).iter().any(|(k, _)| *k == name),
1478            ))
1479        }
1480        "set" => {
1481            let name = super::arg_str(args, 0);
1482            let value = super::arg_str(args, 1);
1483            if let Some(i) = name.chars().position(|c| !is_token_char(c)) {
1484                return Err(mime_syntax_err("parameter name", &name, Some(i)));
1485            }
1486            if name.is_empty() {
1487                return Err(mime_syntax_err("parameter name", &name, None));
1488            }
1489            if let Some(i) = value.chars().position(|c| !is_quoted_string_char(c)) {
1490                return Err(mime_syntax_err("parameter value", &value, Some(i)));
1491            }
1492            let mut pairs = mime_pairs_of(recv);
1493            match pairs.iter_mut().find(|(k, _)| *k == name) {
1494                Some(slot) => slot.1 = value,
1495                None => pairs.push((name, value)),
1496            }
1497            set_mime_pairs(recv, &pairs);
1498            Ok(Value::Undef)
1499        }
1500        "delete" => {
1501            let name = super::arg_str(args, 0);
1502            let mut pairs = mime_pairs_of(recv);
1503            pairs.retain(|(k, _)| *k != name);
1504            set_mime_pairs(recv, &pairs);
1505            Ok(Value::Undef)
1506        }
1507        "keys" => {
1508            let pairs = mime_pairs_of(recv);
1509            Ok(with_host(|h| {
1510                let items = pairs.into_iter().map(|(k, _)| h.new_str(k)).collect();
1511                h.alloc(JsObj::Iter { items, idx: 0 })
1512            }))
1513        }
1514        "values" => {
1515            let pairs = mime_pairs_of(recv);
1516            Ok(with_host(|h| {
1517                let items = pairs.into_iter().map(|(_, v)| h.new_str(v)).collect();
1518                h.alloc(JsObj::Iter { items, idx: 0 })
1519            }))
1520        }
1521        "entries" | "@@iterator" => {
1522            let pairs = mime_pairs_of(recv);
1523            Ok(with_host(|h| {
1524                let items = pairs
1525                    .into_iter()
1526                    .map(|(k, v)| {
1527                        let kv = vec![h.new_str(k), h.new_str(v)];
1528                        h.new_array(kv)
1529                    })
1530                    .collect();
1531                h.alloc(JsObj::Iter { items, idx: 0 })
1532            }))
1533        }
1534        "toString" | "toJSON" => {
1535            let s = serialize_mime_params(&mime_pairs_of(recv));
1536            Ok(with_host(|h| h.new_str(s)))
1537        }
1538        _ => Err(crate::host::type_error(&format!(
1539            "mimeParams.{method} is not a function"
1540        ))),
1541    }
1542}
1543
1544/// `new util.MIMEType(input)` — parse into `type`/`subtype`/`essence`/`params`.
1545pub fn construct_mime_type(args: &[Value]) -> Result<Value, String> {
1546    // Node coerces the argument to a string first (`${string}`), so a missing
1547    // argument parses as the literal `"undefined"` (which then throws).
1548    let input = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1549    let (mime_type, subtype, params_str) = parse_type_and_subtype(&input)?;
1550    let essence = format!("{mime_type}/{subtype}");
1551    // Build the `MIMEParams` instance BEFORE the allocating `with_host` (never nest).
1552    let params = make_mime_params(&parse_mime_params(&params_str));
1553    Ok(with_host(|h| {
1554        let mut m = IndexMap::new();
1555        m.insert("@@native".into(), h.new_str("MIMEType"));
1556        m.insert("type".into(), h.new_str(mime_type));
1557        m.insert("subtype".into(), h.new_str(subtype));
1558        m.insert("essence".into(), h.new_str(essence));
1559        m.insert("params".into(), params);
1560        h.new_object(m)
1561    }))
1562}
1563
1564/// `MIMEType` instance methods. `type`/`subtype`/`essence`/`params` are data
1565/// properties read directly; `toString`/`toJSON` serialize live (reflecting any
1566/// `params` mutation).
1567pub fn mime_type_instance_call(
1568    recv: &Value,
1569    method: &str,
1570    _args: &[Value],
1571) -> Result<Value, String> {
1572    match method {
1573        "toString" | "toJSON" => {
1574            // Read the essence and the live params object under one borrow.
1575            let (essence, params) = with_host(|h| match h.get(recv) {
1576                Some(JsObj::Object(p)) => (
1577                    p.get("essence").map(|x| h.str_of(x)).unwrap_or_default(),
1578                    p.get("params").cloned().unwrap_or(Value::Undef),
1579                ),
1580                _ => (String::new(), Value::Undef),
1581            });
1582            let param_str = serialize_mime_params(&mime_pairs_of(&params));
1583            let out = if param_str.is_empty() {
1584                essence
1585            } else {
1586                format!("{essence};{param_str}")
1587            };
1588            Ok(with_host(|h| h.new_str(out)))
1589        }
1590        _ => Err(crate::host::type_error(&format!(
1591            "mimeType.{method} is not a function"
1592        ))),
1593    }
1594}