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.
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: the property reads happen above; the copy follows.
33      let bytes = unsafe { ab.as_bytes() }
34        .ok_or_else(|| throw_named(ctx, "TypeError", "detached ArrayBuffer"))?;
35      return bytes
36        .get(offset..offset + len)
37        .map(<[u8]>::to_vec)
38        .ok_or_else(|| throw_named(ctx, "TypeError", "view out of bounds"));
39    }
40  }
41  Err(throw_named(
42    ctx,
43    "TypeError",
44    "expected an ArrayBuffer or ArrayBuffer view",
45  ))
46}
47
48/// Decode a string under one of the encodings `Buffer` supports.
49fn decode(ctx: &Ctx<'_>, s: &str, encoding: &str) -> rquickjs::Result<Vec<u8>> {
50  match encoding {
51    "utf8" | "utf-8" => Ok(s.as_bytes().to_vec()),
52    "base64" => base64::engine::general_purpose::STANDARD
53      .decode(s)
54      .map_err(|e| throw_named(ctx, "TypeError", format!("invalid base64: {e}"))),
55    "hex" => (0..s.len() / 2)
56      .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16))
57      .collect::<Result<Vec<u8>, _>>()
58      .map_err(|e| throw_named(ctx, "TypeError", format!("invalid hex: {e}"))),
59    other => Err(throw_named(
60      ctx,
61      "TypeError",
62      format!("unsupported Buffer encoding {other:?} (utf8 | base64 | hex)"),
63    )),
64  }
65}
66
67/// Node's `Buffer.from` lowering: a string in `encoding`, an array of
68/// byte values, another `Buffer`, or any `BufferSource`.
69///
70/// # Errors
71///
72/// A `TypeError` for an unsupported encoding or a value that is none of
73/// those.
74pub fn value_to_bytes<'js>(
75  ctx: &Ctx<'js>,
76  value: &Value<'js>,
77  encoding: Option<&str>,
78) -> rquickjs::Result<Vec<u8>> {
79  if let Some(s) = value.as_string() {
80    return decode(ctx, &s.to_string()?, encoding.unwrap_or("utf8"));
81  }
82  if let Some(obj) = value.as_object() {
83    // A `Buffer` needs no branch of its own: it is a `Uint8Array`
84    // subclass, so the BufferSource walk below already reads it.
85    if let Some(arr) = obj.as_array() {
86      let mut out = Vec::with_capacity(arr.len());
87      for i in 0..arr.len() {
88        out.push(arr.get::<u8>(i)?);
89      }
90      return Ok(out);
91    }
92  }
93  buffer_source_bytes(ctx, value)
94}