ferridriver_script/bindings/
webapi.rs1use base64::Engine as _;
11use base64::engine::GeneralPurpose;
12use base64::engine::general_purpose::GeneralPurposeConfig;
13use rquickjs::function::{Constructor, Func, Opt};
14use rquickjs::{Class, Ctx, Function, JsLifetime, TypedArray, Value, class::Trace};
15
16#[derive(Trace, JsLifetime, Default)]
18#[rquickjs::class(rename = "TextEncoder")]
19pub struct TextEncoder {}
20
21#[rquickjs::methods]
22impl TextEncoder {
23 #[qjs(constructor)]
24 pub fn new() -> Self {
25 Self {}
26 }
27
28 #[qjs(get, rename = "encoding")]
29 pub fn encoding(&self) -> &'static str {
30 "utf-8"
31 }
32
33 pub fn encode<'js>(&self, ctx: Ctx<'js>, input: Opt<String>) -> rquickjs::Result<TypedArray<'js, u8>> {
34 TypedArray::new(ctx, input.0.unwrap_or_default().into_bytes())
35 }
36}
37
38#[derive(Trace, JsLifetime)]
40#[rquickjs::class(rename = "TextDecoder")]
41pub struct TextDecoder {
42 encoding: String,
43}
44
45#[rquickjs::methods]
46impl TextDecoder {
47 #[qjs(constructor)]
48 pub fn new(label: Opt<String>) -> Self {
49 let _ = label;
52 Self {
53 encoding: "utf-8".to_string(),
54 }
55 }
56
57 #[qjs(get, rename = "encoding")]
58 pub fn encoding(&self) -> String {
59 self.encoding.clone()
60 }
61
62 pub fn decode(&self, input: Opt<Value<'_>>) -> rquickjs::Result<String> {
63 let Some(v) = input.0 else {
64 return Ok(String::new());
65 };
66 let bytes = value_to_bytes(&v)?;
67 Ok(String::from_utf8_lossy(&bytes).into_owned())
68 }
69}
70
71fn value_to_bytes(v: &Value<'_>) -> rquickjs::Result<Vec<u8>> {
74 if let Ok(ta) = TypedArray::<u8>::from_value(v.clone())
75 && let Some(b) = ta.as_bytes()
76 {
77 return Ok(b.to_vec());
78 }
79 if let Some(obj) = v.as_object()
80 && let Some(buf) = rquickjs::ArrayBuffer::from_object(obj.clone())
81 && let Some(b) = buf.as_bytes()
82 {
83 return Ok(b.to_vec());
84 }
85 if let Some(arr) = v.as_array() {
86 let mut out = Vec::with_capacity(arr.len());
87 for item in arr.iter::<u8>() {
88 out.push(item?);
89 }
90 return Ok(out);
91 }
92 Ok(Vec::new())
93}
94
95#[derive(Trace, JsLifetime)]
97#[rquickjs::class(rename = "URL")]
98pub struct Url {
99 #[qjs(skip_trace)]
100 inner: url::Url,
101}
102
103#[rquickjs::methods]
104impl Url {
105 #[qjs(constructor)]
106 pub fn new(url: String, base: Opt<String>) -> rquickjs::Result<Self> {
107 let parsed = match base.0 {
108 Some(b) => url::Url::parse(&b)
109 .and_then(|base| base.join(&url))
110 .map_err(|e| rquickjs::Error::new_from_js_message("URL", "TypeError", e.to_string()))?,
111 None => {
112 url::Url::parse(&url).map_err(|e| rquickjs::Error::new_from_js_message("URL", "TypeError", e.to_string()))?
113 },
114 };
115 Ok(Self { inner: parsed })
116 }
117
118 #[qjs(get, rename = "href")]
119 pub fn href(&self) -> String {
120 self.inner.as_str().to_string()
121 }
122
123 #[qjs(set, rename = "href")]
127 pub fn set_href(&mut self, value: String) -> rquickjs::Result<()> {
128 self.inner =
129 url::Url::parse(&value).map_err(|e| rquickjs::Error::new_from_js_message("URL", "TypeError", e.to_string()))?;
130 Ok(())
131 }
132
133 #[qjs(get, rename = "origin")]
134 pub fn origin(&self) -> String {
135 self.inner.origin().ascii_serialization()
136 }
137
138 #[qjs(get, rename = "protocol")]
139 pub fn protocol(&self) -> String {
140 format!("{}:", self.inner.scheme())
141 }
142
143 #[qjs(set, rename = "protocol")]
146 pub fn set_protocol(&mut self, value: String) {
147 let scheme = value.strip_suffix(':').unwrap_or(&value);
148 let _ = self.inner.set_scheme(scheme);
149 }
150
151 #[qjs(get, rename = "username")]
152 pub fn username(&self) -> String {
153 self.inner.username().to_string()
154 }
155
156 #[qjs(set, rename = "username")]
157 pub fn set_username(&mut self, value: String) {
158 let _ = self.inner.set_username(&value);
159 }
160
161 #[qjs(get, rename = "password")]
162 pub fn password(&self) -> String {
163 self.inner.password().unwrap_or("").to_string()
164 }
165
166 #[qjs(set, rename = "password")]
167 pub fn set_password(&mut self, value: String) {
168 let _ = self
169 .inner
170 .set_password(if value.is_empty() { None } else { Some(&value) });
171 }
172
173 #[qjs(get, rename = "hostname")]
174 pub fn hostname(&self) -> String {
175 self.inner.host_str().unwrap_or("").to_string()
176 }
177
178 #[qjs(set, rename = "hostname")]
179 pub fn set_hostname(&mut self, value: String) {
180 let _ = self.inner.set_host(Some(&value));
181 }
182
183 #[qjs(get, rename = "port")]
184 pub fn port(&self) -> String {
185 self.inner.port().map(|p| p.to_string()).unwrap_or_default()
186 }
187
188 #[qjs(set, rename = "port")]
189 pub fn set_port(&mut self, value: String) {
190 let port = value.trim().parse::<u16>().ok();
191 let _ = self.inner.set_port(port);
192 }
193
194 #[qjs(get, rename = "host")]
195 pub fn host(&self) -> String {
196 match (self.inner.host_str(), self.inner.port()) {
197 (Some(h), Some(p)) => format!("{h}:{p}"),
198 (Some(h), None) => h.to_string(),
199 (None, _) => String::new(),
200 }
201 }
202
203 #[qjs(set, rename = "host")]
204 pub fn set_host(&mut self, value: String) {
205 if let Some((h, p)) = value.rsplit_once(':') {
206 if self.inner.set_host(Some(h)).is_ok() {
207 let _ = self.inner.set_port(p.parse::<u16>().ok());
208 }
209 } else {
210 let _ = self.inner.set_host(Some(&value));
211 }
212 }
213
214 #[qjs(get, rename = "pathname")]
215 pub fn pathname(&self) -> String {
216 self.inner.path().to_string()
217 }
218
219 #[qjs(set, rename = "pathname")]
220 pub fn set_pathname(&mut self, value: String) {
221 self.inner.set_path(&value);
222 }
223
224 #[qjs(get, rename = "search")]
225 pub fn search(&self) -> String {
226 match self.inner.query() {
227 Some(q) if !q.is_empty() => format!("?{q}"),
228 _ => String::new(),
229 }
230 }
231
232 #[qjs(set, rename = "search")]
233 pub fn set_search(&mut self, value: String) {
234 let q = value.strip_prefix('?').unwrap_or(&value);
235 self.inner.set_query(if q.is_empty() { None } else { Some(q) });
236 }
237
238 #[qjs(get, rename = "hash")]
239 pub fn hash(&self) -> String {
240 match self.inner.fragment() {
241 Some(f) if !f.is_empty() => format!("#{f}"),
242 _ => String::new(),
243 }
244 }
245
246 #[qjs(set, rename = "hash")]
247 pub fn set_hash(&mut self, value: String) {
248 let f = value.strip_prefix('#').unwrap_or(&value);
249 self.inner.set_fragment(if f.is_empty() { None } else { Some(f) });
250 }
251
252 #[qjs(get, rename = "searchParams")]
255 pub fn search_params<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
256 let query = self.inner.query().unwrap_or("");
257 let ctor: Constructor<'js> = ctx.globals().get("URLSearchParams")?;
258 ctor.construct((query.to_string(),))
259 }
260
261 #[qjs(rename = "toString")]
262 pub fn to_js_string(&self) -> String {
263 self.inner.as_str().to_string()
264 }
265
266 #[qjs(rename = "toJSON")]
267 pub fn to_json(&self) -> String {
268 self.inner.as_str().to_string()
269 }
270}
271
272fn forgiving_base64_decode(input: &str) -> Result<Vec<u8>, &'static str> {
280 let mut s: String = input
281 .chars()
282 .filter(|c| !matches!(c, '\t' | '\n' | '\u{0C}' | '\r' | ' '))
283 .collect();
284 if s.ends_with('=') {
287 s.pop();
288 if s.ends_with('=') {
289 s.pop();
290 }
291 }
292 if s.len() % 4 == 1 || s.contains('=') {
293 return Err("invalid base64 length");
294 }
295 if !s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') {
296 return Err("invalid base64 character");
297 }
298 let engine = GeneralPurpose::new(
301 &base64::alphabet::STANDARD,
302 GeneralPurposeConfig::new()
303 .with_encode_padding(false)
304 .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent)
305 .with_decode_allow_trailing_bits(true),
306 );
307 engine.decode(s.as_bytes()).map_err(|_| "invalid base64")
308}
309
310pub fn install(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
314 let globals = ctx.globals();
315
316 Class::<TextEncoder>::define(&globals)?;
317 Class::<TextDecoder>::define(&globals)?;
318 Class::<Url>::define(&globals)?;
319
320 globals.set(
323 "queueMicrotask",
324 Func::from(|cb: Function<'_>| -> rquickjs::Result<()> {
325 cb.defer::<()>(())?;
326 Ok(())
327 }),
328 )?;
329
330 globals.set(
332 "btoa",
333 Func::from(|s: String| -> rquickjs::Result<String> {
334 let mut bytes = Vec::with_capacity(s.len());
335 for ch in s.chars() {
336 let c = ch as u32;
337 if c > 0xFF {
338 return Err(rquickjs::Error::new_from_js_message(
339 "btoa",
340 "InvalidCharacterError",
341 "string contains characters outside the Latin1 range".to_string(),
342 ));
343 }
344 bytes.push(c as u8);
345 }
346 Ok(base64::engine::general_purpose::STANDARD.encode(bytes))
347 }),
348 )?;
349 globals.set(
350 "atob",
351 Func::from(|s: String| -> rquickjs::Result<String> {
352 let bytes = forgiving_base64_decode(&s)
353 .map_err(|m| rquickjs::Error::new_from_js_message("atob", "InvalidCharacterError", m.to_string()))?;
354 Ok(bytes.into_iter().map(|b| b as char).collect())
355 }),
356 )?;
357
358 Ok(())
359}