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    "writeUInt16BE",
62    "writeUInt16LE",
63    "readUInt32BE",
64    "readUInt32LE",
65    "readInt8",
66    "readInt16BE",
67    "readInt16LE",
68    "readInt32BE",
69    "readInt32LE",
70    "writeUInt32BE",
71    "writeUInt32LE",
72    "writeInt32BE",
73    "writeInt32LE",
74    "at",
75    "values",
76    "keys",
77    "entries",
78    "swap16",
79    "swap32",
80    "swap64",
81];
82
83/// Free functions of the `buffer` module itself (`require('buffer').atob`, …), as
84/// opposed to the `Buffer` constructor's static methods above. Needs the parent
85/// `"buffer"` routing arm (see final report).
86pub const MODULE_METHODS: &[&str] = &["atob", "btoa", "isAscii", "isUtf8", "transcode"];
87
88/// Dispatch a `require('buffer').<method>` free function.
89pub fn module_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
90    Some(match method {
91        // atob: base64 → a binary (latin1) string.
92        "atob" => {
93            let s = arg_str(args, 0);
94            let bytes = from_base64(&s);
95            let bin: String = bytes.iter().map(|b| *b as char).collect();
96            Ok(with_host(|h| h.new_str(bin)))
97        }
98        // btoa: a binary string → base64 (each char's low byte is one octet).
99        "btoa" => {
100            let s = arg_str(args, 0);
101            let bytes: Vec<u8> = s.chars().map(|c| c as u32 as u8).collect();
102            let b64 = to_base64(&bytes);
103            Ok(with_host(|h| h.new_str(b64)))
104        }
105        "isAscii" => {
106            let bytes = input_bytes(args.first());
107            Ok(Value::Bool(bytes.iter().all(|b| *b < 0x80)))
108        }
109        "isUtf8" => {
110            let bytes = input_bytes(args.first());
111            Ok(Value::Bool(std::str::from_utf8(&bytes).is_ok()))
112        }
113        // transcode(source, fromEnc, toEnc): re-encode bytes between utf8/latin1/
114        // ascii/utf16le (best-effort; hex/base64 are not transcode encodings).
115        "transcode" => {
116            let src = input_bytes(args.first());
117            let from = arg_str(args, 1);
118            let to = arg_str(args, 2);
119            let s = bytes_to_string(&src, &from);
120            let out = string_to_bytes(&s, &to);
121            Ok(from_bytes(&out))
122        }
123        _ => return None,
124    })
125}
126
127/// Raw bytes of a Buffer/Blob arg, or the UTF-8 bytes of a string arg.
128fn input_bytes(v: Option<&Value>) -> Vec<u8> {
129    match v {
130        None => Vec::new(),
131        Some(v) => {
132            if let Some(s) = with_host(|h| h.as_str(v)) {
133                s.into_bytes()
134            } else {
135                bytes_of(v)
136            }
137        }
138    }
139}
140
141/// Interpret bytes under `enc` as a Rust string (for `transcode`).
142fn bytes_to_string(bytes: &[u8], enc: &str) -> String {
143    match enc.to_ascii_lowercase().as_str() {
144        "ascii" | "latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
145        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
146            let units: Vec<u16> = bytes
147                .chunks_exact(2)
148                .map(|c| u16::from_le_bytes([c[0], c[1]]))
149                .collect();
150            String::from_utf16_lossy(&units)
151        }
152        _ => String::from_utf8_lossy(bytes).into_owned(),
153    }
154}
155
156/// Encode a Rust string into `enc` bytes (for `transcode`).
157///
158/// `transcode` is ICU, not `Buffer.from`, so its `ascii` arm SUBSTITUTES rather
159/// than truncates: node renders `transcode(Buffer.from('aÿ'),'utf8','ascii')` as
160/// `61 3f` — an unrepresentable character becomes `?`.
161fn string_to_bytes(s: &str, enc: &str) -> Vec<u8> {
162    match enc.to_ascii_lowercase().as_str() {
163        "ascii" => s
164            .chars()
165            .map(|c| if c.is_ascii() { c as u8 } else { b'?' })
166            .collect(),
167        "latin1" | "binary" => s.chars().map(|c| c as u32 as u8).collect(),
168        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
169            s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
170        }
171        _ => s.as_bytes().to_vec(),
172    }
173}
174
175// ── Blob / File ──────────────────────────────────────────────────────────────
176//
177// A `Blob` is a native object tagged `@@native = "Blob"` (a `File` is `"File"`)
178// whose bytes live in `@@bytes`, with `size`/`type` (and `File`'s `name`/
179// `lastModified`) as readable data properties. Needs parent construct/instance
180// wiring (see final report).
181
182/// Concatenate one Blob-part's bytes: a string contributes its UTF-8 bytes, a
183/// Buffer/Blob its raw bytes.
184fn part_bytes(v: &Value) -> Vec<u8> {
185    match with_host(|h| h.as_str(v)) {
186        Some(s) => s.into_bytes(),
187        None => bytes_of(v),
188    }
189}
190
191/// Gather the byte payload from a `BlobPart[]` (the first constructor argument).
192fn gather_parts(parts: &Value) -> Vec<u8> {
193    let items = with_host(|h| match h.get(parts) {
194        Some(JsObj::Array(it)) => it.clone(),
195        _ => Vec::new(),
196    });
197    let mut out = Vec::new();
198    for it in &items {
199        out.extend(part_bytes(it));
200    }
201    out
202}
203
204/// The `type` string from an options bag (`{ type }`), or "".
205fn opt_type(opts: Option<&Value>) -> String {
206    match opts {
207        Some(v) => with_host(|h| match h.get(v) {
208            Some(JsObj::Object(p)) => p.get("type").map(|x| h.str_of(x)).unwrap_or_default(),
209            _ => String::new(),
210        }),
211        None => String::new(),
212    }
213}
214
215/// Build a `Blob`/`File` native object with the shared `@@bytes`/`size`/`type`
216/// fields; `File` adds `name`/`lastModified`.
217fn build_blob(tag: &str, bytes: &[u8], typ: &str, extra: IndexMap<String, Value>) -> Value {
218    with_host(|h| {
219        let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
220        let mut m = IndexMap::new();
221        m.insert("@@native".into(), h.new_str(tag.to_string()));
222        m.insert("@@bytes".into(), arr);
223        m.insert("size".into(), Value::Float(bytes.len() as f64));
224        m.insert("type".into(), h.new_str(typ.to_string()));
225        for (k, v) in extra {
226            m.insert(k, v);
227        }
228        h.new_object(m)
229    })
230}
231
232/// `new Blob(parts[, options])`.
233pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
234    let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
235    let typ = opt_type(args.get(1));
236    Ok(build_blob("Blob", &bytes, &typ, IndexMap::new()))
237}
238
239/// `new File(parts, name[, options])`.
240pub fn construct_file(args: &[Value]) -> Result<Value, String> {
241    let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
242    let name = arg_str(args, 1);
243    let typ = opt_type(args.get(2));
244    // lastModified: options.lastModified or 0.
245    let last_modified = args
246        .get(2)
247        .map(|v| {
248            with_host(|h| match h.get(v) {
249                Some(JsObj::Object(p)) => {
250                    p.get("lastModified").map(|x| h.to_number(x)).unwrap_or(0.0)
251                }
252                _ => 0.0,
253            })
254        })
255        .unwrap_or(0.0);
256    let extra = with_host(|h| {
257        let mut m = IndexMap::new();
258        m.insert("name".to_string(), h.new_str(name));
259        m.insert("lastModified".to_string(), Value::Float(last_modified));
260        m
261    });
262    Ok(build_blob("File", &bytes, &typ, extra))
263}
264
265/// Method names for `Blob`/`File` instances (parent `instance_has_method`).
266pub const BLOB_METHODS: &[&str] = &["text", "arrayBuffer", "bytes", "slice"];
267
268/// `Blob`/`File` instance methods. `text`/`arrayBuffer`/`bytes` return already-
269/// resolved Promises (Node's async accessors); `slice` returns a new `Blob`.
270/// `arrayBuffer`/`bytes` resolve with a `Buffer` (this runtime's byte container)
271/// rather than a bare `ArrayBuffer`/`Uint8Array`.
272pub fn blob_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
273    let bytes = bytes_of(recv);
274    match method {
275        "text" => {
276            let s = String::from_utf8_lossy(&bytes).into_owned();
277            let sv = with_host(|h| h.new_str(s));
278            Ok(crate::host::promise_of(&sv))
279        }
280        "arrayBuffer" | "bytes" => {
281            let buf = from_bytes(&bytes);
282            Ok(crate::host::promise_of(&buf))
283        }
284        "slice" => {
285            let (s, e) = slice_bounds(args, bytes.len());
286            let typ = if args.len() > 2 {
287                arg_str(args, 2)
288            } else {
289                String::new()
290            };
291            Ok(build_blob("Blob", &bytes[s..e], &typ, IndexMap::new()))
292        }
293        _ => Err(crate::host::type_error(&format!(
294            "blob.{method} is not a function"
295        ))),
296    }
297}
298
299/// Build a Buffer value from raw bytes.
300pub fn from_bytes(bytes: &[u8]) -> Value {
301    with_host(|h| {
302        let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
303        let mut m = IndexMap::new();
304        m.insert("@@native".into(), h.new_str("Buffer"));
305        m.insert("@@bytes".into(), arr);
306        m.insert("length".into(), Value::Float(bytes.len() as f64));
307        // The `Uint8Array` view properties a Buffer inherits in Node. `byteLength`
308        // equals `length` because the element size is 1.
309        m.insert("byteLength".into(), Value::Float(bytes.len() as f64));
310        m.insert("byteOffset".into(), Value::Float(0.0));
311        m.insert("BYTES_PER_ELEMENT".into(), Value::Float(1.0));
312        let obj = h.new_object(m);
313        // A Buffer is a real `Uint8Array` subclass instance in Node, so link it
314        // to the actual `Buffer.prototype` object rather than leaving it a bare
315        // tagged object with no `[[Prototype]]`.
316        h.ensure_native_protos();
317        if let Some(p) = h.native_proto("Buffer") {
318            h.set_proto(&obj, p);
319        }
320        // The view metadata is real but non-enumerable; V8 keeps `length` and
321        // friends off `Object.keys(buf)` (whose own keys are the byte indices).
322        for k in ["length", "byteLength", "byteOffset", "BYTES_PER_ELEMENT"] {
323            h.hide_prop(&obj, k);
324        }
325        obj
326    })
327}
328
329fn bytes_of(recv: &Value) -> Vec<u8> {
330    with_host(|h| match h.get(recv) {
331        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|v| h.get(v)) {
332            Some(JsObj::Array(items)) => items.iter().map(|v| h.to_number(v) as u8).collect(),
333            _ => Vec::new(),
334        },
335        _ => Vec::new(),
336    })
337}
338
339/// The handle of `recv`'s hidden `@@bytes` array, without copying it.
340fn bytes_handle(recv: &Value) -> Option<Value> {
341    with_host(|h| match h.get(recv) {
342        Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
343        _ => None,
344    })
345}
346
347/// `buf[i]` — the byte at an integer index, or `undefined` past the end.
348///
349/// Reads through to the single element. Materialising the whole buffer here
350/// (the old `bytes_of` call) made one indexed read O(len), so any loop over a
351/// Buffer — including a request body arriving through `express.json()` — cost
352/// O(len^2).
353pub fn byte_get(recv: &Value, index: &str) -> Value {
354    let i: usize = match index.parse() {
355        Ok(i) => i,
356        Err(_) => return Value::Undef,
357    };
358    let arr = match bytes_handle(recv) {
359        Some(a) => a,
360        None => return Value::Undef,
361    };
362    with_host(|h| match h.get(&arr) {
363        Some(JsObj::Array(items)) => match items.get(i) {
364            Some(v) => Value::Float(h.to_number(v)),
365            None => Value::Undef,
366        },
367        _ => Value::Undef,
368    })
369}
370
371/// `buf[i] = n` — write one byte (truncated to 8 bits, as a Uint8Array does).
372/// Returns whether `recv` was a Buffer and the write landed.
373///
374/// Writes the single element in place; the old path copied the buffer out,
375/// changed one byte, and wrote every byte back.
376pub fn byte_set(recv: &Value, index: &str, val: &Value) -> bool {
377    if super::native_tag(recv).as_deref() != Some("Buffer") {
378        return false;
379    }
380    let i: usize = match index.parse() {
381        Ok(i) => i,
382        Err(_) => return false,
383    };
384    let arr = match bytes_handle(recv) {
385        Some(a) => a,
386        None => return false,
387    };
388    let b = with_host(|h| h.to_number(val)) as i64 as u8;
389    with_host(|h| {
390        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
391            // Out of range writes are dropped, not appended.
392            if let Some(slot) = items.get_mut(i) {
393                *slot = Value::Float(b as f64);
394            }
395        }
396    });
397    true
398}
399
400pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
401    Some(match method {
402        "from" => from(args),
403        "alloc" => {
404            let n = super::arg_num(args, 0).max(0.0) as usize;
405            // A string fill repeats to length n; a numeric fill is a single byte.
406            // alloc(size[, fill[, encoding]]).
407            let pat = if args.len() > 1 {
408                let enc = if args.len() > 2 {
409                    arg_str(args, 2)
410                } else {
411                    "utf8".into()
412                };
413                fill_pattern(args, 1, &enc)
414            } else {
415                vec![0]
416            };
417            let bytes: Vec<u8> = if pat.is_empty() {
418                vec![0u8; n]
419            } else {
420                (0..n).map(|i| pat[i % pat.len()]).collect()
421            };
422            Ok(from_bytes(&bytes))
423        }
424        // `allocUnsafeSlow` differs from `allocUnsafe` only in skipping Node's
425        // shared pool — an allocator detail with no observable difference here,
426        // where every Buffer already owns its bytes.
427        "allocUnsafe" | "allocUnsafeSlow" => Ok(from_bytes(&vec![
428            0u8;
429            super::arg_num(args, 0).max(0.0)
430                as usize
431        ])),
432        "concat" => concat(args),
433        // `Buffer.of(...bytes)` — the `%TypedArray%.of` form: each argument is one
434        // byte. Measured: `Buffer.of(1,2,3).toString('hex') === '010203'`,
435        // `Buffer.of().length === 0`.
436        "of" => Ok(from_bytes(
437            &args
438                .iter()
439                .map(|v| crate::host::with_host(|h| h.to_number(v)) as u8)
440                .collect::<Vec<u8>>(),
441        )),
442        // `Buffer.isEncoding(enc)` — case-insensitive over the encodings Node
443        // accepts. Measured: `UTF8`, `UTF-8`, `ASCII` and `Hex` are all true;
444        // `utf7`, `utf-16be`, `none` and `''` are all false.
445        "isEncoding" => Ok(Value::Bool(matches!(
446            super::arg_str(args, 0).to_ascii_lowercase().as_str(),
447            "utf8"
448                | "utf-8"
449                | "ucs2"
450                | "ucs-2"
451                | "utf16le"
452                | "utf-16le"
453                | "latin1"
454                | "binary"
455                | "base64"
456                | "base64url"
457                | "hex"
458                | "ascii"
459        ))),
460        // Static `Buffer.compare(a, b)` — the sort comparator form.
461        "compare" => {
462            let a = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
463            let b = bytes_of(&args.get(1).cloned().unwrap_or(Value::Undef));
464            Ok(Value::Float(match a.cmp(&b) {
465                std::cmp::Ordering::Less => -1.0,
466                std::cmp::Ordering::Equal => 0.0,
467                std::cmp::Ordering::Greater => 1.0,
468            }))
469        }
470        "isBuffer" => Ok(Value::Bool(
471            super::native_tag(&args.first().cloned().unwrap_or(Value::Undef)).as_deref()
472                == Some("Buffer"),
473        )),
474        "byteLength" => {
475            // A Buffer/typed array/ArrayBuffer argument reports its VIEW size,
476            // not its element count and not the length of a stringification.
477            if let Some(n) = view_byte_length(&args.first().cloned().unwrap_or(Value::Undef)) {
478                return Some(Ok(Value::Float(n)));
479            }
480            let enc = args
481                .get(1)
482                .map(|_| arg_str(args, 1))
483                .unwrap_or_else(|| "utf8".into());
484            Ok(Value::Float(
485                decode_str(&arg_str(args, 0), &enc).len() as f64
486            ))
487        }
488        _ => return None,
489    })
490}
491
492/// The bytes of any byte-like source: a JS array of byte values, another
493/// `Buffer`, ANY typed array, or an `ArrayBuffer`. `None` for anything else
494/// (a string, which every caller handles with its own encoding rules).
495///
496/// A `Buffer` is a `Uint8Array` subclass, so every place that accepts a Buffer
497/// accepts a typed array too. Each of `Buffer.from`, `Buffer.concat`,
498/// `buf.equals` and `buf.indexOf` had its own notion of "byte source" and all
499/// four understood `@@bytes` only: passing a `Uint8Array` made `from` fall
500/// through to the STRING path and produce the bytes of `"[object Object]"`,
501/// made `concat` contribute nothing, and made `equals`/`indexOf` silently miss.
502/// Routing them all through one helper is what keeps them from drifting again.
503///
504/// Element values are truncated to a byte each, which is what Node does:
505/// `Buffer.from(new Int32Array([1, 2, 300]))` is `<Buffer 01 02 2c>`.
506pub fn bytes_like(v: &Value) -> Option<Vec<u8>> {
507    // A Buffer or a typed array of any kind: its ELEMENTS, truncated.
508    if let Some(elems) = crate::stdlib::typedarray::elems_of(v) {
509        return Some(elems.iter().map(|x| *x as i64 as u8).collect());
510    }
511    // An `ArrayBuffer` carries only a byte length in this model, so it reads as
512    // that many zero bytes. Node would share the memory; nothing here can
513    // observe the difference until typed arrays get a real backing store.
514    if super::native_tag(v).as_deref() == Some("ArrayBuffer") {
515        let n = with_host(|h| match h.get(v) {
516            Some(JsObj::Object(p)) => p.get("byteLength").map(|b| h.to_number(b) as usize),
517            _ => None,
518        });
519        return n.map(|n| vec![0u8; n]);
520    }
521    // A plain JS array of byte values.
522    with_host(|h| match h.get(v) {
523        Some(JsObj::Array(items)) => {
524            Some(items.iter().map(|x| h.to_number(x) as i64 as u8).collect())
525        }
526        _ => None,
527    })
528}
529
530/// The `byteLength` a `Buffer`/typed array/`ArrayBuffer` reports, which is the
531/// VIEW's size in bytes rather than its element count — `Buffer.byteLength(new
532/// Int32Array([1,2,3]))` is 12, not 3. Verified against node v26.7.0.
533fn view_byte_length(v: &Value) -> Option<f64> {
534    match super::native_tag(v).as_deref() {
535        Some("Buffer") | Some("TypedArray") | Some("ArrayBuffer") => {
536            with_host(|h| match h.get(v) {
537                Some(JsObj::Object(p)) => p.get("byteLength").map(|b| h.to_number(b)),
538                _ => None,
539            })
540        }
541        _ => None,
542    }
543}
544
545fn from(args: &[Value]) -> Result<Value, String> {
546    let v = args.first().cloned().unwrap_or(Value::Undef);
547    // Any byte-like source (array of bytes, Buffer, typed array, ArrayBuffer).
548    if let Some(bytes) = bytes_like(&v) {
549        return Ok(from_bytes(&bytes));
550    }
551    // String with an optional encoding.
552    let enc = if args.len() > 1 {
553        arg_str(args, 1)
554    } else {
555        "utf8".into()
556    };
557    Ok(from_bytes(&decode_str(&arg_str(args, 0), &enc)))
558}
559
560fn concat(args: &[Value]) -> Result<Value, String> {
561    let list = with_host(
562        |h| match h.get(&args.first().cloned().unwrap_or(Value::Undef)) {
563            Some(JsObj::Array(items)) => items.clone(),
564            _ => Vec::new(),
565        },
566    );
567    let mut out = Vec::new();
568    for b in &list {
569        // Each part may be a Buffer OR any other typed array.
570        out.extend(bytes_like(b).unwrap_or_default());
571    }
572    Ok(from_bytes(&out))
573}
574
575/// Buffer instance methods.
576pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
577    let bytes = bytes_of(recv);
578    match method {
579        // toString([encoding[, start[, end]]]) — the range was ignored, so every
580        // partial read (`buf.toString('utf8', 1, 3)`) returned the WHOLE buffer.
581        "toString" => {
582            let enc = match args.first() {
583                None | Some(Value::Undef) => "utf8".into(),
584                _ => arg_str(args, 0),
585            };
586            let len = bytes.len();
587            let clamp = |i: usize| -> usize {
588                let n = super::arg_num(args, i);
589                if n.is_nan() {
590                    0
591                } else {
592                    n.clamp(0.0, len as f64) as usize
593                }
594            };
595            let start = if args.len() > 1 { clamp(1) } else { 0 };
596            let end = if args.len() > 2 { clamp(2) } else { len };
597            // An inverted range is empty, not reversed.
598            let slice = if start < end {
599                &bytes[start..end]
600            } else {
601                &[][..]
602            };
603            Ok(with_host(|h| h.new_str(encode_bytes(slice, &enc))))
604        }
605        "toJSON" => Ok(with_host(|h| {
606            let data = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
607            let mut m = IndexMap::new();
608            m.insert("type".into(), h.new_str("Buffer"));
609            m.insert("data".into(), data);
610            h.new_object(m)
611        })),
612        "equals" => {
613            // Comparable against any typed array, not just another Buffer.
614            let other = bytes_like(&args.first().cloned().unwrap_or(Value::Undef));
615            Ok(Value::Bool(other.is_some_and(|o| bytes == o)))
616        }
617        "slice" | "subarray" => {
618            let (s, e) = slice_bounds(args, bytes.len());
619            Ok(from_bytes(&bytes[s..e]))
620        }
621        "readUInt8" => {
622            let i = read_offset(args, 1, bytes.len())?;
623            Ok(Value::Float(bytes[i] as f64))
624        }
625        // indexOf/lastIndexOf/includes(value[, byteOffset][, encoding]) — both
626        // trailing arguments used to be ignored, so a search always started at 0
627        // and always read the needle as UTF-8.
628        "includes" | "indexOf" | "lastIndexOf" => {
629            let len = bytes.len();
630            let last = method == "lastIndexOf";
631            // `byteOffset` is a string when it is really the encoding.
632            let (from, enc) = match args.get(1) {
633                None | Some(Value::Undef) => (None, arg_str(args, 2)),
634                Some(v) if with_host(|h| h.as_str(v)).is_some() => (None, arg_str(args, 1)),
635                _ => (Some(super::arg_num(args, 1)), arg_str(args, 2)),
636            };
637            let enc = if enc.is_empty() { "utf8".into() } else { enc };
638            // The needle is a string, a byte value, or another Buffer.
639            let target = args.first().cloned().unwrap_or(Value::Undef);
640            let needle = match &target {
641                Value::Int(_) | Value::Float(_) => vec![super::arg_num(args, 0) as u8],
642                // A Buffer or any other typed array searches by its bytes.
643                _ if bytes_like(&target).is_some() => bytes_like(&target).unwrap_or_default(),
644                _ => decode_str(&arg_str(args, 0), &enc),
645            };
646            // A negative offset counts back from the end; NaN is 0. Out of range
647            // means "no room to match" forwards, and "whole buffer" backwards.
648            let from = from.map(|n| {
649                if n.is_nan() {
650                    0
651                } else if n < 0.0 {
652                    (len as f64 + n).max(0.0) as usize
653                } else {
654                    (n as usize).min(len)
655                }
656            });
657            // An empty needle matches at the offset itself, clamped to the length.
658            let pos = if needle.is_empty() {
659                Some(from.unwrap_or(if last { len } else { 0 }).min(len))
660            } else if last {
661                // lastIndexOf searches at or before the offset, so the match may
662                // start at `from` itself and run past it.
663                let hi = (from.unwrap_or(len) + needle.len()).min(len);
664                bytes[..hi]
665                    .windows(needle.len())
666                    .rposition(|w| w == needle.as_slice())
667            } else {
668                let lo = from.unwrap_or(0);
669                bytes[lo..]
670                    .windows(needle.len())
671                    .position(|w| w == needle.as_slice())
672                    .map(|p| p + lo)
673            };
674            if method == "includes" {
675                Ok(Value::Bool(pos.is_some()))
676            } else {
677                Ok(Value::Float(pos.map(|p| p as f64).unwrap_or(-1.0)))
678            }
679        }
680        // Lexicographic byte comparison → -1 / 0 / 1.
681        "compare" => {
682            let other = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
683            Ok(Value::Float(match bytes.cmp(&other) {
684                std::cmp::Ordering::Less => -1.0,
685                std::cmp::Ordering::Equal => 0.0,
686                std::cmp::Ordering::Greater => 1.0,
687            }))
688        }
689        // Big-endian / little-endian integer reads.
690        "readUInt16BE" => {
691            let i = read_offset(args, 2, bytes.len())?;
692            let v = ((bytes[i] as u16) << 8) | bytes[i + 1] as u16;
693            Ok(Value::Float(v as f64))
694        }
695        "readUInt16LE" => {
696            let i = read_offset(args, 2, bytes.len())?;
697            let v = (bytes[i] as u16) | ((bytes[i + 1] as u16) << 8);
698            Ok(Value::Float(v as f64))
699        }
700        // 32-bit and signed reads. `readIntXX` reinterprets the same bytes as
701        // two's complement.
702        "readUInt32BE" | "readUInt32LE" | "readInt32BE" | "readInt32LE" => {
703            let i = read_offset(args, 4, bytes.len())?;
704            let at = |k: usize| bytes[i + k] as u32;
705            let v = if method.ends_with("BE") {
706                (at(0) << 24) | (at(1) << 16) | (at(2) << 8) | at(3)
707            } else {
708                at(0) | (at(1) << 8) | (at(2) << 16) | (at(3) << 24)
709            };
710            Ok(Value::Float(if method.starts_with("readInt") {
711                v as i32 as f64
712            } else {
713                v as f64
714            }))
715        }
716        "readInt8" => {
717            let i = read_offset(args, 1, bytes.len())?;
718            Ok(Value::Float(bytes[i] as i8 as f64))
719        }
720        "readInt16BE" | "readInt16LE" => {
721            let i = read_offset(args, 2, bytes.len())?;
722            let at = |k: usize| bytes[i + k] as u16;
723            let v = if method.ends_with("BE") {
724                (at(0) << 8) | at(1)
725            } else {
726                at(0) | (at(1) << 8)
727            };
728            Ok(Value::Float(v as i16 as f64))
729        }
730        // `buf[i]` by method: `at` accepts a negative index like Array.prototype.at.
731        "at" => {
732            let i = super::arg_num(args, 0);
733            let idx = if i < 0.0 { i + bytes.len() as f64 } else { i };
734            Ok(match bytes.get(idx.max(-1.0) as usize) {
735                Some(b) if idx >= 0.0 => Value::Float(*b as f64),
736                _ => Value::Undef,
737            })
738        }
739        // Iteration helpers: a Buffer is an index/byte collection.
740        "values" | "keys" | "entries" => {
741            let items: Vec<Value> = with_host(|h| match method {
742                "keys" => (0..bytes.len()).map(|i| Value::Float(i as f64)).collect(),
743                "values" => bytes.iter().map(|b| Value::Float(*b as f64)).collect(),
744                _ => bytes
745                    .iter()
746                    .enumerate()
747                    .map(|(i, b)| {
748                        h.new_array(vec![Value::Float(i as f64), Value::Float(*b as f64)])
749                    })
750                    .collect(),
751            });
752            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
753        }
754        // In-place writes: mutate the backing `@@bytes`, return the next offset.
755        "writeUInt8" => {
756            let mut b = bytes.clone();
757            let off = super::arg_num(args, 1).max(0.0) as usize;
758            if off < b.len() {
759                b[off] = super::arg_num(args, 0) as u8;
760            }
761            set_bytes(recv, &b);
762            Ok(Value::Float((off + 1) as f64))
763        }
764        "writeUInt16BE" | "writeUInt16LE" => {
765            let mut b = bytes.clone();
766            let val = super::arg_num(args, 0) as u16;
767            let off = super::arg_num(args, 1).max(0.0) as usize;
768            let (hi, lo) = ((val >> 8) as u8, (val & 0xff) as u8);
769            let (b0, b1) = if method == "writeUInt16BE" {
770                (hi, lo)
771            } else {
772                (lo, hi)
773            };
774            if off + 1 < b.len() {
775                b[off] = b0;
776                b[off + 1] = b1;
777            }
778            set_bytes(recv, &b);
779            Ok(Value::Float((off + 2) as f64))
780        }
781        "writeUInt32BE" | "writeUInt32LE" | "writeInt32BE" | "writeInt32LE" => {
782            let mut b = bytes.clone();
783            let val = super::arg_num(args, 0) as i64 as u32;
784            let off = super::arg_num(args, 1).max(0.0) as usize;
785            let be = [
786                (val >> 24) as u8,
787                (val >> 16) as u8,
788                (val >> 8) as u8,
789                val as u8,
790            ];
791            let out: Vec<u8> = if method.ends_with("BE") {
792                be.to_vec()
793            } else {
794                be.iter().rev().copied().collect()
795            };
796            if off + 3 < b.len() {
797                b[off..off + 4].copy_from_slice(&out);
798            }
799            set_bytes(recv, &b);
800            Ok(Value::Float((off + 4) as f64))
801        }
802        // write(string[, offset[, length]][, encoding]) — returns bytes written.
803        // `length` and `encoding` used to be ignored entirely: every write was
804        // UTF-8 and ran to the end of the buffer.
805        "write" => {
806            let mut b = bytes.clone();
807            let Some((off, max, enc)) = write_args(args, b.len()) else {
808                return Err(crate::host::range_error(&format!(
809                    "The value of \"offset\" is out of range. It must be >= 0 && <= {}. Received {}",
810                    b.len(),
811                    super::arg_num(args, 1)
812                )));
813            };
814            let src = truncate_chars(&arg_str(args, 0), &enc, max);
815            let n = src.len().min(b.len().saturating_sub(off));
816            b[off..off + n].copy_from_slice(&src[..n]);
817            set_bytes(recv, &b);
818            Ok(Value::Float(n as f64))
819        }
820        // swap16/32/64 reverse each 2/4/8-byte group IN PLACE and return the
821        // same Buffer, so `b.swap16()` mutates `b`. A length that is not a whole
822        // number of groups is a RangeError rather than a partial swap.
823        "swap16" | "swap32" | "swap64" => {
824            let group = match method {
825                "swap16" => 2,
826                "swap32" => 4,
827                _ => 8,
828            };
829            if bytes.len() % group != 0 {
830                return Err(crate::host::coded_error(
831                    "RangeError",
832                    "ERR_INVALID_BUFFER_SIZE",
833                    &format!("Buffer size must be a multiple of {}-bits", group * 8),
834                ));
835            }
836            let mut b = bytes.clone();
837            for c in b.chunks_mut(group) {
838                c.reverse();
839            }
840            set_bytes(recv, &b);
841            Ok(recv.clone())
842        }
843        // fill(value[, start[, end]]) — value is a byte or a repeated string.
844        // fill(value[, offset[, end]][, encoding]). A STRING in the `offset` or
845        // `end` slot is the encoding, and node then resets the range to the
846        // whole buffer rather than shifting the remaining arguments left — so
847        // `fill('41','hex',1,3)` fills all of it, not `1..3`.
848        "fill" => {
849            let mut b = bytes.clone();
850            let len = b.len();
851            let (start, end, enc) = if arg_is_str(args, 1) {
852                (0, len, arg_str(args, 1))
853            } else if arg_is_str(args, 2) {
854                let s = (super::arg_num(args, 1).max(0.0) as usize).min(len);
855                (s, len, arg_str(args, 2))
856            } else {
857                let s = if args.len() > 1 {
858                    (super::arg_num(args, 1).max(0.0) as usize).min(len)
859                } else {
860                    0
861                };
862                let e = if args.len() > 2 {
863                    (super::arg_num(args, 2).max(0.0) as usize).min(len)
864                } else {
865                    len
866                };
867                let enc = if args.len() > 3 {
868                    arg_str(args, 3)
869                } else {
870                    "utf8".into()
871                };
872                (s, e, enc)
873            };
874            let pat = fill_pattern(args, 0, &enc);
875            if !pat.is_empty() {
876                for (k, slot) in b[start..end.max(start)].iter_mut().enumerate() {
877                    *slot = pat[k % pat.len()];
878                }
879            }
880            set_bytes(recv, &b);
881            Ok(recv.clone())
882        }
883        // copy(target[, targetStart[, sourceStart[, sourceEnd]]]) — returns count.
884        "copy" => {
885            let target = args.first().cloned().unwrap_or(Value::Undef);
886            let mut tb = bytes_of(&target);
887            let tstart = if args.len() > 1 {
888                super::arg_num(args, 1).max(0.0) as usize
889            } else {
890                0
891            };
892            let sstart = if args.len() > 2 {
893                super::arg_num(args, 2).max(0.0) as usize
894            } else {
895                0
896            };
897            let send = if args.len() > 3 {
898                (super::arg_num(args, 3) as usize).min(bytes.len())
899            } else {
900                bytes.len()
901            };
902            let mut n = 0;
903            for (k, &byte) in bytes[sstart..send.max(sstart)].iter().enumerate() {
904                if tstart + k < tb.len() {
905                    tb[tstart + k] = byte;
906                    n += 1;
907                }
908            }
909            set_bytes(&target, &tb);
910            Ok(Value::Float(n as f64))
911        }
912        // `TypedArray.prototype.set(source[, offset])` — copy `source`'s bytes in
913        // at `offset`, throwing when they would not fit (as Node does).
914        "set" => {
915            let src = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
916            let offset = if args.len() > 1 {
917                super::arg_num(args, 1).max(0.0) as usize
918            } else {
919                0
920            };
921            if offset + src.len() > bytes.len() {
922                return Err(crate::host::range_error("offset is out of bounds"));
923            }
924            let mut out = bytes.clone();
925            out[offset..offset + src.len()].copy_from_slice(&src);
926            set_bytes(recv, &out);
927            Ok(Value::Undef)
928        }
929        // A Buffer IS a `Uint8Array`, so anything it does not implement itself
930        // falls through to the shared typed-array behaviour it inherits —
931        // `every`, `map`, `filter`, `forEach`, `reduce`, `sort` and the rest.
932        // These used to READ as functions (the thunks resolve through
933        // `Uint8Array.prototype` on the chain) but throw on call, because
934        // dispatch landed here and stopped. A method the typed arrays do not
935        // have either still reports the Buffer-shaped error below.
936        _ if crate::stdlib::typedarray::PROTOTYPE_METHODS.contains(&method) => {
937            crate::stdlib::typedarray::instance_call(recv, method, args)
938        }
939        _ => Err(crate::host::type_error(&format!(
940            "buffer.{method} is not a function"
941        ))),
942    }
943}
944
945/// The fill pattern at `args[idx]`: a string's utf-8 bytes, else a single byte.
946fn fill_pattern(args: &[Value], idx: usize, enc: &str) -> Vec<u8> {
947    match args.get(idx) {
948        None => vec![0],
949        Some(v) => {
950            let is_str = matches!(v, Value::Str(_))
951                || with_host(|h| matches!(h.get(v), Some(JsObj::Str(_))));
952            if is_str {
953                decode_str(&arg_str(args, idx), enc)
954            } else {
955                vec![super::arg_num(args, idx) as u8]
956            }
957        }
958    }
959}
960
961/// Whether `args[i]` is a string — the test that separates a positional
962/// `offset`/`end` from a trailing `encoding` in `fill`/`write`/`indexOf`.
963fn arg_is_str(args: &[Value], i: usize) -> bool {
964    args.get(i)
965        .is_some_and(|v| with_host(|h| h.as_str(v)).is_some())
966}
967
968/// Overwrite `recv`'s backing `@@bytes` array (for in-place buffer writes).
969fn set_bytes(recv: &Value, new: &[u8]) {
970    with_host(|h| {
971        let arr = match h.get(recv) {
972            Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
973            _ => None,
974        };
975        if let Some(a) = arr {
976            if let Some(JsObj::Array(items)) = h.get_mut(&a) {
977                *items = new.iter().map(|b| Value::Float(*b as f64)).collect();
978            }
979        }
980    });
981}
982
983fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
984    let norm = |n: f64| -> usize {
985        if n < 0.0 {
986            (len as f64 + n).max(0.0) as usize
987        } else {
988            (n as usize).min(len)
989        }
990    };
991    let s = if args.is_empty() {
992        0
993    } else {
994        norm(super::arg_num(args, 0))
995    };
996    let e = if args.len() < 2 {
997        len
998    } else {
999        norm(super::arg_num(args, 1))
1000    };
1001    (s.min(e), e.max(s))
1002}
1003
1004/// String → bytes under a Node buffer encoding.
1005///
1006/// The `utf16le` family and `base64url` used to fall through to the UTF-8 arm,
1007/// which is silent corruption rather than a missing feature: `Buffer.from('abc',
1008/// 'utf16le')` produced the 3 bytes `616263` instead of node's 6 bytes
1009/// `610062006300`, and every `byteLength`/`write`/`fill` that funnels through
1010/// here inherited the wrong count.
1011pub(crate) fn decode_str(s: &str, enc: &str) -> Vec<u8> {
1012    match enc.to_ascii_lowercase().as_str() {
1013        "hex" => from_hex(s),
1014        "base64" | "base64url" => from_base64(s),
1015        // One byte per UTF-16 CODE UNIT, not per code point: node writes
1016        // `Buffer.from("\u{1D4B3}","latin1")` as the low bytes of the surrogate
1017        // pair (`35 b3`), two bytes, not the one low byte of U+1D4B3. Encoding
1018        // does NOT mask to 7 bits even for `ascii` — only decoding does.
1019        "ascii" | "latin1" | "binary" => s.encode_utf16().map(|u| u as u8).collect(),
1020        // A JS string IS UTF-16, so this encoding is the identity on its code
1021        // units, written little-endian — not a transcode of the UTF-8 bytes.
1022        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
1023            s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
1024        }
1025        _ => s.as_bytes().to_vec(),
1026    }
1027}
1028
1029pub(crate) fn encode_bytes(bytes: &[u8], enc: &str) -> String {
1030    match enc.to_ascii_lowercase().as_str() {
1031        "hex" => to_hex(bytes),
1032        "base64" => to_base64(bytes),
1033        "base64url" => super::to_base64url(bytes),
1034        // Decoding `ascii` masks off the high bit (node: `Buffer.from([0xff])
1035        // .toString('ascii')` is `U+007F`); `latin1` keeps the whole byte.
1036        "ascii" => bytes.iter().map(|b| (*b & 0x7f) as char).collect(),
1037        "latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
1038        // A trailing odd byte has no code unit and is dropped, as node does.
1039        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
1040            let units: Vec<u16> = bytes
1041                .chunks_exact(2)
1042                .map(|c| u16::from_le_bytes([c[0], c[1]]))
1043                .collect();
1044            crate::utf16::to_string_lossy(&units)
1045        }
1046        _ => String::from_utf8_lossy(bytes).into_owned(),
1047    }
1048}
1049
1050/// Resolve the `(offset, length, encoding)` triple of `buf.write(string[,
1051/// offset[, length]][, encoding])`, whose trailing arguments are positional-
1052/// or-encoding depending on their runtime type (node's own `Buffer.prototype
1053/// .write` does exactly this dispatch).
1054///
1055/// `args[0]` is the string; this reads from `args[1]` on. Returns `None` when
1056/// the offset is out of range, which is a `RangeError` at the call site.
1057fn write_args(args: &[Value], len: usize) -> Option<(usize, usize, String)> {
1058    let num = |i: usize| super::arg_num(args, i);
1059    // write(string) / write(string, encoding)
1060    if args.len() < 2 {
1061        return Some((0, len, "utf8".into()));
1062    }
1063    if arg_is_str(args, 1) {
1064        return Some((0, len, arg_str(args, 1)));
1065    }
1066    let off = num(1);
1067    if !(0.0..=len as f64).contains(&off) {
1068        return None;
1069    }
1070    let off = off as usize;
1071    // write(string, offset) / write(string, offset, encoding)
1072    if args.len() < 3 {
1073        return Some((off, len - off, "utf8".into()));
1074    }
1075    if arg_is_str(args, 2) {
1076        return Some((off, len - off, arg_str(args, 2)));
1077    }
1078    let max = len - off;
1079    let n = (num(2).max(0.0) as usize).min(max);
1080    let enc = if args.len() > 3 {
1081        arg_str(args, 3)
1082    } else {
1083        "utf8".into()
1084    };
1085    Some((off, n, enc))
1086}
1087
1088/// Truncate `bytes` to at most `max`, never splitting a multi-byte character.
1089///
1090/// `buf.write` writes whole characters only: node reports 2, not 4, for
1091/// `Buffer.alloc(4).write('é€')` — the 2-byte `é` fits and the 3-byte `€` is
1092/// dropped whole rather than half-written. Only the variable-width encodings
1093/// need this; the fixed-width ones are already aligned by construction.
1094fn truncate_chars(s: &str, enc: &str, max: usize) -> Vec<u8> {
1095    let bytes = decode_str(s, enc);
1096    if bytes.len() <= max {
1097        return bytes;
1098    }
1099    match enc.to_ascii_lowercase().as_str() {
1100        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => bytes[..max - max % 2].to_vec(),
1101        "hex" | "base64" | "base64url" | "ascii" | "latin1" | "binary" => bytes[..max].to_vec(),
1102        _ => {
1103            let mut end = max;
1104            while end > 0 && (bytes[end] & 0xC0) == 0x80 {
1105                end -= 1;
1106            }
1107            bytes[..end].to_vec()
1108        }
1109    }
1110}
1111
1112/// The validated byte offset for a fixed-width `buf.readXxx(offset)`.
1113///
1114/// Every read used to be `arg_num(args, 0).max(0.0) as usize` with
1115/// `bytes.get(i).unwrap_or(&0)` behind it, so an out-of-range read silently
1116/// produced zeroes instead of throwing — and a negative offset silently became
1117/// 0. Node raises one of two coded errors, and which one depends on whether the
1118/// buffer could hold the value at all:
1119///
1120/// ```text
1121/// Buffer.alloc(4).readUInt8(5)     RangeError [ERR_OUT_OF_RANGE]
1122///     The value of "offset" is out of range. It must be >= 0 and <= 3. Received 5
1123/// Buffer.alloc(0).readUInt8(0)     RangeError [ERR_BUFFER_OUT_OF_BOUNDS]
1124///     Attempt to access memory outside buffer bounds
1125/// ```
1126fn read_offset(args: &[Value], size: usize, len: usize) -> Result<usize, String> {
1127    if len < size {
1128        return Err(crate::host::coded_error(
1129            "RangeError",
1130            "ERR_BUFFER_OUT_OF_BOUNDS",
1131            "Attempt to access memory outside buffer bounds",
1132        ));
1133    }
1134    let max = len - size;
1135    let raw = match args.first() {
1136        None | Some(Value::Undef) => 0.0,
1137        Some(_) => super::arg_num(args, 0),
1138    };
1139    // A non-integer offset is its own rejection, with a different tail than the
1140    // range one — `readUInt8(1.5)` is "It must be an integer", not a bound.
1141    if raw.fract() != 0.0 || raw.is_nan() {
1142        return Err(crate::host::coded_error(
1143            "RangeError",
1144            "ERR_OUT_OF_RANGE",
1145            &format!(
1146                "The value of \"offset\" is out of range. It must be an integer. Received {}",
1147                crate::host::fmt_number(raw)
1148            ),
1149        ));
1150    }
1151    let off = raw;
1152    if off < 0.0 || off > max as f64 {
1153        return Err(crate::host::coded_error(
1154            "RangeError",
1155            "ERR_OUT_OF_RANGE",
1156            &format!(
1157                "The value of \"offset\" is out of range. It must be >= 0 and <= {max}. \
1158                 Received {}",
1159                crate::host::fmt_number(raw)
1160            ),
1161        ));
1162    }
1163    Ok(off as usize)
1164}