Skip to main content

ferridriver_script/bindings/
blob.rs

1//! WHATWG `Blob` (spec subset, no deps; semantics studied from the
2//! read-only llrt reference). `new Blob(parts?, { type? })` where parts
3//! is an iterable of string / `Blob` / `Uint8Array` / `ArrayBuffer`;
4//! `size`, `type`, `text()`, `arrayBuffer()`, `bytes()`, `slice()`,
5//! `stream()` (a `ReadableStream`). Body readers are returned
6//! synchronously (await-transparent), consistent with the rest of the
7//! fetch subset.
8
9use rquickjs::function::Opt;
10use rquickjs::{Class, Ctx, Object, TypedArray, Value, class::Trace};
11
12#[derive(Trace)]
13#[rquickjs::class(rename = "Blob")]
14pub struct BlobJs {
15  #[qjs(skip_trace)]
16  data: Vec<u8>,
17  #[qjs(skip_trace)]
18  type_: String,
19}
20
21#[allow(unsafe_code)]
22unsafe impl rquickjs::JsLifetime<'_> for BlobJs {
23  type Changed<'to> = BlobJs;
24}
25
26impl BlobJs {
27  pub fn new_parts(data: Vec<u8>, type_: String) -> Self {
28    Self { data, type_ }
29  }
30
31  pub fn bytes_ref(&self) -> &[u8] {
32    &self.data
33  }
34
35  pub fn mime(&self) -> &str {
36    &self.type_
37  }
38
39  /// Bytes + mime of a JS value if it is a `Blob` instance.
40  pub fn from_js_blob(v: &Value<'_>) -> Option<(Vec<u8>, String)> {
41    Class::<BlobJs>::from_value(v)
42      .ok()
43      .map(|b| (b.borrow().data.clone(), b.borrow().type_.clone()))
44  }
45}
46
47/// Concatenate one `BlobPart` (string -> UTF-8, `Blob`/`Uint8Array`/
48/// `ArrayBuffer` -> raw bytes; anything else ignored) into `out`.
49fn push_part(out: &mut Vec<u8>, elem: &Value<'_>) {
50  if let Some(s) = elem.as_string().and_then(|s| s.to_string().ok()) {
51    out.extend_from_slice(s.as_bytes());
52    return;
53  }
54  if let Some((bytes, _)) = BlobJs::from_js_blob(elem) {
55    out.extend_from_slice(&bytes);
56    return;
57  }
58  if let Ok(ta) = TypedArray::<u8>::from_value(elem.clone()) {
59    let b: &[u8] = ta.as_ref();
60    out.extend_from_slice(b);
61    return;
62  }
63  if let Some(ab) = rquickjs::ArrayBuffer::from_value(elem.clone())
64    && let Some(b) = ab.as_bytes()
65  {
66    out.extend_from_slice(b);
67  }
68}
69
70#[rquickjs::methods(rename_all = "camelCase")]
71impl BlobJs {
72  #[qjs(constructor)]
73  pub fn new<'js>(parts: Opt<Value<'js>>, options: Opt<Object<'js>>) -> Self {
74    let mut data = Vec::new();
75    if let Some(arr) = parts.0.as_ref().and_then(|v| v.as_array()) {
76      for i in 0..arr.len() {
77        if let Ok(elem) = arr.get::<Value<'js>>(i) {
78          push_part(&mut data, &elem);
79        }
80      }
81    }
82    // Spec: a Blob's `type` is lowercased; invalid (non-ASCII-printable)
83    // becomes "".
84    let type_ = options
85      .0
86      .and_then(|o| o.get::<_, String>("type").ok())
87      .map(|t| t.to_ascii_lowercase())
88      .filter(|t| t.chars().all(|c| ('\u{20}'..='\u{7e}').contains(&c)))
89      .unwrap_or_default();
90    Self { data, type_ }
91  }
92
93  #[qjs(get, rename = "size")]
94  pub fn size(&self) -> usize {
95    self.data.len()
96  }
97
98  #[qjs(get, rename = "type")]
99  pub fn type_(&self) -> String {
100    self.type_.clone()
101  }
102
103  #[qjs(rename = "text")]
104  pub fn text(&self) -> String {
105    String::from_utf8_lossy(&self.data).into_owned()
106  }
107
108  #[qjs(rename = "arrayBuffer")]
109  pub fn array_buffer<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
110    rquickjs::ArrayBuffer::new(ctx, self.data.clone()).map(rquickjs::ArrayBuffer::into_value)
111  }
112
113  #[qjs(rename = "bytes")]
114  pub fn bytes<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
115    Ok(TypedArray::<u8>::new(ctx, self.data.clone())?.into_value())
116  }
117
118  /// `slice(start?, end?, contentType?)` — byte range (negative indices
119  /// count from the end), per spec.
120  #[qjs(rename = "slice")]
121  pub fn slice(&self, start: Opt<i64>, end: Opt<i64>, content_type: Opt<String>) -> BlobJs {
122    let len = i64::try_from(self.data.len()).unwrap_or(i64::MAX);
123    let norm = |v: i64| if v < 0 { (len + v).max(0) } else { v.min(len) };
124    let s = norm(start.0.unwrap_or(0)) as usize;
125    let e = norm(end.0.unwrap_or(len)) as usize;
126    BlobJs {
127      data: if s < e { self.data[s..e].to_vec() } else { Vec::new() },
128      type_: content_type.0.map(|t| t.to_ascii_lowercase()).unwrap_or_default(),
129    }
130  }
131
132  /// `stream()` -> a `ReadableStream` of the blob bytes.
133  #[qjs(rename = "stream")]
134  pub fn stream<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, crate::bindings::streams::ReadableStreamJs>> {
135    Class::instance(
136      ctx,
137      crate::bindings::streams::ReadableStreamJs::from_bytes(self.data.clone()),
138    )
139  }
140}