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
10pub const STATIC_METHODS: &[&str] = &[
11    "from",
12    "alloc",
13    "allocUnsafe",
14    "concat",
15    "isBuffer",
16    "byteLength",
17];
18
19/// Free functions of the `buffer` module itself (`require('buffer').atob`, …), as
20/// opposed to the `Buffer` constructor's static methods above. Needs the parent
21/// `"buffer"` routing arm (see final report).
22pub const MODULE_METHODS: &[&str] = &["atob", "btoa", "isAscii", "isUtf8", "transcode"];
23
24/// Dispatch a `require('buffer').<method>` free function.
25pub fn module_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
26    Some(match method {
27        // atob: base64 → a binary (latin1) string.
28        "atob" => {
29            let s = arg_str(args, 0);
30            let bytes = from_base64(&s);
31            let bin: String = bytes.iter().map(|b| *b as char).collect();
32            Ok(with_host(|h| h.new_str(bin)))
33        }
34        // btoa: a binary string → base64 (each char's low byte is one octet).
35        "btoa" => {
36            let s = arg_str(args, 0);
37            let bytes: Vec<u8> = s.chars().map(|c| c as u32 as u8).collect();
38            let b64 = to_base64(&bytes);
39            Ok(with_host(|h| h.new_str(b64)))
40        }
41        "isAscii" => {
42            let bytes = input_bytes(args.first());
43            Ok(Value::Bool(bytes.iter().all(|b| *b < 0x80)))
44        }
45        "isUtf8" => {
46            let bytes = input_bytes(args.first());
47            Ok(Value::Bool(std::str::from_utf8(&bytes).is_ok()))
48        }
49        // transcode(source, fromEnc, toEnc): re-encode bytes between utf8/latin1/
50        // ascii/utf16le (best-effort; hex/base64 are not transcode encodings).
51        "transcode" => {
52            let src = input_bytes(args.first());
53            let from = arg_str(args, 1);
54            let to = arg_str(args, 2);
55            let s = bytes_to_string(&src, &from);
56            let out = string_to_bytes(&s, &to);
57            Ok(from_bytes(&out))
58        }
59        _ => return None,
60    })
61}
62
63/// Raw bytes of a Buffer/Blob arg, or the UTF-8 bytes of a string arg.
64fn input_bytes(v: Option<&Value>) -> Vec<u8> {
65    match v {
66        None => Vec::new(),
67        Some(v) => {
68            if let Some(s) = with_host(|h| h.as_str(v)) {
69                s.into_bytes()
70            } else {
71                bytes_of(v)
72            }
73        }
74    }
75}
76
77/// Interpret bytes under `enc` as a Rust string (for `transcode`).
78fn bytes_to_string(bytes: &[u8], enc: &str) -> String {
79    match enc.to_ascii_lowercase().as_str() {
80        "ascii" | "latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
81        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
82            let units: Vec<u16> = bytes
83                .chunks_exact(2)
84                .map(|c| u16::from_le_bytes([c[0], c[1]]))
85                .collect();
86            String::from_utf16_lossy(&units)
87        }
88        _ => String::from_utf8_lossy(bytes).into_owned(),
89    }
90}
91
92/// Encode a Rust string into `enc` bytes (for `transcode`).
93fn string_to_bytes(s: &str, enc: &str) -> Vec<u8> {
94    match enc.to_ascii_lowercase().as_str() {
95        "ascii" | "latin1" | "binary" => s.chars().map(|c| c as u32 as u8).collect(),
96        "utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
97            s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
98        }
99        _ => s.as_bytes().to_vec(),
100    }
101}
102
103// ── Blob / File ──────────────────────────────────────────────────────────────
104//
105// A `Blob` is a native object tagged `@@native = "Blob"` (a `File` is `"File"`)
106// whose bytes live in `@@bytes`, with `size`/`type` (and `File`'s `name`/
107// `lastModified`) as readable data properties. Needs parent construct/instance
108// wiring (see final report).
109
110/// Concatenate one Blob-part's bytes: a string contributes its UTF-8 bytes, a
111/// Buffer/Blob its raw bytes.
112fn part_bytes(v: &Value) -> Vec<u8> {
113    match with_host(|h| h.as_str(v)) {
114        Some(s) => s.into_bytes(),
115        None => bytes_of(v),
116    }
117}
118
119/// Gather the byte payload from a `BlobPart[]` (the first constructor argument).
120fn gather_parts(parts: &Value) -> Vec<u8> {
121    let items = with_host(|h| match h.get(parts) {
122        Some(JsObj::Array(it)) => it.clone(),
123        _ => Vec::new(),
124    });
125    let mut out = Vec::new();
126    for it in &items {
127        out.extend(part_bytes(it));
128    }
129    out
130}
131
132/// The `type` string from an options bag (`{ type }`), or "".
133fn opt_type(opts: Option<&Value>) -> String {
134    match opts {
135        Some(v) => with_host(|h| match h.get(v) {
136            Some(JsObj::Object(p)) => p.get("type").map(|x| h.str_of(x)).unwrap_or_default(),
137            _ => String::new(),
138        }),
139        None => String::new(),
140    }
141}
142
143/// Build a `Blob`/`File` native object with the shared `@@bytes`/`size`/`type`
144/// fields; `File` adds `name`/`lastModified`.
145fn build_blob(tag: &str, bytes: &[u8], typ: &str, extra: IndexMap<String, Value>) -> Value {
146    with_host(|h| {
147        let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
148        let mut m = IndexMap::new();
149        m.insert("@@native".into(), h.new_str(tag.to_string()));
150        m.insert("@@bytes".into(), arr);
151        m.insert("size".into(), Value::Float(bytes.len() as f64));
152        m.insert("type".into(), h.new_str(typ.to_string()));
153        for (k, v) in extra {
154            m.insert(k, v);
155        }
156        h.new_object(m)
157    })
158}
159
160/// `new Blob(parts[, options])`.
161pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
162    let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
163    let typ = opt_type(args.get(1));
164    Ok(build_blob("Blob", &bytes, &typ, IndexMap::new()))
165}
166
167/// `new File(parts, name[, options])`.
168pub fn construct_file(args: &[Value]) -> Result<Value, String> {
169    let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
170    let name = arg_str(args, 1);
171    let typ = opt_type(args.get(2));
172    // lastModified: options.lastModified or 0.
173    let last_modified = args
174        .get(2)
175        .map(|v| {
176            with_host(|h| match h.get(v) {
177                Some(JsObj::Object(p)) => {
178                    p.get("lastModified").map(|x| h.to_number(x)).unwrap_or(0.0)
179                }
180                _ => 0.0,
181            })
182        })
183        .unwrap_or(0.0);
184    let extra = with_host(|h| {
185        let mut m = IndexMap::new();
186        m.insert("name".to_string(), h.new_str(name));
187        m.insert("lastModified".to_string(), Value::Float(last_modified));
188        m
189    });
190    Ok(build_blob("File", &bytes, &typ, extra))
191}
192
193/// Method names for `Blob`/`File` instances (parent `instance_has_method`).
194pub const BLOB_METHODS: &[&str] = &["text", "arrayBuffer", "bytes", "slice"];
195
196/// `Blob`/`File` instance methods. `text`/`arrayBuffer`/`bytes` return already-
197/// resolved Promises (Node's async accessors); `slice` returns a new `Blob`.
198/// `arrayBuffer`/`bytes` resolve with a `Buffer` (this runtime's byte container)
199/// rather than a bare `ArrayBuffer`/`Uint8Array`.
200pub fn blob_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
201    let bytes = bytes_of(recv);
202    match method {
203        "text" => {
204            let s = String::from_utf8_lossy(&bytes).into_owned();
205            let sv = with_host(|h| h.new_str(s));
206            Ok(crate::host::promise_of(&sv))
207        }
208        "arrayBuffer" | "bytes" => {
209            let buf = from_bytes(&bytes);
210            Ok(crate::host::promise_of(&buf))
211        }
212        "slice" => {
213            let (s, e) = slice_bounds(args, bytes.len());
214            let typ = if args.len() > 2 {
215                arg_str(args, 2)
216            } else {
217                String::new()
218            };
219            Ok(build_blob("Blob", &bytes[s..e], &typ, IndexMap::new()))
220        }
221        _ => Err(crate::host::type_error(&format!(
222            "blob.{method} is not a function"
223        ))),
224    }
225}
226
227/// Build a Buffer value from raw bytes.
228pub fn from_bytes(bytes: &[u8]) -> Value {
229    with_host(|h| {
230        let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
231        let mut m = IndexMap::new();
232        m.insert("@@native".into(), h.new_str("Buffer"));
233        m.insert("@@bytes".into(), arr);
234        m.insert("length".into(), Value::Float(bytes.len() as f64));
235        h.new_object(m)
236    })
237}
238
239fn bytes_of(recv: &Value) -> Vec<u8> {
240    with_host(|h| match h.get(recv) {
241        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|v| h.get(v)) {
242            Some(JsObj::Array(items)) => items.iter().map(|v| h.to_number(v) as u8).collect(),
243            _ => Vec::new(),
244        },
245        _ => Vec::new(),
246    })
247}
248
249pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
250    Some(match method {
251        "from" => from(args),
252        "alloc" => {
253            let n = super::arg_num(args, 0).max(0.0) as usize;
254            // A string fill repeats to length n; a numeric fill is a single byte.
255            let pat = if args.len() > 1 {
256                fill_pattern(args, 1)
257            } else {
258                vec![0]
259            };
260            let bytes: Vec<u8> = if pat.is_empty() {
261                vec![0u8; n]
262            } else {
263                (0..n).map(|i| pat[i % pat.len()]).collect()
264            };
265            Ok(from_bytes(&bytes))
266        }
267        "allocUnsafe" => Ok(from_bytes(&vec![
268            0u8;
269            super::arg_num(args, 0).max(0.0) as usize
270        ])),
271        "concat" => concat(args),
272        "isBuffer" => Ok(Value::Bool(
273            super::native_tag(&args.first().cloned().unwrap_or(Value::Undef)).as_deref()
274                == Some("Buffer"),
275        )),
276        "byteLength" => {
277            let enc = args
278                .get(1)
279                .map(|_| arg_str(args, 1))
280                .unwrap_or_else(|| "utf8".into());
281            Ok(Value::Float(
282                decode_str(&arg_str(args, 0), &enc).len() as f64
283            ))
284        }
285        _ => return None,
286    })
287}
288
289fn from(args: &[Value]) -> Result<Value, String> {
290    let v = args.first().cloned().unwrap_or(Value::Undef);
291    // Array of byte values.
292    let arr = with_host(|h| match h.get(&v) {
293        Some(JsObj::Array(items)) => Some(
294            items
295                .iter()
296                .map(|x| h.to_number(x) as u8)
297                .collect::<Vec<u8>>(),
298        ),
299        _ => None,
300    });
301    if let Some(bytes) = arr {
302        return Ok(from_bytes(&bytes));
303    }
304    // Another Buffer: copy.
305    if super::native_tag(&v).as_deref() == Some("Buffer") {
306        return Ok(from_bytes(&bytes_of(&v)));
307    }
308    // String with an optional encoding.
309    let enc = if args.len() > 1 {
310        arg_str(args, 1)
311    } else {
312        "utf8".into()
313    };
314    Ok(from_bytes(&decode_str(&arg_str(args, 0), &enc)))
315}
316
317fn concat(args: &[Value]) -> Result<Value, String> {
318    let list = with_host(
319        |h| match h.get(&args.first().cloned().unwrap_or(Value::Undef)) {
320            Some(JsObj::Array(items)) => items.clone(),
321            _ => Vec::new(),
322        },
323    );
324    let mut out = Vec::new();
325    for b in &list {
326        out.extend(bytes_of(b));
327    }
328    Ok(from_bytes(&out))
329}
330
331/// Buffer instance methods.
332pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
333    let bytes = bytes_of(recv);
334    match method {
335        "toString" => {
336            let enc = if args.is_empty() {
337                "utf8".into()
338            } else {
339                arg_str(args, 0)
340            };
341            Ok(with_host(|h| h.new_str(encode_bytes(&bytes, &enc))))
342        }
343        "toJSON" => Ok(with_host(|h| {
344            let data = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
345            let mut m = IndexMap::new();
346            m.insert("type".into(), h.new_str("Buffer"));
347            m.insert("data".into(), data);
348            h.new_object(m)
349        })),
350        "equals" => {
351            let other = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
352            Ok(Value::Bool(bytes == other))
353        }
354        "slice" | "subarray" => {
355            let (s, e) = slice_bounds(args, bytes.len());
356            Ok(from_bytes(&bytes[s..e]))
357        }
358        "readUInt8" => {
359            let i = super::arg_num(args, 0).max(0.0) as usize;
360            Ok(Value::Float(*bytes.get(i).unwrap_or(&0) as f64))
361        }
362        "includes" | "indexOf" | "lastIndexOf" => {
363            let needle = decode_str(&arg_str(args, 0), "utf8");
364            // An empty needle matches at 0 (indexOf) / len (lastIndexOf), like Node.
365            let pos = if needle.is_empty() {
366                Some(if method == "lastIndexOf" {
367                    bytes.len()
368                } else {
369                    0
370                })
371            } else if method == "lastIndexOf" {
372                bytes
373                    .windows(needle.len())
374                    .rposition(|w| w == needle.as_slice())
375            } else {
376                bytes
377                    .windows(needle.len())
378                    .position(|w| w == needle.as_slice())
379            };
380            if method == "includes" {
381                Ok(Value::Bool(pos.is_some()))
382            } else {
383                Ok(Value::Float(pos.map(|p| p as f64).unwrap_or(-1.0)))
384            }
385        }
386        // Lexicographic byte comparison → -1 / 0 / 1.
387        "compare" => {
388            let other = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
389            Ok(Value::Float(match bytes.cmp(&other) {
390                std::cmp::Ordering::Less => -1.0,
391                std::cmp::Ordering::Equal => 0.0,
392                std::cmp::Ordering::Greater => 1.0,
393            }))
394        }
395        // Big-endian / little-endian integer reads.
396        "readUInt16BE" => {
397            let i = super::arg_num(args, 0).max(0.0) as usize;
398            let v = ((*bytes.get(i).unwrap_or(&0) as u16) << 8)
399                | *bytes.get(i + 1).unwrap_or(&0) as u16;
400            Ok(Value::Float(v as f64))
401        }
402        "readUInt16LE" => {
403            let i = super::arg_num(args, 0).max(0.0) as usize;
404            let v = (*bytes.get(i).unwrap_or(&0) as u16)
405                | ((*bytes.get(i + 1).unwrap_or(&0) as u16) << 8);
406            Ok(Value::Float(v as f64))
407        }
408        // In-place writes: mutate the backing `@@bytes`, return the next offset.
409        "writeUInt8" => {
410            let mut b = bytes.clone();
411            let off = super::arg_num(args, 1).max(0.0) as usize;
412            if off < b.len() {
413                b[off] = super::arg_num(args, 0) as u8;
414            }
415            set_bytes(recv, &b);
416            Ok(Value::Float((off + 1) as f64))
417        }
418        "writeUInt16BE" | "writeUInt16LE" => {
419            let mut b = bytes.clone();
420            let val = super::arg_num(args, 0) as u16;
421            let off = super::arg_num(args, 1).max(0.0) as usize;
422            let (hi, lo) = ((val >> 8) as u8, (val & 0xff) as u8);
423            let (b0, b1) = if method == "writeUInt16BE" {
424                (hi, lo)
425            } else {
426                (lo, hi)
427            };
428            if off + 1 < b.len() {
429                b[off] = b0;
430                b[off + 1] = b1;
431            }
432            set_bytes(recv, &b);
433            Ok(Value::Float((off + 2) as f64))
434        }
435        // write(string[, offset[, length]][, encoding]) — returns bytes written.
436        "write" => {
437            let mut b = bytes.clone();
438            let src = decode_str(&arg_str(args, 0), "utf8");
439            let off = if args.len() > 1 {
440                super::arg_num(args, 1).max(0.0) as usize
441            } else {
442                0
443            };
444            let mut n = 0;
445            for (k, &byte) in src.iter().enumerate() {
446                if off + k < b.len() {
447                    b[off + k] = byte;
448                    n += 1;
449                }
450            }
451            set_bytes(recv, &b);
452            Ok(Value::Float(n as f64))
453        }
454        // fill(value[, start[, end]]) — value is a byte or a repeated string.
455        "fill" => {
456            let mut b = bytes.clone();
457            let len = b.len();
458            let start = if args.len() > 1 {
459                super::arg_num(args, 1).max(0.0) as usize
460            } else {
461                0
462            };
463            let end = if args.len() > 2 {
464                (super::arg_num(args, 2) as usize).min(len)
465            } else {
466                len
467            };
468            let pat = fill_pattern(args, 0);
469            if !pat.is_empty() {
470                for (k, slot) in b[start..end.max(start)].iter_mut().enumerate() {
471                    *slot = pat[k % pat.len()];
472                }
473            }
474            set_bytes(recv, &b);
475            Ok(recv.clone())
476        }
477        // copy(target[, targetStart[, sourceStart[, sourceEnd]]]) — returns count.
478        "copy" => {
479            let target = args.first().cloned().unwrap_or(Value::Undef);
480            let mut tb = bytes_of(&target);
481            let tstart = if args.len() > 1 {
482                super::arg_num(args, 1).max(0.0) as usize
483            } else {
484                0
485            };
486            let sstart = if args.len() > 2 {
487                super::arg_num(args, 2).max(0.0) as usize
488            } else {
489                0
490            };
491            let send = if args.len() > 3 {
492                (super::arg_num(args, 3) as usize).min(bytes.len())
493            } else {
494                bytes.len()
495            };
496            let mut n = 0;
497            for (k, &byte) in bytes[sstart..send.max(sstart)].iter().enumerate() {
498                if tstart + k < tb.len() {
499                    tb[tstart + k] = byte;
500                    n += 1;
501                }
502            }
503            set_bytes(&target, &tb);
504            Ok(Value::Float(n as f64))
505        }
506        _ => Err(crate::host::type_error(&format!(
507            "buffer.{method} is not a function"
508        ))),
509    }
510}
511
512/// The fill pattern at `args[idx]`: a string's utf-8 bytes, else a single byte.
513fn fill_pattern(args: &[Value], idx: usize) -> Vec<u8> {
514    match args.get(idx) {
515        None => vec![0],
516        Some(v) => {
517            let is_str = matches!(v, Value::Str(_))
518                || with_host(|h| matches!(h.get(v), Some(JsObj::Str(_))));
519            if is_str {
520                decode_str(&arg_str(args, idx), "utf8")
521            } else {
522                vec![super::arg_num(args, idx) as u8]
523            }
524        }
525    }
526}
527
528/// Overwrite `recv`'s backing `@@bytes` array (for in-place buffer writes).
529fn set_bytes(recv: &Value, new: &[u8]) {
530    with_host(|h| {
531        let arr = match h.get(recv) {
532            Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
533            _ => None,
534        };
535        if let Some(a) = arr {
536            if let Some(JsObj::Array(items)) = h.get_mut(&a) {
537                *items = new.iter().map(|b| Value::Float(*b as f64)).collect();
538            }
539        }
540    });
541}
542
543fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
544    let norm = |n: f64| -> usize {
545        if n < 0.0 {
546            (len as f64 + n).max(0.0) as usize
547        } else {
548            (n as usize).min(len)
549        }
550    };
551    let s = if args.is_empty() {
552        0
553    } else {
554        norm(super::arg_num(args, 0))
555    };
556    let e = if args.len() < 2 {
557        len
558    } else {
559        norm(super::arg_num(args, 1))
560    };
561    (s.min(e), e.max(s))
562}
563
564fn decode_str(s: &str, enc: &str) -> Vec<u8> {
565    match enc.to_ascii_lowercase().as_str() {
566        "hex" => from_hex(s),
567        "base64" | "base64url" => from_base64(s),
568        "ascii" | "latin1" | "binary" => s.chars().map(|c| c as u8).collect(),
569        _ => s.as_bytes().to_vec(),
570    }
571}
572
573fn encode_bytes(bytes: &[u8], enc: &str) -> String {
574    match enc.to_ascii_lowercase().as_str() {
575        "hex" => to_hex(bytes),
576        "base64" | "base64url" => to_base64(bytes),
577        "ascii" | "latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
578        _ => String::from_utf8_lossy(bytes).into_owned(),
579    }
580}