ferridriver_script/bindings/
form_data.rs1use 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 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 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}