Skip to main content

ferrijs_std/node/
bytes.rs

1//! Byte extraction: the one place a JS value becomes `Vec<u8>`.
2//!
3//! `BufferSource` (an `ArrayBuffer` or any view over one), Node's
4//! `Buffer`, an array of byte values, or a string in one of the encodings
5//! `Buffer` understands. Every consumer — `crypto`, the compression
6//! streams, `Buffer.from`, `setInputFiles` — reads through here rather
7//! than repeating the walk.
8
9use base64::Engine as _;
10use rquickjs::{ArrayBuffer, Ctx, Value};
11
12use super::throw_named;
13
14/// A `BufferSource`: an `ArrayBuffer`, or a view over one.
15///
16/// # Errors
17///
18/// A `TypeError` when the value is neither, or when the buffer is
19/// detached or the view is out of bounds.
20pub fn buffer_source_bytes(ctx: &Ctx<'_>, value: &Value<'_>) -> rquickjs::Result<Vec<u8>> {
21  if let Some(ab) = ArrayBuffer::from_value(value.clone()) {
22    // SAFETY: copied out immediately; nothing runs JS in between.
23    return unsafe { ab.as_bytes() }
24      .map(<[u8]>::to_vec)
25      .ok_or_else(|| throw_named(ctx, "TypeError", "detached ArrayBuffer"));
26  }
27  if let Some(obj) = value.as_object() {
28    let buffer: rquickjs::Result<ArrayBuffer<'_>> = obj.get("buffer");
29    if let Ok(ab) = buffer {
30      let offset: usize = obj.get("byteOffset")?;
31      let len: usize = obj.get("byteLength")?;
32      // SAFETY: both property reads happen above; from here to the
33      // `to_vec` below nothing re-enters script.
34      let bytes = unsafe { ab.as_bytes() }
35        .ok_or_else(|| throw_named(ctx, "TypeError", "detached ArrayBuffer"))?;
36      return bytes
37        .get(offset..offset + len)
38        .map(<[u8]>::to_vec)
39        .ok_or_else(|| throw_named(ctx, "TypeError", "view out of bounds"));
40    }
41  }
42  Err(throw_named(
43    ctx,
44    "TypeError",
45    "expected an ArrayBuffer or ArrayBuffer view",
46  ))
47}
48
49/// Decode a string under one of the encodings `Buffer` supports.
50fn decode(ctx: &Ctx<'_>, s: &str, encoding: &str) -> rquickjs::Result<Vec<u8>> {
51  match encoding {
52    "utf8" | "utf-8" => Ok(s.as_bytes().to_vec()),
53    "base64" => base64::engine::general_purpose::STANDARD
54      .decode(s)
55      .map_err(|e| throw_named(ctx, "TypeError", format!("invalid base64: {e}"))),
56    "hex" => (0..s.len() / 2)
57      .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16))
58      .collect::<Result<Vec<u8>, _>>()
59      .map_err(|e| throw_named(ctx, "TypeError", format!("invalid hex: {e}"))),
60    other => Err(throw_named(
61      ctx,
62      "TypeError",
63      format!("unsupported Buffer encoding {other:?} (utf8 | base64 | hex)"),
64    )),
65  }
66}
67
68/// Node's `Buffer.from` lowering: a string in `encoding`, an array of
69/// byte values, another `Buffer`, or any `BufferSource`.
70///
71/// # Errors
72///
73/// A `TypeError` for an unsupported encoding or a value that is none of
74/// those.
75pub fn value_to_bytes<'js>(
76  ctx: &Ctx<'js>,
77  value: &Value<'js>,
78  encoding: Option<&str>,
79) -> rquickjs::Result<Vec<u8>> {
80  if let Some(s) = value.as_string() {
81    return decode(ctx, &s.to_string()?, encoding.unwrap_or("utf8"));
82  }
83  if let Some(obj) = value.as_object() {
84    // A `Buffer` needs no branch of its own: it is a `Uint8Array`
85    // subclass, so the BufferSource walk below already reads it.
86    if let Some(arr) = obj.as_array() {
87      let mut out = Vec::with_capacity(arr.len());
88      for i in 0..arr.len() {
89        out.push(arr.get::<u8>(i)?);
90      }
91      return Ok(out);
92    }
93  }
94  buffer_source_bytes(ctx, value)
95}