1pub mod blob_bytes;
10pub mod compression;
11pub mod form_data;
12pub mod js_iterator;
13pub mod performance;
14pub mod timers;
15
16use base64::Engine as _;
17use base64::engine::GeneralPurpose;
18use base64::engine::general_purpose::GeneralPurposeConfig;
19use rquickjs::function::{Constructor, Func, This};
20use rquickjs::{Class, Ctx, Filter, Object, TypedArray, Value};
21
22pub fn init(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
29 let globals = ctx.globals();
30
31 globals.set(
33 "btoa",
34 Func::from(|s: String| -> rquickjs::Result<String> {
35 let mut bytes = Vec::with_capacity(s.len());
36 for ch in s.chars() {
37 let c = ch as u32;
38 if c > 0xFF {
39 return Err(rquickjs::Error::new_from_js_message(
40 "btoa",
41 "InvalidCharacterError",
42 "string contains characters outside the Latin1 range".to_string(),
43 ));
44 }
45 bytes.push(c as u8);
46 }
47 Ok(base64::engine::general_purpose::STANDARD.encode(bytes))
48 }),
49 )?;
50 globals.set(
51 "atob",
52 Func::from(|s: String| -> rquickjs::Result<String> {
53 let bytes = forgiving_base64_decode(&s)
54 .map_err(|m| rquickjs::Error::new_from_js_message("atob", "InvalidCharacterError", m.to_string()))?;
55 Ok(bytes.into_iter().map(|b| b as char).collect())
56 }),
57 )?;
58
59 globals.set("structuredClone", Func::from(structured_clone))?;
60
61 performance::init(ctx)?;
62
63 rquickjs::Class::<form_data::FormDataJs>::define(&globals)?;
64 compression::install(ctx)?;
65
66 Ok(())
67}
68
69fn forgiving_base64_decode(input: &str) -> Result<Vec<u8>, &'static str> {
77 let mut s: String = input
78 .chars()
79 .filter(|c| !matches!(c, '\t' | '\n' | '\u{0C}' | '\r' | ' '))
80 .collect();
81 if s.ends_with('=') {
84 s.pop();
85 if s.ends_with('=') {
86 s.pop();
87 }
88 }
89 if s.len() % 4 == 1 || s.contains('=') {
90 return Err("invalid base64 length");
91 }
92 if !s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') {
93 return Err("invalid base64 character");
94 }
95 let engine = GeneralPurpose::new(
98 &base64::alphabet::STANDARD,
99 GeneralPurposeConfig::new()
100 .with_encode_padding(false)
101 .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent)
102 .with_decode_allow_trailing_bits(true),
103 );
104 engine.decode(s.as_bytes()).map_err(|_| "invalid base64")
105}
106
107fn structured_clone<'js>(ctx: Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Value<'js>> {
117 let mut seen: Vec<(Value<'js>, Value<'js>)> = Vec::new();
118 let realm = Realm::read(&ctx)?;
119 clone_value(&ctx, &realm, &value, &mut seen)
120}
121
122struct Realm<'js> {
129 date: Value<'js>,
130 regexp: Value<'js>,
131 map: Value<'js>,
132 set: Value<'js>,
133 object_proto: Option<Object<'js>>,
134}
135
136impl<'js> Realm<'js> {
137 fn read(ctx: &Ctx<'js>) -> rquickjs::Result<Self> {
138 let globals = ctx.globals();
139 let object: Value<'js> = globals.get("Object")?;
140 Ok(Self {
141 date: globals.get("Date")?,
142 regexp: globals.get("RegExp")?,
143 map: globals.get("Map")?,
144 set: globals.get("Set")?,
145 object_proto: object
146 .as_object()
147 .and_then(|o| o.get::<_, Value<'js>>("prototype").ok())
148 .and_then(|v| v.as_object().cloned()),
149 })
150 }
151}
152
153fn data_clone_error(ctx: &Ctx<'_>, what: &str) -> rquickjs::Error {
154 let ex = crate::exceptions::DOMException::new_with_name(
155 ctx,
156 crate::exceptions::DOMExceptionName::DataCloneError,
157 format!("{what} could not be cloned"),
158 );
159 match ex.and_then(|ex| Class::instance(ctx.clone(), ex)) {
160 Ok(ex) => ctx.throw(ex.into_value()),
161 Err(e) => e,
162 }
163}
164
165fn clone_value<'js>(
166 ctx: &Ctx<'js>,
167 realm: &Realm<'js>,
168 value: &Value<'js>,
169 seen: &mut Vec<(Value<'js>, Value<'js>)>,
170) -> rquickjs::Result<Value<'js>> {
171 if value.is_function() {
172 return Err(data_clone_error(ctx, "a function"));
173 }
174 if value.type_of() == rquickjs::Type::Symbol {
175 return Err(data_clone_error(ctx, "a symbol"));
176 }
177 let Some(obj) = value.as_object() else {
178 return Ok(value.clone());
180 };
181 if let Some((_, clone)) = seen.iter().find(|(orig, _)| orig.as_object() == Some(obj)) {
182 return Ok(clone.clone());
183 }
184
185 if let Some(arr) = value.as_array() {
191 let out = rquickjs::Array::new(ctx.clone())?;
192 seen.push((value.clone(), out.clone().into_value()));
193 for i in 0..arr.len() {
194 let item: Value<'js> = arr.get(i)?;
195 out.set(i, clone_value(ctx, realm, &item, seen)?)?;
196 }
197 return Ok(out.into_value());
198 }
199
200 let proto = obj.get_prototype();
201 if proto.is_none() || proto == realm.object_proto {
203 let out = Object::new(ctx.clone())?;
204 seen.push((value.clone(), out.clone().into_value()));
205 for entry in obj.own_props::<Value<'js>, Value<'js>>(Filter::new().enum_only().string()) {
209 let (key, v) = entry?;
210 out.set(key, clone_value(ctx, realm, &v, seen)?)?;
211 }
212 return Ok(out.into_value());
213 }
214
215 if obj.is_instance_of(&realm.date) {
217 let ctor = Constructor::from_value(realm.date.clone())?;
218 let time: f64 = obj
219 .get::<_, rquickjs::Function<'js>>("getTime")?
220 .call((This(obj.clone()),))?;
221 return ctor.construct::<_, Value<'js>>((time,));
222 }
223 if obj.is_instance_of(&realm.regexp) {
224 let ctor = Constructor::from_value(realm.regexp.clone())?;
225 let source: String = obj.get("source")?;
226 let flags: String = obj.get("flags")?;
227 return ctor.construct::<_, Value<'js>>((source, flags));
228 }
229 if let Some(buf) = rquickjs::ArrayBuffer::from_object(obj.clone()) {
230 let bytes = unsafe { buf.as_bytes() }.unwrap_or_default().to_vec();
232 return Ok(rquickjs::ArrayBuffer::new(ctx.clone(), bytes)?.into_value());
233 }
234 if let Ok(ta) = TypedArray::<u8>::from_value(value.clone()) {
235 let bytes = unsafe { ta.as_bytes() }.unwrap_or_default().to_vec();
237 return Ok(TypedArray::new(ctx.clone(), bytes)?.into_value());
238 }
239
240 if obj.is_instance_of(&realm.map) {
241 let ctor = Constructor::from_value(realm.map.clone())?;
242 let out: Value<'js> = ctor.construct(())?;
243 seen.push((value.clone(), out.clone()));
244 let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
245 let set: rquickjs::Function<'js> = out_obj.get("set")?;
246 for entry in iterate_entries(ctx, obj)? {
247 let (k, v) = entry?;
248 set.call::<_, ()>((
249 This(out_obj.clone()),
250 clone_value(ctx, realm, &k, seen)?,
251 clone_value(ctx, realm, &v, seen)?,
252 ))?;
253 }
254 return Ok(out);
255 }
256 if obj.is_instance_of(&realm.set) {
257 let ctor = Constructor::from_value(realm.set.clone())?;
258 let out: Value<'js> = ctor.construct(())?;
259 seen.push((value.clone(), out.clone()));
260 let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
261 let add: rquickjs::Function<'js> = out_obj.get("add")?;
262 for entry in iterate_entries(ctx, obj)? {
263 let (k, _) = entry?;
264 add.call::<_, ()>((This(out_obj.clone()), clone_value(ctx, realm, &k, seen)?))?;
265 }
266 return Ok(out);
267 }
268
269 Err(data_clone_error(ctx, "an object that is not a plain object"))
273}
274
275#[allow(clippy::type_complexity)]
279fn iterate_entries<'js>(
280 ctx: &Ctx<'js>,
281 target: &Object<'js>,
282) -> rquickjs::Result<Vec<rquickjs::Result<(Value<'js>, Value<'js>)>>> {
283 let entries: rquickjs::Function<'js> = target.get("entries")?;
284 let iter: Value<'js> = entries.call((This(target.clone()),))?;
285 let array_ctor: Value<'js> = ctx.globals().get("Array")?;
286 let from: rquickjs::Function<'js> = array_ctor
287 .as_object()
288 .ok_or_else(|| rquickjs::Exception::throw_type(ctx, "Array is not an object"))?
289 .get("from")?;
290 let list: rquickjs::Array<'js> = from.call((This(array_ctor), iter))?;
291 Ok(
292 (0..list.len())
293 .map(|i| {
294 let pair: rquickjs::Array<'js> = list.get(i)?;
295 Ok((pair.get(0)?, pair.get(1)?))
296 })
297 .collect(),
298 )
299}