Skip to main content

ferridriver_script/bindings/
form_data.rs

1//! WHATWG `FormData` (spec subset, no deps; multipart serialization
2//! studied from the read-only llrt reference). `append`/`set`/`get`/
3//! `getAll`/`has`/`delete`/`keys`/`values`/`entries`/`forEach`; string
4//! or `Blob`/`File`-ish values. `entries`/`keys`/`values` return arrays
5//! (iterable: `for..of fd.entries()`, spread, `Array.from`, `forEach`);
6//! `[Symbol.iterator]` is the entries array. `fetch` with a `FormData`
7//! body serializes `multipart/form-data` in-binding (no core change).
8
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use rquickjs::atom::PredefinedAtom;
12use rquickjs::function::Opt;
13use rquickjs::{Class, Ctx, Function, Value, class::Trace};
14
15use crate::bindings::blob::BlobJs;
16
17#[derive(Clone)]
18enum FormEntry {
19  Text(String),
20  File {
21    bytes: Vec<u8>,
22    filename: String,
23    content_type: String,
24  },
25}
26
27#[derive(Trace, Default)]
28#[rquickjs::class(rename = "FormData")]
29pub struct FormDataJs {
30  #[qjs(skip_trace)]
31  entries: Vec<(String, FormEntry)>,
32}
33
34#[allow(unsafe_code)]
35unsafe impl rquickjs::JsLifetime<'_> for FormDataJs {
36  type Changed<'to> = FormDataJs;
37}
38
39impl FormDataJs {
40  fn coerce(value: &Value<'_>, filename: Option<String>) -> FormEntry {
41    if let Some((bytes, ct)) = BlobJs::from_js_blob(value) {
42      return FormEntry::File {
43        bytes,
44        filename: filename.unwrap_or_else(|| "blob".to_string()),
45        content_type: if ct.is_empty() {
46          "application/octet-stream".to_string()
47        } else {
48          ct
49        },
50      };
51    }
52    let s = value
53      .as_string()
54      .and_then(|s| s.to_string().ok())
55      .or_else(|| value.as_number().map(|n| n.to_string()))
56      .or_else(|| value.as_bool().map(|b| b.to_string()))
57      .unwrap_or_default();
58    FormEntry::Text(s)
59  }
60
61  fn entry_value<'js>(ctx: &Ctx<'js>, e: &FormEntry) -> rquickjs::Result<Value<'js>> {
62    match e {
63      FormEntry::Text(s) => Ok(rquickjs::String::from_str(ctx.clone(), s)?.into_value()),
64      FormEntry::File {
65        bytes, content_type, ..
66      } => {
67        let blob = Class::instance(ctx.clone(), BlobJs::new_parts(bytes.clone(), content_type.clone()))?;
68        Ok(blob.into_value())
69      },
70    }
71  }
72
73  /// `(multipart-body, content-type)` for a `fetch` `FormData` body.
74  pub fn to_multipart(&self) -> (Vec<u8>, String) {
75    use std::io::Write as _;
76    static SEQ: AtomicU64 = AtomicU64::new(0);
77    let nanos = std::time::SystemTime::now()
78      .duration_since(std::time::UNIX_EPOCH)
79      .map_or(0, |d| d.as_nanos());
80    let boundary = format!(
81      "----ferridriverFormBoundary{:x}{:x}",
82      nanos,
83      SEQ.fetch_add(1, Ordering::Relaxed)
84    );
85    let mut body = Vec::new();
86    for (name, value) in &self.entries {
87      match value {
88        FormEntry::Text(text) => {
89          let _ = write!(
90            &mut body,
91            "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{text}\r\n"
92          );
93        },
94        FormEntry::File {
95          bytes,
96          filename,
97          content_type,
98        } => {
99          let _ = write!(
100            &mut body,
101            "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"; filename=\"{filename}\"\r\nContent-Type: {content_type}\r\n\r\n"
102          );
103          body.extend_from_slice(bytes);
104          body.extend_from_slice(b"\r\n");
105        },
106      }
107    }
108    let _ = write!(&mut body, "--{boundary}--\r\n");
109    (body, format!("multipart/form-data; boundary={boundary}"))
110  }
111}
112
113#[rquickjs::methods(rename_all = "camelCase")]
114impl FormDataJs {
115  #[qjs(constructor)]
116  pub fn new() -> Self {
117    Self::default()
118  }
119
120  #[qjs(rename = "append")]
121  pub fn append(&mut self, name: String, value: Value<'_>, filename: Opt<String>) {
122    self.entries.push((name, Self::coerce(&value, filename.0)));
123  }
124
125  #[qjs(rename = "set")]
126  pub fn set(&mut self, name: String, value: Value<'_>, filename: Opt<String>) {
127    let entry = Self::coerce(&value, filename.0);
128    // Spec: replace the FIRST entry of `name` in place and drop the
129    // rest; append if none — order of the first occurrence is kept.
130    if let Some(i) = self.entries.iter().position(|(k, _)| k == &name) {
131      self.entries[i].1 = entry;
132      let mut seen = false;
133      self.entries.retain(|(k, _)| {
134        if k == &name {
135          if seen {
136            return false;
137          }
138          seen = true;
139        }
140        true
141      });
142    } else {
143      self.entries.push((name, entry));
144    }
145  }
146
147  #[qjs(rename = "has")]
148  pub fn has(&self, name: String) -> bool {
149    self.entries.iter().any(|(k, _)| k == &name)
150  }
151
152  #[qjs(rename = "delete")]
153  pub fn delete(&mut self, name: String) {
154    self.entries.retain(|(k, _)| k != &name);
155  }
156
157  #[qjs(rename = "get")]
158  pub fn get<'js>(&self, ctx: Ctx<'js>, name: String) -> rquickjs::Result<Value<'js>> {
159    match self.entries.iter().find(|(k, _)| k == &name) {
160      Some((_, e)) => Self::entry_value(&ctx, e),
161      None => Ok(Value::new_null(ctx)),
162    }
163  }
164
165  #[qjs(rename = "getAll")]
166  pub fn get_all<'js>(&self, ctx: Ctx<'js>, name: String) -> rquickjs::Result<Vec<Value<'js>>> {
167    self
168      .entries
169      .iter()
170      .filter(|(k, _)| k == &name)
171      .map(|(_, e)| Self::entry_value(&ctx, e))
172      .collect()
173  }
174
175  #[qjs(rename = "keys")]
176  pub fn keys(&self) -> Vec<String> {
177    self.entries.iter().map(|(k, _)| k.clone()).collect()
178  }
179
180  #[qjs(rename = "values")]
181  pub fn values<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Vec<Value<'js>>> {
182    self.entries.iter().map(|(_, e)| Self::entry_value(&ctx, e)).collect()
183  }
184
185  #[qjs(rename = "entries")]
186  pub fn entries<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Vec<Vec<Value<'js>>>> {
187    self
188      .entries
189      .iter()
190      .map(|(k, e)| {
191        Ok(vec![
192          rquickjs::String::from_str(ctx.clone(), k)?.into_value(),
193          Self::entry_value(&ctx, e)?,
194        ])
195      })
196      .collect()
197  }
198
199  #[qjs(rename = PredefinedAtom::SymbolIterator)]
200  pub fn js_iter<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Vec<Vec<Value<'js>>>> {
201    self.entries(ctx)
202  }
203
204  #[qjs(rename = "forEach")]
205  pub fn for_each<'js>(&self, ctx: Ctx<'js>, cb: Function<'js>) -> rquickjs::Result<()> {
206    for (k, e) in &self.entries {
207      let v = Self::entry_value(&ctx, e)?;
208      cb.call::<_, ()>((v, k.clone()))?;
209    }
210    Ok(())
211  }
212}