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