Skip to main content

nodejs/stdlib/
buffer.rs

1//! Node `Buffer` (global + `require('buffer').Buffer`). A Buffer is a plain
2//! object tagged `@@native = "Buffer"` whose bytes live in a hidden `@@bytes`
3//! array; `length` is an enumerable data property so `buf.length` reads directly.
4
5use super::{arg_str, from_base64, from_hex, to_base64, to_hex};
6use crate::host::{with_host, JsObj};
7use fusevm::Value;
8use indexmap::IndexMap;
9
10/// The statics on `Buffer`. node v26.7.0's
11/// `Object.getOwnPropertyNames(Buffer).filter(n => typeof Buffer[n] === 'function')`
12/// reports eleven; this is ten of them.
13///
14/// Three used to be missing, and the gap was not cosmetic: `safe-buffer`
15/// feature-detects `Buffer.from && Buffer.alloc && Buffer.allocUnsafe &&
16/// Buffer.allocUnsafeSlow` and, on a miss, exports its own legacy `SafeBuffer`
17/// wrapper instead of the real `buffer` module. express's `res.send` takes
18/// `Buffer` from `safe-buffer`, so every `res.json()` went through that wrapper
19/// and died on its `Buffer(arg, …)` call.
20///
21/// The eleventh, `copyBytesFrom`, is deliberately absent rather than faked. It
22/// copies a typed array's raw BYTES with `offset`/`length` counted in ELEMENTS,
23/// which needs a per-kind little-endian serializer for all nine element kinds
24/// (typed arrays are stored here as a `@@elems` array of NUMBERS, not bytes).
25/// Listing it without that would advertise a method the dispatcher cannot
26/// implement — the exact drift this list exists to prevent.
27pub const STATIC_METHODS: &[&str] = &[
28    "from",
29    "alloc",
30    "allocUnsafe",
31    "allocUnsafeSlow",
32    "concat",
33    "isBuffer",
34    "isEncoding",
35    "byteLength",
36    "compare",
37    "of",
38];
39
40/// The methods a `Buffer` instance answers — the surface `instance_call`
41/// dispatches, and the set installed as `@proto:Buffer:<m>` thunks on the real
42/// `Buffer.prototype` object.
43pub const INSTANCE_METHODS: &[&str] = &[
44    "toString",
45    "set",
46    "toJSON",
47    "equals",
48    "slice",
49    "subarray",
50    "readUInt8",
51    "includes",
52    "indexOf",
53    "lastIndexOf",
54    "write",
55    "copy",
56    "fill",
57    "compare",
58    "readUInt16BE",
59    "readUInt16LE",
60    "writeUInt8",
61    "writeInt8",
62    "writeInt16BE",
63    "writeInt16LE",
64    "readFloatBE",
65    "readFloatLE",
66    "writeFloatBE",
67    "writeFloatLE",
68    "readDoubleBE",
69    "readDoubleLE",
70    "writeDoubleBE",
71    "writeDoubleLE",
72    "readBigInt64BE",
73    "readBigInt64LE",
74    "readBigUInt64BE",
75    "readBigUInt64LE",
76    "writeBigInt64BE",
77    "writeBigInt64LE",
78    "writeBigUInt64BE",
79    "writeBigUInt64LE",
80    "readIntBE",
81    "readIntLE",
82    "readUIntBE",
83    "readUIntLE",
84    "writeIntBE",
85    "writeIntLE",
86    "writeUIntBE",
87    "writeUIntLE",
88    "writeUInt16BE",
89    "writeUInt16LE",
90    "readUInt32BE",
91    "readUInt32LE",
92    "readInt8",
93    "readInt16BE",
94    "readInt16LE",
95    "readInt32BE",
96    "readInt32LE",
97    "writeUInt32BE",
98    "writeUInt32LE",
99    "writeInt32BE",
100    "writeInt32LE",
101    "at",
102    "values",
103    "keys",
104    "entries",
105    "swap16",
106    "swap32",
107    "swap64",
108];
109
110/// Free functions of the `buffer` module itself (`require('buffer').atob`, …), as
111/// opposed to the `Buffer` constructor's static methods above. Needs the parent
112/// `"buffer"` routing arm (see final report).
113pub const MODULE_METHODS: &[&str] = &["atob", "btoa", "isAscii", "isUtf8", "transcode"];
114
115/// Dispatch a `require('buffer').<method>` free function.
116pub fn module_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
117    Some(match method {
118        // atob: base64 → a binary (latin1) string.
119        "atob" => {
120            let s = arg_str(args, 0);
121            let bytes = from_base64(&s);
122            let bin: String = bytes.iter().map(|b| *b as char).collect();
123            Ok(with_host(|h| h.new_str(bin)))
124        }
125        // btoa: a binary string → base64 (each char's low byte is one octet).
126        "btoa" => {
127            let s = arg_str(args, 0);
128            let bytes: Vec<u8> = s.chars().map(|c| c as u32 as u8).collect();
129            let b64 = to_base64(&bytes);
130            Ok(with_host(|h| h.new_str(b64)))
131        }
132        "isAscii" => {
133            let bytes = input_bytes(args.first());
134            Ok(Value::Bool(bytes.iter().all(|b| *b < 0x80)))
135        }
136        "isUtf8" => {
137            let bytes = input_bytes(args.first());
138            Ok(Value::Bool(std::str::from_utf8(&bytes).is_ok()))
139        }
140        // transcode(source, fromEnc, toEnc): re-encode bytes between utf8/latin1/
141        // ascii/utf16le (best-effort; hex/base64 are not transcode encodings).
142        "transcode" => {
143            let src = input_bytes(args.first());
144            let from = arg_str(args, 1);
145            let to = arg_str(args, 2);
146            let s = bytes_to_string(&src, &from);
147            let out = string_to_bytes(&s, &to);
148            Ok(from_bytes(&out))
149        }
150        _ => return None,
151    })
152}
153
154/// Raw bytes of a Buffer/Blob arg, or the UTF-8 bytes of a string arg.
155fn input_bytes(v: Option<&Value>) -> Vec<u8> {
156    match v {
157        None => Vec::new(),
158        Some(v) => {
159            if let Some(s) = with_host(|h| h.as_str(v)) {
160                s.into_bytes()
161            } else {
162                bytes_of(v)
163            }
164        }
165    }
166}
167
168/// Interpret bytes under `enc` as a Rust string (for `transcode`).
169fn bytes_to_string(bytes: &[u8], enc: &str) -> String {
170    match enc.to_ascii_lowercase().as_str() {
171        "ascii" | "latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
172        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
173            let units: Vec<u16> = bytes
174                .chunks_exact(2)
175                .map(|c| u16::from_le_bytes([c[0], c[1]]))
176                .collect();
177            String::from_utf16_lossy(&units)
178        }
179        _ => String::from_utf8_lossy(bytes).into_owned(),
180    }
181}
182
183/// Encode a Rust string into `enc` bytes (for `transcode`).
184///
185/// `transcode` is ICU, not `Buffer.from`, so its `ascii` arm SUBSTITUTES rather
186/// than truncates: node renders `transcode(Buffer.from('aÿ'),'utf8','ascii')` as
187/// `61 3f` — an unrepresentable character becomes `?`.
188fn string_to_bytes(s: &str, enc: &str) -> Vec<u8> {
189    match enc.to_ascii_lowercase().as_str() {
190        "ascii" => s
191            .chars()
192            .map(|c| if c.is_ascii() { c as u8 } else { b'?' })
193            .collect(),
194        "latin1" | "binary" => s.chars().map(|c| c as u32 as u8).collect(),
195        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
196            s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
197        }
198        _ => s.as_bytes().to_vec(),
199    }
200}
201
202// ── Blob / File ──────────────────────────────────────────────────────────────
203//
204// A `Blob` is a native object tagged `@@native = "Blob"` (a `File` is `"File"`)
205// whose bytes live in `@@bytes`, with `size`/`type` (and `File`'s `name`/
206// `lastModified`) as readable data properties. Needs parent construct/instance
207// wiring (see final report).
208
209/// Concatenate one Blob-part's bytes: a string contributes its UTF-8 bytes, a
210/// Buffer/Blob its raw bytes.
211fn part_bytes(v: &Value) -> Vec<u8> {
212    match with_host(|h| h.as_str(v)) {
213        Some(s) => s.into_bytes(),
214        None => bytes_of(v),
215    }
216}
217
218/// Gather the byte payload from a `BlobPart[]` (the first constructor argument).
219fn gather_parts(parts: &Value) -> Vec<u8> {
220    let items = with_host(|h| match h.get(parts) {
221        Some(JsObj::Array(it)) => it.clone(),
222        _ => Vec::new(),
223    });
224    let mut out = Vec::new();
225    for it in &items {
226        out.extend(part_bytes(it));
227    }
228    out
229}
230
231/// The `type` string from an options bag (`{ type }`), or "".
232fn opt_type(opts: Option<&Value>) -> String {
233    match opts {
234        Some(v) => with_host(|h| match h.get(v) {
235            Some(JsObj::Object(p)) => p.get("type").map(|x| h.str_of(x)).unwrap_or_default(),
236            _ => String::new(),
237        }),
238        None => String::new(),
239    }
240}
241
242/// Build a `Blob`/`File` native object with the shared `@@bytes`/`size`/`type`
243/// fields; `File` adds `name`/`lastModified`.
244fn build_blob(tag: &str, bytes: &[u8], typ: &str, extra: IndexMap<String, Value>) -> Value {
245    with_host(|h| {
246        let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
247        let mut m = IndexMap::new();
248        m.insert("@@native".into(), h.new_str(tag.to_string()));
249        m.insert("@@bytes".into(), arr);
250        m.insert("size".into(), Value::Float(bytes.len() as f64));
251        m.insert("type".into(), h.new_str(typ.to_string()));
252        for (k, v) in extra {
253            m.insert(k, v);
254        }
255        h.new_object(m)
256    })
257}
258
259/// `new Blob(parts[, options])`.
260pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
261    let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
262    let typ = opt_type(args.get(1));
263    Ok(build_blob("Blob", &bytes, &typ, IndexMap::new()))
264}
265
266/// `new File(parts, name[, options])`.
267pub fn construct_file(args: &[Value]) -> Result<Value, String> {
268    let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
269    let name = arg_str(args, 1);
270    let typ = opt_type(args.get(2));
271    // lastModified: options.lastModified or 0.
272    let last_modified = args
273        .get(2)
274        .map(|v| {
275            with_host(|h| match h.get(v) {
276                Some(JsObj::Object(p)) => {
277                    p.get("lastModified").map(|x| h.to_number(x)).unwrap_or(0.0)
278                }
279                _ => 0.0,
280            })
281        })
282        .unwrap_or(0.0);
283    let extra = with_host(|h| {
284        let mut m = IndexMap::new();
285        m.insert("name".to_string(), h.new_str(name));
286        m.insert("lastModified".to_string(), Value::Float(last_modified));
287        m
288    });
289    Ok(build_blob("File", &bytes, &typ, extra))
290}
291
292/// Method names for `Blob`/`File` instances (parent `instance_has_method`).
293pub const BLOB_METHODS: &[&str] = &["text", "arrayBuffer", "bytes", "slice"];
294
295/// `Blob`/`File` instance methods. `text`/`arrayBuffer`/`bytes` return already-
296/// resolved Promises (Node's async accessors); `slice` returns a new `Blob`.
297/// `arrayBuffer`/`bytes` resolve with a `Buffer` (this runtime's byte container)
298/// rather than a bare `ArrayBuffer`/`Uint8Array`.
299pub fn blob_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
300    let bytes = bytes_of(recv);
301    match method {
302        "text" => {
303            let s = String::from_utf8_lossy(&bytes).into_owned();
304            let sv = with_host(|h| h.new_str(s));
305            Ok(crate::host::promise_of(&sv))
306        }
307        "arrayBuffer" | "bytes" => {
308            let buf = from_bytes(&bytes);
309            Ok(crate::host::promise_of(&buf))
310        }
311        "slice" => {
312            let (s, e) = slice_bounds(args, bytes.len());
313            let typ = if args.len() > 2 {
314                arg_str(args, 2)
315            } else {
316                String::new()
317            };
318            Ok(build_blob("Blob", &bytes[s..e], &typ, IndexMap::new()))
319        }
320        _ => Err(crate::host::type_error(&format!(
321            "blob.{method} is not a function"
322        ))),
323    }
324}
325
326/// A Buffer that is a WINDOW onto an existing `ArrayBuffer`'s store, sharing
327/// its bytes rather than copying them.
328pub fn share_array_buffer(ab: &Value, off: usize, len: usize) -> Value {
329    let store = crate::stdlib::typedarray::buffer_store(ab);
330    with_host(|h| {
331        let mut m = IndexMap::new();
332        m.insert("@@native".into(), h.new_str("Buffer"));
333        m.insert(
334            "@@bytes".into(),
335            store.unwrap_or_else(|| h.new_array(Vec::new())),
336        );
337        m.insert("@@buffer".into(), ab.clone());
338        m.insert("buffer".into(), ab.clone());
339        m.insert("length".into(), Value::Float(len as f64));
340        m.insert("byteLength".into(), Value::Float(len as f64));
341        m.insert("byteOffset".into(), Value::Float(off as f64));
342        m.insert("BYTES_PER_ELEMENT".into(), Value::Float(1.0));
343        let obj = h.new_object(m);
344        h.ensure_native_protos();
345        if let Some(p) = h.native_proto("Buffer") {
346            h.set_proto(&obj, p);
347        }
348        for k in [
349            "buffer",
350            "length",
351            "byteLength",
352            "byteOffset",
353            "BYTES_PER_ELEMENT",
354        ] {
355            h.hide_prop(&obj, k);
356        }
357        obj
358    })
359}
360
361/// Build a Buffer value from raw bytes.
362pub fn from_bytes(bytes: &[u8]) -> Value {
363    with_host(|h| {
364        let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
365        let mut m = IndexMap::new();
366        m.insert("@@native".into(), h.new_str("Buffer"));
367        m.insert("@@bytes".into(), arr);
368        m.insert("length".into(), Value::Float(bytes.len() as f64));
369        // The `Uint8Array` view properties a Buffer inherits in Node. `byteLength`
370        // equals `length` because the element size is 1.
371        m.insert("byteLength".into(), Value::Float(bytes.len() as f64));
372        m.insert("byteOffset".into(), Value::Float(0.0));
373        m.insert("BYTES_PER_ELEMENT".into(), Value::Float(1.0));
374        let obj = h.new_object(m);
375        // A Buffer is a real `Uint8Array` subclass instance in Node, so link it
376        // to the actual `Buffer.prototype` object rather than leaving it a bare
377        // tagged object with no `[[Prototype]]`.
378        h.ensure_native_protos();
379        if let Some(p) = h.native_proto("Buffer") {
380            h.set_proto(&obj, p);
381        }
382        // The view metadata is real but non-enumerable; V8 keeps `length` and
383        // friends off `Object.keys(buf)` (whose own keys are the byte indices).
384        for k in ["length", "byteLength", "byteOffset", "BYTES_PER_ELEMENT"] {
385            h.hide_prop(&obj, k);
386        }
387        obj
388    })
389}
390
391/// A Buffer's window onto its byte store as `(byteOffset, length)`.
392///
393/// A Buffer built from its own bytes spans the whole store, so this is
394/// `(0, len)` and every accessor behaves as it did. One built over an
395/// `ArrayBuffer` SHARES that buffer's array and may start partway into it, which
396/// is what makes `Buffer.from(ab, 2, 2)` alias rather than copy.
397fn window(recv: &Value) -> (usize, usize) {
398    with_host(|h| match h.get(recv) {
399        Some(JsObj::Object(p)) => {
400            let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0) as usize;
401            let store = match p.get("@@bytes").and_then(|v| h.get(v)) {
402                Some(JsObj::Array(items)) => items.len(),
403                _ => 0,
404            };
405            let len = p
406                .get("length")
407                .map(|l| h.to_number(l) as usize)
408                .unwrap_or(store);
409            (off.min(store), len.min(store.saturating_sub(off)))
410        }
411        _ => (0, 0),
412    })
413}
414
415fn bytes_of(recv: &Value) -> Vec<u8> {
416    let (off, len) = window(recv);
417    with_host(|h| match h.get(recv) {
418        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|v| h.get(v)) {
419            Some(JsObj::Array(items)) => items[off..off + len]
420                .iter()
421                .map(|v| h.to_number(v) as u8)
422                .collect(),
423            _ => Vec::new(),
424        },
425        _ => Vec::new(),
426    })
427}
428
429/// The handle of `recv`'s hidden `@@bytes` array, without copying it.
430fn bytes_handle(recv: &Value) -> Option<Value> {
431    with_host(|h| match h.get(recv) {
432        Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
433        _ => None,
434    })
435}
436
437/// `buf[i]` — the byte at an integer index, or `undefined` past the end.
438///
439/// Reads through to the single element. Materialising the whole buffer here
440/// (the old `bytes_of` call) made one indexed read O(len), so any loop over a
441/// Buffer — including a request body arriving through `express.json()` — cost
442/// O(len^2).
443pub fn byte_get(recv: &Value, index: &str) -> Value {
444    let i: usize = match index.parse() {
445        Ok(i) => i,
446        Err(_) => return Value::Undef,
447    };
448    let arr = match bytes_handle(recv) {
449        Some(a) => a,
450        None => return Value::Undef,
451    };
452    let (off, len) = window(recv);
453    if i >= len {
454        return Value::Undef;
455    }
456    with_host(|h| match h.get(&arr) {
457        Some(JsObj::Array(items)) => match items.get(off + i) {
458            Some(v) => Value::Float(h.to_number(v)),
459            None => Value::Undef,
460        },
461        _ => Value::Undef,
462    })
463}
464
465/// `buf[i] = n` — write one byte (truncated to 8 bits, as a Uint8Array does).
466/// Returns whether `recv` was a Buffer and the write landed.
467///
468/// Writes the single element in place; the old path copied the buffer out,
469/// changed one byte, and wrote every byte back.
470pub fn byte_set(recv: &Value, index: &str, val: &Value) -> bool {
471    if super::native_tag(recv).as_deref() != Some("Buffer") {
472        return false;
473    }
474    let i: usize = match index.parse() {
475        Ok(i) => i,
476        Err(_) => return false,
477    };
478    let arr = match bytes_handle(recv) {
479        Some(a) => a,
480        None => return false,
481    };
482    let b = with_host(|h| h.to_number(val)) as i64 as u8;
483    // Indexed through the Buffer's WINDOW, so one sharing an ArrayBuffer writes
484    // where its own view starts rather than at the store's origin.
485    let (off, len) = window(recv);
486    if i >= len {
487        return true;
488    }
489    with_host(|h| {
490        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
491            // Out of range writes are dropped, not appended.
492            if let Some(slot) = items.get_mut(off + i) {
493                *slot = Value::Float(b as f64);
494            }
495        }
496    });
497    true
498}
499
500/// `buffer.kMaxLength` — node v26's ceiling on a Buffer's size, 2^53 - 1.
501const K_MAX_LENGTH: f64 = 9_007_199_254_740_991.0;
502
503/// Node's `validateNumber(size, 'size', 0, kMaxLength)`, the check
504/// `Buffer.alloc` / `allocUnsafe` / `allocUnsafeSlow` run before allocating: a
505/// non-number is `ERR_INVALID_ARG_TYPE`, and a negative, too-large or NaN size is
506/// `ERR_OUT_OF_RANGE`. Without it `Buffer.alloc(-1)` and `Buffer.alloc('x')`
507/// quietly returned an empty buffer, and `Buffer.alloc(2 ** 53)` aborted the
508/// process on a failed allocation instead of throwing.
509fn validate_size(args: &[Value]) -> Result<usize, String> {
510    let v = args.first().cloned().unwrap_or(Value::Undef);
511    if with_host(|h| h.type_of(&v)) != "number" {
512        return Err(crate::host::invalid_arg_type(
513            "size", "argument", "number", &v,
514        ));
515    }
516    let n = with_host(|h| h.to_number(&v));
517    if n.is_nan() || !(0.0..=K_MAX_LENGTH).contains(&n) {
518        return Err(crate::host::coded_error(
519            "RangeError",
520            "ERR_OUT_OF_RANGE",
521            &format!(
522                "The value of \"size\" is out of range. It must be >= 0 && <= {}. Received {}",
523                crate::host::fmt_number(K_MAX_LENGTH),
524                out_of_range_received(n)
525            ),
526        ));
527    }
528    Ok(n as usize)
529}
530
531/// How node's `ERR_OUT_OF_RANGE` shows a numeric input: an integer beyond 2^32
532/// through `addNumericalSeparator` (`_` every three characters from the right,
533/// applied to the printed text, so `1e21` becomes `1e_+21` exactly as in node),
534/// anything else as `inspect` would.
535fn out_of_range_received(n: f64) -> String {
536    let shown = crate::host::fmt_number(n);
537    if n.fract() != 0.0 || n.abs() <= 4_294_967_296.0 {
538        return shown;
539    }
540    let start = usize::from(shown.starts_with('-'));
541    let mut i = shown.len();
542    let mut groups = String::new();
543    while i >= start + 4 {
544        groups = format!("_{}{groups}", &shown[i - 3..i]);
545        i -= 3;
546    }
547    format!("{}{groups}", &shown[..i])
548}
549
550pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
551    Some(match method {
552        "from" => from(args),
553        "alloc" => {
554            let n = match validate_size(args) {
555                Ok(n) => n,
556                Err(e) => return Some(Err(e)),
557            };
558            // A string fill repeats to length n; a numeric fill is a single byte.
559            // alloc(size[, fill[, encoding]]).
560            let pat = if args.len() > 1 {
561                let enc = if args.len() > 2 {
562                    arg_str(args, 2)
563                } else {
564                    "utf8".into()
565                };
566                fill_pattern(args, 1, &enc)
567            } else {
568                vec![0]
569            };
570            let bytes: Vec<u8> = if pat.is_empty() {
571                vec![0u8; n]
572            } else {
573                (0..n).map(|i| pat[i % pat.len()]).collect()
574            };
575            Ok(from_bytes(&bytes))
576        }
577        // `allocUnsafeSlow` differs from `allocUnsafe` only in skipping Node's
578        // shared pool — an allocator detail with no observable difference here,
579        // where every Buffer already owns its bytes.
580        "allocUnsafe" | "allocUnsafeSlow" => validate_size(args).map(|n| from_bytes(&vec![0u8; n])),
581        "concat" => concat(args),
582        // `Buffer.of(...bytes)` — the `%TypedArray%.of` form: each argument is one
583        // byte. Measured: `Buffer.of(1,2,3).toString('hex') === '010203'`,
584        // `Buffer.of().length === 0`.
585        "of" => Ok(from_bytes(
586            &args
587                .iter()
588                .map(|v| crate::host::with_host(|h| h.to_number(v)) as u8)
589                .collect::<Vec<u8>>(),
590        )),
591        // `Buffer.isEncoding(enc)` — case-insensitive over the encodings Node
592        // accepts. Measured: `UTF8`, `UTF-8`, `ASCII` and `Hex` are all true;
593        // `utf7`, `utf-16be`, `none` and `''` are all false.
594        "isEncoding" => Ok(Value::Bool(matches!(
595            super::arg_str(args, 0).to_ascii_lowercase().as_str(),
596            "utf8"
597                | "utf-8"
598                | "ucs2"
599                | "ucs-2"
600                | "utf16le"
601                | "utf-16le"
602                | "latin1"
603                | "binary"
604                | "base64"
605                | "base64url"
606                | "hex"
607                | "ascii"
608        ))),
609        // Static `Buffer.compare(a, b)` — the sort comparator form.
610        "compare" => {
611            let a = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
612            let b = bytes_of(&args.get(1).cloned().unwrap_or(Value::Undef));
613            Ok(Value::Float(match a.cmp(&b) {
614                std::cmp::Ordering::Less => -1.0,
615                std::cmp::Ordering::Equal => 0.0,
616                std::cmp::Ordering::Greater => 1.0,
617            }))
618        }
619        "isBuffer" => Ok(Value::Bool(
620            super::native_tag(&args.first().cloned().unwrap_or(Value::Undef)).as_deref()
621                == Some("Buffer"),
622        )),
623        "byteLength" => {
624            // A Buffer/typed array/ArrayBuffer argument reports its VIEW size,
625            // not its element count and not the length of a stringification.
626            if let Some(n) = view_byte_length(&args.first().cloned().unwrap_or(Value::Undef)) {
627                return Some(Ok(Value::Float(n)));
628            }
629            let enc = args
630                .get(1)
631                .map(|_| arg_str(args, 1))
632                .unwrap_or_else(|| "utf8".into());
633            Ok(Value::Float(
634                decode_str(&arg_str(args, 0), &enc).len() as f64
635            ))
636        }
637        _ => return None,
638    })
639}
640
641/// The bytes of any byte-like source: a JS array of byte values, another
642/// `Buffer`, ANY typed array, or an `ArrayBuffer`. `None` for anything else
643/// (a string, which every caller handles with its own encoding rules).
644///
645/// A `Buffer` is a `Uint8Array` subclass, so every place that accepts a Buffer
646/// accepts a typed array too. Each of `Buffer.from`, `Buffer.concat`,
647/// `buf.equals` and `buf.indexOf` had its own notion of "byte source" and all
648/// four understood `@@bytes` only: passing a `Uint8Array` made `from` fall
649/// through to the STRING path and produce the bytes of `"[object Object]"`,
650/// made `concat` contribute nothing, and made `equals`/`indexOf` silently miss.
651/// Routing them all through one helper is what keeps them from drifting again.
652///
653/// Element values are truncated to a byte each, which is what Node does:
654/// `Buffer.from(new Int32Array([1, 2, 300]))` is `<Buffer 01 02 2c>`.
655/// The bytes of a byte VIEW — a `Buffer`, any typed array, a `DataView` or an
656/// `ArrayBuffer` — and nothing else.
657///
658/// Narrower than `bytes_like`, which also accepts a plain JS array of byte
659/// values. The APIs that take "a Buffer, TypedArray, DataView or string" want
660/// exactly this set: an array argument is a TypeError in node, so widening to
661/// it would trade one divergence for another.
662pub fn view_bytes(v: &Value) -> Option<Vec<u8>> {
663    match super::native_tag(v).as_deref() {
664        // A `DataView` exposes no elements, so its bytes come straight from the
665        // window it holds onto its buffer. It is deliberately absent from
666        // `bytes_like`: `Buffer.from(dataView)` is EMPTY in node, because
667        // `Buffer.from` wants something array-like and a DataView has no
668        // `length`.
669        Some("DataView") => {
670            let n = with_host(|h| match h.get(v) {
671                Some(JsObj::Object(p)) => p.get("byteLength").map(|l| h.to_number(l) as usize),
672                _ => None,
673            })
674            .unwrap_or(0);
675            crate::stdlib::typedarray::view_bytes(v, 0, n)
676        }
677        Some("Buffer") | Some("TypedArray") | Some("ArrayBuffer") => bytes_like(v),
678        _ => None,
679    }
680}
681
682pub fn bytes_like(v: &Value) -> Option<Vec<u8>> {
683    // A Buffer or a typed array of any kind: its ELEMENTS, truncated.
684    if let Some(elems) = crate::stdlib::typedarray::elems_of(v) {
685        return Some(elems.iter().map(|x| *x as i64 as u8).collect());
686    }
687    // An `ArrayBuffer` is handled by `from`, which SHARES its store rather than
688    // copying — reaching here would produce a detached copy.
689    if super::native_tag(v).as_deref() == Some("ArrayBuffer") {
690        return Some(crate::stdlib::typedarray::buffer_bytes_snapshot(v).unwrap_or_default());
691    }
692    // A plain JS array of byte values.
693    with_host(|h| match h.get(v) {
694        Some(JsObj::Array(items)) => {
695            Some(items.iter().map(|x| h.to_number(x) as i64 as u8).collect())
696        }
697        _ => None,
698    })
699}
700
701/// The `byteLength` a `Buffer`/typed array/`ArrayBuffer` reports, which is the
702/// VIEW's size in bytes rather than its element count — `Buffer.byteLength(new
703/// Int32Array([1,2,3]))` is 12, not 3. Verified against node v26.7.0.
704fn view_byte_length(v: &Value) -> Option<f64> {
705    match super::native_tag(v).as_deref() {
706        Some("Buffer") | Some("TypedArray") | Some("ArrayBuffer") => {
707            with_host(|h| match h.get(v) {
708                Some(JsObj::Object(p)) => p.get("byteLength").map(|b| h.to_number(b)),
709                _ => None,
710            })
711        }
712        _ => None,
713    }
714}
715
716fn from(args: &[Value]) -> Result<Value, String> {
717    let v = args.first().cloned().unwrap_or(Value::Undef);
718    // `Buffer.from(arrayBuffer[, byteOffset[, length]])` SHARES the buffer's
719    // memory — a write through the Buffer is visible through every other view.
720    // It used to copy zero bytes, because an ArrayBuffer had no store at all.
721    if super::native_tag(&v).as_deref() == Some("ArrayBuffer") {
722        let total = crate::stdlib::typedarray::buffer_byte_length(&v);
723        let off = (super::arg_num(args, 1).max(0.0) as usize).min(total);
724        let len = match args.get(2) {
725            Some(Value::Undef) | None => total - off,
726            Some(_) => (super::arg_num(args, 2).max(0.0) as usize).min(total - off),
727        };
728        return Ok(share_array_buffer(&v, off, len));
729    }
730    // Any byte-like source (array of bytes, Buffer, typed array).
731    if let Some(bytes) = bytes_like(&v) {
732        return Ok(from_bytes(&bytes));
733    }
734    // A string, with an optional encoding.
735    if with_host(|h| h.as_str(&v)).is_some() || matches!(v, Value::Str(_)) {
736        let enc = if args.len() > 1 {
737            arg_str(args, 1)
738        } else {
739            "utf8".into()
740        };
741        return Ok(from_bytes(&decode_str(&arg_str(args, 0), &enc)));
742    }
743    // A `DataView` yields an EMPTY buffer: `Buffer.from` wants something
744    // array-like, and a DataView carries `byteLength` but no `length`.
745    if super::native_tag(&v).as_deref() == Some("DataView") {
746        return Ok(from_bytes(&[]));
747    }
748    // An ARRAY-LIKE object — anything with a numeric `length` — contributes its
749    // index properties, each coerced to a byte. `Buffer.from({length: 2})` is
750    // two zero bytes in node.
751    if matches!(v, Value::Obj(_)) {
752        let len = crate::builtins::get_property(&v, "length").unwrap_or(Value::Undef);
753        if !matches!(len, Value::Undef) {
754            let n = with_host(|h| h.to_number(&len));
755            if n.is_finite() && n >= 0.0 {
756                let n = n as usize;
757                let mut out = Vec::with_capacity(n);
758                for i in 0..n {
759                    let e =
760                        crate::builtins::get_property(&v, &i.to_string()).unwrap_or(Value::Undef);
761                    let b = with_host(|h| h.to_number(&e));
762                    out.push(if b.is_finite() { b as i64 as u8 } else { 0 });
763                }
764                return Ok(from_bytes(&out));
765            }
766        }
767    }
768    // Anything else is a TypeError, not a stringification: `Buffer.from(5)`
769    // used to produce the single byte `0x35` (the digit "5") and
770    // `Buffer.from(null)` the four bytes of `"null"`.
771    Err(crate::host::plain_coded_error(
772        "TypeError",
773        "ERR_INVALID_ARG_TYPE",
774        &format!(
775            "The first argument must be of type string or an instance of \
776Buffer, ArrayBuffer, or Array or an Array-like Object. Received {}",
777            received_label(&v)
778        ),
779    ))
780}
781
782/// How node names a rejected `Buffer.from` argument: `null`, `type number (5)`,
783/// or `an instance of Object`.
784fn received_label(v: &Value) -> String {
785    if matches!(v, Value::Undef) {
786        return "undefined".into();
787    }
788    if with_host(|h| h.is_null(v)) {
789        return "null".into();
790    }
791    let ty = with_host(|h| h.type_of(v));
792    if ty == "object" || ty == "function" {
793        let ctor = with_host(|h| h.ctor_name(v));
794        let ctor = if ctor.is_empty() {
795            "Object".into()
796        } else {
797            ctor
798        };
799        return format!("an instance of {ctor}");
800    }
801    let shown = with_host(|h| h.inspect(v));
802    format!("type {ty} ({shown})")
803}
804
805fn concat(args: &[Value]) -> Result<Value, String> {
806    let list = with_host(
807        |h| match h.get(&args.first().cloned().unwrap_or(Value::Undef)) {
808            Some(JsObj::Array(items)) => items.clone(),
809            _ => Vec::new(),
810        },
811    );
812    let mut out = Vec::new();
813    for b in &list {
814        // Each part may be a Buffer OR any other typed array.
815        out.extend(bytes_like(b).unwrap_or_default());
816    }
817    Ok(from_bytes(&out))
818}
819
820/// Buffer instance methods.
821pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
822    let bytes = bytes_of(recv);
823    match method {
824        // toString([encoding[, start[, end]]]) — the range was ignored, so every
825        // partial read (`buf.toString('utf8', 1, 3)`) returned the WHOLE buffer.
826        "toString" => {
827            let enc = match args.first() {
828                None | Some(Value::Undef) => "utf8".into(),
829                _ => arg_str(args, 0),
830            };
831            let len = bytes.len();
832            let clamp = |i: usize| -> usize {
833                let n = super::arg_num(args, i);
834                if n.is_nan() {
835                    0
836                } else {
837                    n.clamp(0.0, len as f64) as usize
838                }
839            };
840            let start = if args.len() > 1 { clamp(1) } else { 0 };
841            let end = if args.len() > 2 { clamp(2) } else { len };
842            // An inverted range is empty, not reversed.
843            let slice = if start < end {
844                &bytes[start..end]
845            } else {
846                &[][..]
847            };
848            Ok(with_host(|h| h.new_str(encode_bytes(slice, &enc))))
849        }
850        "toJSON" => Ok(with_host(|h| {
851            let data = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
852            let mut m = IndexMap::new();
853            m.insert("type".into(), h.new_str("Buffer"));
854            m.insert("data".into(), data);
855            h.new_object(m)
856        })),
857        "equals" => {
858            // Comparable against any typed array, not just another Buffer.
859            let other = bytes_like(&args.first().cloned().unwrap_or(Value::Undef));
860            Ok(Value::Bool(other.is_some_and(|o| bytes == o)))
861        }
862        "slice" | "subarray" => {
863            let (s, e) = slice_bounds(args, bytes.len());
864            Ok(from_bytes(&bytes[s..e]))
865        }
866        "readUInt8" => {
867            let i = read_offset(args, 1, bytes.len())?;
868            Ok(Value::Float(bytes[i] as f64))
869        }
870        // indexOf/lastIndexOf/includes(value[, byteOffset][, encoding]) — both
871        // trailing arguments used to be ignored, so a search always started at 0
872        // and always read the needle as UTF-8.
873        "includes" | "indexOf" | "lastIndexOf" => {
874            let len = bytes.len();
875            let last = method == "lastIndexOf";
876            // `byteOffset` is a string when it is really the encoding.
877            let (from, enc) = match args.get(1) {
878                None | Some(Value::Undef) => (None, arg_str(args, 2)),
879                Some(v) if with_host(|h| h.as_str(v)).is_some() => (None, arg_str(args, 1)),
880                _ => (Some(super::arg_num(args, 1)), arg_str(args, 2)),
881            };
882            let enc = if enc.is_empty() { "utf8".into() } else { enc };
883            // The needle is a string, a byte value, or another Buffer.
884            let target = args.first().cloned().unwrap_or(Value::Undef);
885            let needle = match &target {
886                Value::Int(_) | Value::Float(_) => vec![super::arg_num(args, 0) as u8],
887                // A Buffer or any other typed array searches by its bytes.
888                _ if bytes_like(&target).is_some() => bytes_like(&target).unwrap_or_default(),
889                _ => decode_str(&arg_str(args, 0), &enc),
890            };
891            // A negative offset counts back from the end; NaN is 0. Out of range
892            // means "no room to match" forwards, and "whole buffer" backwards.
893            let from = from.map(|n| {
894                if n.is_nan() {
895                    0
896                } else if n < 0.0 {
897                    (len as f64 + n).max(0.0) as usize
898                } else {
899                    (n as usize).min(len)
900                }
901            });
902            // An empty needle matches at the offset itself, clamped to the length.
903            let pos = if needle.is_empty() {
904                Some(from.unwrap_or(if last { len } else { 0 }).min(len))
905            } else if last {
906                // lastIndexOf searches at or before the offset, so the match may
907                // start at `from` itself and run past it.
908                let hi = (from.unwrap_or(len) + needle.len()).min(len);
909                bytes[..hi]
910                    .windows(needle.len())
911                    .rposition(|w| w == needle.as_slice())
912            } else {
913                let lo = from.unwrap_or(0);
914                bytes[lo..]
915                    .windows(needle.len())
916                    .position(|w| w == needle.as_slice())
917                    .map(|p| p + lo)
918            };
919            if method == "includes" {
920                Ok(Value::Bool(pos.is_some()))
921            } else {
922                Ok(Value::Float(pos.map(|p| p as f64).unwrap_or(-1.0)))
923            }
924        }
925        // Lexicographic byte comparison → -1 / 0 / 1.
926        "compare" => {
927            let other = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
928            Ok(Value::Float(match bytes.cmp(&other) {
929                std::cmp::Ordering::Less => -1.0,
930                std::cmp::Ordering::Equal => 0.0,
931                std::cmp::Ordering::Greater => 1.0,
932            }))
933        }
934        // Big-endian / little-endian integer reads.
935        "readUInt16BE" => {
936            let i = read_offset(args, 2, bytes.len())?;
937            let v = ((bytes[i] as u16) << 8) | bytes[i + 1] as u16;
938            Ok(Value::Float(v as f64))
939        }
940        "readUInt16LE" => {
941            let i = read_offset(args, 2, bytes.len())?;
942            let v = (bytes[i] as u16) | ((bytes[i + 1] as u16) << 8);
943            Ok(Value::Float(v as f64))
944        }
945        // 32-bit and signed reads. `readIntXX` reinterprets the same bytes as
946        // two's complement.
947        "readUInt32BE" | "readUInt32LE" | "readInt32BE" | "readInt32LE" => {
948            let i = read_offset(args, 4, bytes.len())?;
949            let at = |k: usize| bytes[i + k] as u32;
950            let v = if method.ends_with("BE") {
951                (at(0) << 24) | (at(1) << 16) | (at(2) << 8) | at(3)
952            } else {
953                at(0) | (at(1) << 8) | (at(2) << 16) | (at(3) << 24)
954            };
955            Ok(Value::Float(if method.starts_with("readInt") {
956                v as i32 as f64
957            } else {
958                v as f64
959            }))
960        }
961        "readInt8" => {
962            let i = read_offset(args, 1, bytes.len())?;
963            Ok(Value::Float(bytes[i] as i8 as f64))
964        }
965        "readInt16BE" | "readInt16LE" => {
966            let i = read_offset(args, 2, bytes.len())?;
967            let at = |k: usize| bytes[i + k] as u16;
968            let v = if method.ends_with("BE") {
969                (at(0) << 8) | at(1)
970            } else {
971                at(0) | (at(1) << 8)
972            };
973            Ok(Value::Float(v as i16 as f64))
974        }
975        // `buf[i]` by method: `at` accepts a negative index like Array.prototype.at.
976        "at" => {
977            let i = super::arg_num(args, 0);
978            let idx = if i < 0.0 { i + bytes.len() as f64 } else { i };
979            Ok(match bytes.get(idx.max(-1.0) as usize) {
980                Some(b) if idx >= 0.0 => Value::Float(*b as f64),
981                _ => Value::Undef,
982            })
983        }
984        // Iteration helpers: a Buffer is an index/byte collection.
985        // A Buffer's `Symbol.iterator` IS `values`, inherited from
986        // `%TypedArray%.prototype`, so it dispatches here rather than reporting
987        // itself missing.
988        "values" | "keys" | "entries" | "@@iterator" => {
989            let items: Vec<Value> = with_host(|h| match method {
990                "keys" => (0..bytes.len()).map(|i| Value::Float(i as f64)).collect(),
991                "values" | "@@iterator" => bytes.iter().map(|b| Value::Float(*b as f64)).collect(),
992                _ => bytes
993                    .iter()
994                    .enumerate()
995                    .map(|(i, b)| {
996                        h.new_array(vec![Value::Float(i as f64), Value::Float(*b as f64)])
997                    })
998                    .collect(),
999            });
1000            Ok(with_host(|h| {
1001                h.alloc(JsObj::Iter {
1002                    items,
1003                    idx: 0,
1004                    array: None,
1005                })
1006            }))
1007        }
1008        // IEEE-754 reads/writes. `f32`/`f64` go through their raw bit patterns,
1009        // so the endianness handling is the same byte reversal as the integers.
1010        "readFloatBE" | "readFloatLE" => {
1011            let i = read_offset(args, 4, bytes.len())?;
1012            let mut raw = [0u8; 4];
1013            raw.copy_from_slice(&bytes[i..i + 4]);
1014            if method.ends_with("LE") {
1015                raw.reverse();
1016            }
1017            Ok(Value::Float(f32::from_be_bytes(raw) as f64))
1018        }
1019        "readDoubleBE" | "readDoubleLE" => {
1020            let i = read_offset(args, 8, bytes.len())?;
1021            let mut raw = [0u8; 8];
1022            raw.copy_from_slice(&bytes[i..i + 8]);
1023            if method.ends_with("LE") {
1024                raw.reverse();
1025            }
1026            Ok(Value::Float(f64::from_be_bytes(raw)))
1027        }
1028        "writeFloatBE" | "writeFloatLE" => {
1029            let mut raw = (super::arg_num(args, 0) as f32).to_be_bytes();
1030            if method.ends_with("LE") {
1031                raw.reverse();
1032            }
1033            let off = super::arg_num(args, 1).max(0.0) as usize;
1034            store_bytes(recv, &bytes, off, &raw)?;
1035            Ok(Value::Float((off + 4) as f64))
1036        }
1037        "writeDoubleBE" | "writeDoubleLE" => {
1038            let mut raw = super::arg_num(args, 0).to_be_bytes();
1039            if method.ends_with("LE") {
1040                raw.reverse();
1041            }
1042            let off = super::arg_num(args, 1).max(0.0) as usize;
1043            store_bytes(recv, &bytes, off, &raw)?;
1044            Ok(Value::Float((off + 8) as f64))
1045        }
1046        // 64-bit integers, which exceed `f64`'s exact range and so are BigInts
1047        // on both sides.
1048        "readBigInt64BE" | "readBigInt64LE" | "readBigUInt64BE" | "readBigUInt64LE" => {
1049            let i = read_offset(args, 8, bytes.len())?;
1050            let mut raw = [0u8; 8];
1051            raw.copy_from_slice(&bytes[i..i + 8]);
1052            if method.ends_with("LE") {
1053                raw.reverse();
1054            }
1055            let n = if method.starts_with("readBigInt") {
1056                num_bigint::BigInt::from(i64::from_be_bytes(raw))
1057            } else {
1058                num_bigint::BigInt::from(u64::from_be_bytes(raw))
1059            };
1060            Ok(with_host(|h| h.alloc(JsObj::BigInt(n))))
1061        }
1062        "writeBigInt64BE" | "writeBigInt64LE" | "writeBigUInt64BE" | "writeBigUInt64LE" => {
1063            let v = args.first().cloned().unwrap_or(Value::Undef);
1064            let n = with_host(|h| match h.get(&v) {
1065                Some(JsObj::BigInt(b)) => b.clone(),
1066                _ => num_bigint::BigInt::from(h.to_number(&v) as i64),
1067            });
1068            // Both signed and unsigned store the same 64 bits; the sign is only
1069            // a question of how they are read back.
1070            let bits = num_traits::ToPrimitive::to_i64(&n)
1071                .map(|x| x as u64)
1072                .or_else(|| num_traits::ToPrimitive::to_u64(&n))
1073                .unwrap_or(0);
1074            let mut raw = bits.to_be_bytes();
1075            if method.ends_with("LE") {
1076                raw.reverse();
1077            }
1078            let off = super::arg_num(args, 1).max(0.0) as usize;
1079            store_bytes(recv, &bytes, off, &raw)?;
1080            Ok(Value::Float((off + 8) as f64))
1081        }
1082        // The variable-width family: `byteLength` is an argument (1..=6), which
1083        // is why these cannot share the fixed-width arms above.
1084        "readIntBE" | "readIntLE" | "readUIntBE" | "readUIntLE" => {
1085            let off = super::arg_num(args, 0).max(0.0) as usize;
1086            let width = (super::arg_num(args, 1).max(1.0) as usize).min(6);
1087            if off + width > bytes.len() {
1088                return Err(range_error_out_of_bounds());
1089            }
1090            let mut acc: u64 = 0;
1091            for k in 0..width {
1092                let b = if method.ends_with("BE") {
1093                    bytes[off + k]
1094                } else {
1095                    bytes[off + width - 1 - k]
1096                };
1097                acc = (acc << 8) | b as u64;
1098            }
1099            let signed = method.starts_with("readInt");
1100            let out = if signed {
1101                // Sign-extend from the top bit of the width-th byte.
1102                let shift = 64 - (width * 8);
1103                ((acc << shift) as i64 >> shift) as f64
1104            } else {
1105                acc as f64
1106            };
1107            Ok(Value::Float(out))
1108        }
1109        "writeIntBE" | "writeIntLE" | "writeUIntBE" | "writeUIntLE" => {
1110            let val = super::arg_num(args, 0) as i64 as u64;
1111            let off = super::arg_num(args, 1).max(0.0) as usize;
1112            let width = (super::arg_num(args, 2).max(1.0) as usize).min(6);
1113            let mut raw: Vec<u8> = (0..width)
1114                .map(|k| (val >> (8 * (width - 1 - k))) as u8)
1115                .collect();
1116            if method.ends_with("LE") {
1117                raw.reverse();
1118            }
1119            store_bytes(recv, &bytes, off, &raw)?;
1120            Ok(Value::Float((off + width) as f64))
1121        }
1122        "writeInt8" => {
1123            let off = super::arg_num(args, 1).max(0.0) as usize;
1124            store_bytes(recv, &bytes, off, &[super::arg_num(args, 0) as i64 as u8])?;
1125            Ok(Value::Float((off + 1) as f64))
1126        }
1127        "writeInt16BE" | "writeInt16LE" => {
1128            let val = super::arg_num(args, 0) as i64 as u16;
1129            let mut raw = val.to_be_bytes();
1130            if method.ends_with("LE") {
1131                raw.reverse();
1132            }
1133            let off = super::arg_num(args, 1).max(0.0) as usize;
1134            store_bytes(recv, &bytes, off, &raw)?;
1135            Ok(Value::Float((off + 2) as f64))
1136        }
1137        // In-place writes: mutate the backing `@@bytes`, return the next offset.
1138        "writeUInt8" => {
1139            let off = super::arg_num(args, 1).max(0.0) as usize;
1140            store_bytes(recv, &bytes, off, &[super::arg_num(args, 0) as u8])?;
1141            Ok(Value::Float((off + 1) as f64))
1142        }
1143        "writeUInt16BE" | "writeUInt16LE" => {
1144            let mut b = bytes.clone();
1145            let val = super::arg_num(args, 0) as u16;
1146            let off = super::arg_num(args, 1).max(0.0) as usize;
1147            let (hi, lo) = ((val >> 8) as u8, (val & 0xff) as u8);
1148            let (b0, b1) = if method == "writeUInt16BE" {
1149                (hi, lo)
1150            } else {
1151                (lo, hi)
1152            };
1153            let _ = &mut b;
1154            store_bytes(recv, &bytes, off, &[b0, b1])?;
1155            Ok(Value::Float((off + 2) as f64))
1156        }
1157        "writeUInt32BE" | "writeUInt32LE" | "writeInt32BE" | "writeInt32LE" => {
1158            let mut b = bytes.clone();
1159            let val = super::arg_num(args, 0) as i64 as u32;
1160            let off = super::arg_num(args, 1).max(0.0) as usize;
1161            let be = [
1162                (val >> 24) as u8,
1163                (val >> 16) as u8,
1164                (val >> 8) as u8,
1165                val as u8,
1166            ];
1167            let out: Vec<u8> = if method.ends_with("BE") {
1168                be.to_vec()
1169            } else {
1170                be.iter().rev().copied().collect()
1171            };
1172            let _ = &mut b;
1173            store_bytes(recv, &bytes, off, &out)?;
1174            Ok(Value::Float((off + 4) as f64))
1175        }
1176        // write(string[, offset[, length]][, encoding]) — returns bytes written.
1177        // `length` and `encoding` used to be ignored entirely: every write was
1178        // UTF-8 and ran to the end of the buffer.
1179        "write" => {
1180            let mut b = bytes.clone();
1181            let Some((off, max, enc)) = write_args(args, b.len()) else {
1182                return Err(crate::host::range_error(&format!(
1183                    "The value of \"offset\" is out of range. It must be >= 0 && <= {}. Received {}",
1184                    b.len(),
1185                    super::arg_num(args, 1)
1186                )));
1187            };
1188            let src = truncate_chars(&arg_str(args, 0), &enc, max);
1189            let n = src.len().min(b.len().saturating_sub(off));
1190            b[off..off + n].copy_from_slice(&src[..n]);
1191            set_bytes(recv, &b);
1192            Ok(Value::Float(n as f64))
1193        }
1194        // swap16/32/64 reverse each 2/4/8-byte group IN PLACE and return the
1195        // same Buffer, so `b.swap16()` mutates `b`. A length that is not a whole
1196        // number of groups is a RangeError rather than a partial swap.
1197        "swap16" | "swap32" | "swap64" => {
1198            let group = match method {
1199                "swap16" => 2,
1200                "swap32" => 4,
1201                _ => 8,
1202            };
1203            if bytes.len() % group != 0 {
1204                return Err(crate::host::coded_error(
1205                    "RangeError",
1206                    "ERR_INVALID_BUFFER_SIZE",
1207                    &format!("Buffer size must be a multiple of {}-bits", group * 8),
1208                ));
1209            }
1210            let mut b = bytes.clone();
1211            for c in b.chunks_mut(group) {
1212                c.reverse();
1213            }
1214            set_bytes(recv, &b);
1215            Ok(recv.clone())
1216        }
1217        // fill(value[, start[, end]]) — value is a byte or a repeated string.
1218        // fill(value[, offset[, end]][, encoding]). A STRING in the `offset` or
1219        // `end` slot is the encoding, and node then resets the range to the
1220        // whole buffer rather than shifting the remaining arguments left — so
1221        // `fill('41','hex',1,3)` fills all of it, not `1..3`.
1222        "fill" => {
1223            let mut b = bytes.clone();
1224            let len = b.len();
1225            let (start, end, enc) = if arg_is_str(args, 1) {
1226                (0, len, arg_str(args, 1))
1227            } else if arg_is_str(args, 2) {
1228                let s = (super::arg_num(args, 1).max(0.0) as usize).min(len);
1229                (s, len, arg_str(args, 2))
1230            } else {
1231                let s = if args.len() > 1 {
1232                    (super::arg_num(args, 1).max(0.0) as usize).min(len)
1233                } else {
1234                    0
1235                };
1236                let e = if args.len() > 2 {
1237                    (super::arg_num(args, 2).max(0.0) as usize).min(len)
1238                } else {
1239                    len
1240                };
1241                let enc = if args.len() > 3 {
1242                    arg_str(args, 3)
1243                } else {
1244                    "utf8".into()
1245                };
1246                (s, e, enc)
1247            };
1248            let pat = fill_pattern(args, 0, &enc);
1249            if !pat.is_empty() {
1250                for (k, slot) in b[start..end.max(start)].iter_mut().enumerate() {
1251                    *slot = pat[k % pat.len()];
1252                }
1253            }
1254            set_bytes(recv, &b);
1255            Ok(recv.clone())
1256        }
1257        // copy(target[, targetStart[, sourceStart[, sourceEnd]]]) — returns count.
1258        "copy" => {
1259            let target = args.first().cloned().unwrap_or(Value::Undef);
1260            let mut tb = bytes_of(&target);
1261            let tstart = if args.len() > 1 {
1262                super::arg_num(args, 1).max(0.0) as usize
1263            } else {
1264                0
1265            };
1266            let sstart = if args.len() > 2 {
1267                super::arg_num(args, 2).max(0.0) as usize
1268            } else {
1269                0
1270            };
1271            let send = if args.len() > 3 {
1272                (super::arg_num(args, 3) as usize).min(bytes.len())
1273            } else {
1274                bytes.len()
1275            };
1276            let mut n = 0;
1277            for (k, &byte) in bytes[sstart..send.max(sstart)].iter().enumerate() {
1278                if tstart + k < tb.len() {
1279                    tb[tstart + k] = byte;
1280                    n += 1;
1281                }
1282            }
1283            set_bytes(&target, &tb);
1284            Ok(Value::Float(n as f64))
1285        }
1286        // `TypedArray.prototype.set(source[, offset])` — copy `source`'s bytes in
1287        // at `offset`, throwing when they would not fit (as Node does).
1288        "set" => {
1289            let src = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
1290            let offset = if args.len() > 1 {
1291                super::arg_num(args, 1).max(0.0) as usize
1292            } else {
1293                0
1294            };
1295            if offset + src.len() > bytes.len() {
1296                return Err(crate::host::range_error("offset is out of bounds"));
1297            }
1298            let mut out = bytes.clone();
1299            out[offset..offset + src.len()].copy_from_slice(&src);
1300            set_bytes(recv, &out);
1301            Ok(Value::Undef)
1302        }
1303        // A Buffer IS a `Uint8Array`, so anything it does not implement itself
1304        // falls through to the shared typed-array behaviour it inherits —
1305        // `every`, `map`, `filter`, `forEach`, `reduce`, `sort` and the rest.
1306        // These used to READ as functions (the thunks resolve through
1307        // `Uint8Array.prototype` on the chain) but throw on call, because
1308        // dispatch landed here and stopped. A method the typed arrays do not
1309        // have either still reports the Buffer-shaped error below.
1310        _ if crate::stdlib::typedarray::PROTOTYPE_METHODS.contains(&method) => {
1311            crate::stdlib::typedarray::instance_call(recv, method, args)
1312        }
1313        _ => Err(crate::host::type_error(&format!(
1314            "buffer.{method} is not a function"
1315        ))),
1316    }
1317}
1318
1319/// The fill pattern at `args[idx]`: a string's utf-8 bytes, else a single byte.
1320fn fill_pattern(args: &[Value], idx: usize, enc: &str) -> Vec<u8> {
1321    match args.get(idx) {
1322        None => vec![0],
1323        Some(v) => {
1324            let is_str = matches!(v, Value::Str(_))
1325                || with_host(|h| matches!(h.get(v), Some(JsObj::Str(_))));
1326            if is_str {
1327                decode_str(&arg_str(args, idx), enc)
1328            } else {
1329                vec![super::arg_num(args, idx) as u8]
1330            }
1331        }
1332    }
1333}
1334
1335/// Whether `args[i]` is a string — the test that separates a positional
1336/// `offset`/`end` from a trailing `encoding` in `fill`/`write`/`indexOf`.
1337fn arg_is_str(args: &[Value], i: usize) -> bool {
1338    args.get(i)
1339        .is_some_and(|v| with_host(|h| h.as_str(v)).is_some())
1340}
1341
1342/// Overwrite `recv`'s backing `@@bytes` array (for in-place buffer writes).
1343/// Write `out` into the buffer's backing bytes at `off`.
1344///
1345/// A write that would run past the end is a `RangeError`, as in node. The
1346/// fixed-width writers used to skip it silently and still return the advanced
1347/// offset, so a caller writing past the end was told it had succeeded and the
1348/// bytes were simply lost.
1349fn store_bytes(recv: &Value, bytes: &[u8], off: usize, out: &[u8]) -> Result<(), String> {
1350    if off + out.len() > bytes.len() {
1351        return Err(range_error_out_of_bounds());
1352    }
1353    let mut b = bytes.to_vec();
1354    b[off..off + out.len()].copy_from_slice(out);
1355    set_bytes(recv, &b);
1356    Ok(())
1357}
1358
1359fn range_error_out_of_bounds() -> String {
1360    crate::host::plain_coded_error(
1361        "RangeError",
1362        "ERR_OUT_OF_RANGE",
1363        "Attempt to access memory outside buffer bounds",
1364    )
1365}
1366
1367fn set_bytes(recv: &Value, new: &[u8]) {
1368    let (off, _) = window(recv);
1369    with_host(|h| {
1370        let arr = match h.get(recv) {
1371            Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
1372            _ => None,
1373        };
1374        if let Some(a) = arr {
1375            if let Some(JsObj::Array(items)) = h.get_mut(&a) {
1376                // Only the window is rewritten, so a Buffer sharing an
1377                // ArrayBuffer never clobbers bytes outside its own view.
1378                for (i, b) in new.iter().enumerate() {
1379                    if off + i < items.len() {
1380                        items[off + i] = Value::Float(*b as f64);
1381                    }
1382                }
1383            }
1384        }
1385    });
1386}
1387
1388fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
1389    let norm = |n: f64| -> usize {
1390        if n < 0.0 {
1391            (len as f64 + n).max(0.0) as usize
1392        } else {
1393            (n as usize).min(len)
1394        }
1395    };
1396    let s = if args.is_empty() {
1397        0
1398    } else {
1399        norm(super::arg_num(args, 0))
1400    };
1401    let e = if args.len() < 2 {
1402        len
1403    } else {
1404        norm(super::arg_num(args, 1))
1405    };
1406    (s.min(e), e.max(s))
1407}
1408
1409/// String → bytes under a Node buffer encoding.
1410///
1411/// The `utf16le` family and `base64url` used to fall through to the UTF-8 arm,
1412/// which is silent corruption rather than a missing feature: `Buffer.from('abc',
1413/// 'utf16le')` produced the 3 bytes `616263` instead of node's 6 bytes
1414/// `610062006300`, and every `byteLength`/`write`/`fill` that funnels through
1415/// here inherited the wrong count.
1416pub(crate) fn decode_str(s: &str, enc: &str) -> Vec<u8> {
1417    match enc.to_ascii_lowercase().as_str() {
1418        "hex" => from_hex(s),
1419        "base64" | "base64url" => from_base64(s),
1420        // One byte per UTF-16 CODE UNIT, not per code point: node writes
1421        // `Buffer.from("\u{1D4B3}","latin1")` as the low bytes of the surrogate
1422        // pair (`35 b3`), two bytes, not the one low byte of U+1D4B3. Encoding
1423        // does NOT mask to 7 bits even for `ascii` — only decoding does.
1424        "ascii" | "latin1" | "binary" => s.encode_utf16().map(|u| u as u8).collect(),
1425        // A JS string IS UTF-16, so this encoding is the identity on its code
1426        // units, written little-endian — not a transcode of the UTF-8 bytes.
1427        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
1428            s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
1429        }
1430        _ => s.as_bytes().to_vec(),
1431    }
1432}
1433
1434pub(crate) fn encode_bytes(bytes: &[u8], enc: &str) -> String {
1435    match enc.to_ascii_lowercase().as_str() {
1436        "hex" => to_hex(bytes),
1437        "base64" => to_base64(bytes),
1438        "base64url" => super::to_base64url(bytes),
1439        // Decoding `ascii` masks off the high bit (node: `Buffer.from([0xff])
1440        // .toString('ascii')` is `U+007F`); `latin1` keeps the whole byte.
1441        "ascii" => bytes.iter().map(|b| (*b & 0x7f) as char).collect(),
1442        "latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
1443        // A trailing odd byte has no code unit and is dropped, as node does.
1444        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
1445            let units: Vec<u16> = bytes
1446                .chunks_exact(2)
1447                .map(|c| u16::from_le_bytes([c[0], c[1]]))
1448                .collect();
1449            crate::utf16::to_string_lossy(&units)
1450        }
1451        _ => String::from_utf8_lossy(bytes).into_owned(),
1452    }
1453}
1454
1455/// Resolve the `(offset, length, encoding)` triple of `buf.write(string[,
1456/// offset[, length]][, encoding])`, whose trailing arguments are positional-
1457/// or-encoding depending on their runtime type (node's own `Buffer.prototype
1458/// .write` does exactly this dispatch).
1459///
1460/// `args[0]` is the string; this reads from `args[1]` on. Returns `None` when
1461/// the offset is out of range, which is a `RangeError` at the call site.
1462fn write_args(args: &[Value], len: usize) -> Option<(usize, usize, String)> {
1463    let num = |i: usize| super::arg_num(args, i);
1464    // write(string) / write(string, encoding)
1465    if args.len() < 2 {
1466        return Some((0, len, "utf8".into()));
1467    }
1468    if arg_is_str(args, 1) {
1469        return Some((0, len, arg_str(args, 1)));
1470    }
1471    let off = num(1);
1472    if !(0.0..=len as f64).contains(&off) {
1473        return None;
1474    }
1475    let off = off as usize;
1476    // write(string, offset) / write(string, offset, encoding)
1477    if args.len() < 3 {
1478        return Some((off, len - off, "utf8".into()));
1479    }
1480    if arg_is_str(args, 2) {
1481        return Some((off, len - off, arg_str(args, 2)));
1482    }
1483    let max = len - off;
1484    let n = (num(2).max(0.0) as usize).min(max);
1485    let enc = if args.len() > 3 {
1486        arg_str(args, 3)
1487    } else {
1488        "utf8".into()
1489    };
1490    Some((off, n, enc))
1491}
1492
1493/// Truncate `bytes` to at most `max`, never splitting a multi-byte character.
1494///
1495/// `buf.write` writes whole characters only: node reports 2, not 4, for
1496/// `Buffer.alloc(4).write('é€')` — the 2-byte `é` fits and the 3-byte `€` is
1497/// dropped whole rather than half-written. Only the variable-width encodings
1498/// need this; the fixed-width ones are already aligned by construction.
1499fn truncate_chars(s: &str, enc: &str, max: usize) -> Vec<u8> {
1500    let bytes = decode_str(s, enc);
1501    if bytes.len() <= max {
1502        return bytes;
1503    }
1504    match enc.to_ascii_lowercase().as_str() {
1505        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => bytes[..max - max % 2].to_vec(),
1506        "hex" | "base64" | "base64url" | "ascii" | "latin1" | "binary" => bytes[..max].to_vec(),
1507        _ => {
1508            let mut end = max;
1509            while end > 0 && (bytes[end] & 0xC0) == 0x80 {
1510                end -= 1;
1511            }
1512            bytes[..end].to_vec()
1513        }
1514    }
1515}
1516
1517/// The validated byte offset for a fixed-width `buf.readXxx(offset)`.
1518///
1519/// Every read used to be `arg_num(args, 0).max(0.0) as usize` with
1520/// `bytes.get(i).unwrap_or(&0)` behind it, so an out-of-range read silently
1521/// produced zeroes instead of throwing — and a negative offset silently became
1522/// 0. Node raises one of two coded errors, and which one depends on whether the
1523/// buffer could hold the value at all:
1524///
1525/// ```text
1526/// Buffer.alloc(4).readUInt8(5)     RangeError [ERR_OUT_OF_RANGE]
1527///     The value of "offset" is out of range. It must be >= 0 and <= 3. Received 5
1528/// Buffer.alloc(0).readUInt8(0)     RangeError [ERR_BUFFER_OUT_OF_BOUNDS]
1529///     Attempt to access memory outside buffer bounds
1530/// ```
1531fn read_offset(args: &[Value], size: usize, len: usize) -> Result<usize, String> {
1532    if len < size {
1533        return Err(crate::host::coded_error(
1534            "RangeError",
1535            "ERR_BUFFER_OUT_OF_BOUNDS",
1536            "Attempt to access memory outside buffer bounds",
1537        ));
1538    }
1539    let max = len - size;
1540    let raw = match args.first() {
1541        None | Some(Value::Undef) => 0.0,
1542        Some(_) => super::arg_num(args, 0),
1543    };
1544    // A non-integer offset is its own rejection, with a different tail than the
1545    // range one — `readUInt8(1.5)` is "It must be an integer", not a bound.
1546    if raw.fract() != 0.0 || raw.is_nan() {
1547        return Err(crate::host::coded_error(
1548            "RangeError",
1549            "ERR_OUT_OF_RANGE",
1550            &format!(
1551                "The value of \"offset\" is out of range. It must be an integer. Received {}",
1552                crate::host::fmt_number(raw)
1553            ),
1554        ));
1555    }
1556    let off = raw;
1557    if off < 0.0 || off > max as f64 {
1558        return Err(crate::host::coded_error(
1559            "RangeError",
1560            "ERR_OUT_OF_RANGE",
1561            &format!(
1562                "The value of \"offset\" is out of range. It must be >= 0 and <= {max}. \
1563                 Received {}",
1564                crate::host::fmt_number(raw)
1565            ),
1566        ));
1567    }
1568    Ok(off as usize)
1569}