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::{Func, This};
20use rquickjs::{Class, Ctx, 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 clone_value(&ctx, &value, &mut seen)
119}
120
121fn data_clone_error(ctx: &Ctx<'_>, what: &str) -> rquickjs::Error {
122 let ex = crate::exceptions::DOMException::new_with_name(
123 ctx,
124 crate::exceptions::DOMExceptionName::DataCloneError,
125 format!("{what} could not be cloned"),
126 );
127 match ex.and_then(|ex| Class::instance(ctx.clone(), ex)) {
128 Ok(ex) => ctx.throw(ex.into_value()),
129 Err(e) => e,
130 }
131}
132
133fn clone_value<'js>(
134 ctx: &Ctx<'js>,
135 value: &Value<'js>,
136 seen: &mut Vec<(Value<'js>, Value<'js>)>,
137) -> rquickjs::Result<Value<'js>> {
138 if value.is_function() {
139 return Err(data_clone_error(ctx, "a function"));
140 }
141 if value.type_of() == rquickjs::Type::Symbol {
142 return Err(data_clone_error(ctx, "a symbol"));
143 }
144 let Some(obj) = value.as_object() else {
145 return Ok(value.clone());
147 };
148 if let Some((_, clone)) = seen.iter().find(|(orig, _)| orig.as_object() == Some(obj)) {
149 return Ok(clone.clone());
150 }
151
152 let globals = ctx.globals();
153 let is_a = |name: &str| -> rquickjs::Result<bool> {
154 let ctor: Value<'js> = globals.get(name)?;
155 Ok(obj.is_instance_of(&ctor))
156 };
157
158 if is_a("Date")? {
160 let ctor: rquickjs::function::Constructor<'js> = globals.get("Date")?;
161 let time: f64 = obj
162 .get::<_, rquickjs::Function<'js>>("getTime")?
163 .call((This(obj.clone()),))?;
164 return ctor.construct::<_, Value<'js>>((time,));
165 }
166 if is_a("RegExp")? {
167 let ctor: rquickjs::function::Constructor<'js> = globals.get("RegExp")?;
168 let source: String = obj.get("source")?;
169 let flags: String = obj.get("flags")?;
170 return ctor.construct::<_, Value<'js>>((source, flags));
171 }
172 if let Some(buf) = rquickjs::ArrayBuffer::from_object(obj.clone()) {
173 let bytes = unsafe { buf.as_bytes() }.unwrap_or_default().to_vec();
175 return Ok(rquickjs::ArrayBuffer::new(ctx.clone(), bytes)?.into_value());
176 }
177 if let Ok(ta) = TypedArray::<u8>::from_value(value.clone()) {
178 let bytes = unsafe { ta.as_bytes() }.unwrap_or_default().to_vec();
180 return Ok(TypedArray::new(ctx.clone(), bytes)?.into_value());
181 }
182
183 if let Some(arr) = value.as_array() {
184 let out = rquickjs::Array::new(ctx.clone())?;
185 seen.push((value.clone(), out.clone().into_value()));
186 for i in 0..arr.len() {
187 let item: Value<'js> = arr.get(i)?;
188 out.set(i, clone_value(ctx, &item, seen)?)?;
189 }
190 return Ok(out.into_value());
191 }
192
193 if is_a("Map")? {
194 let ctor: rquickjs::function::Constructor<'js> = globals.get("Map")?;
195 let out: Value<'js> = ctor.construct(())?;
196 seen.push((value.clone(), out.clone()));
197 let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
198 let set: rquickjs::Function<'js> = out_obj.get("set")?;
199 for entry in iterate_entries(ctx, obj)? {
200 let (k, v) = entry?;
201 set.call::<_, ()>((
202 This(out_obj.clone()),
203 clone_value(ctx, &k, seen)?,
204 clone_value(ctx, &v, seen)?,
205 ))?;
206 }
207 return Ok(out);
208 }
209 if is_a("Set")? {
210 let ctor: rquickjs::function::Constructor<'js> = globals.get("Set")?;
211 let out: Value<'js> = ctor.construct(())?;
212 seen.push((value.clone(), out.clone()));
213 let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
214 let add: rquickjs::Function<'js> = out_obj.get("add")?;
215 for entry in iterate_entries(ctx, obj)? {
216 let (k, _) = entry?;
217 add.call::<_, ()>((This(out_obj.clone()), clone_value(ctx, &k, seen)?))?;
218 }
219 return Ok(out);
220 }
221
222 let object_ctor: Value<'js> = globals.get("Object")?;
225 let proto = obj.get_prototype();
226 let object_proto = object_ctor
227 .as_object()
228 .and_then(|o| o.get::<_, Value<'js>>("prototype").ok())
229 .and_then(|v| v.as_object().cloned());
230 if proto.is_some() && proto != object_proto {
231 return Err(data_clone_error(ctx, "an object that is not a plain object"));
232 }
233
234 let out = Object::new(ctx.clone())?;
235 seen.push((value.clone(), out.clone().into_value()));
236 for key in obj.keys::<String>() {
237 let key = key?;
238 let v: Value<'js> = obj.get(&key)?;
239 out.set(key, clone_value(ctx, &v, seen)?)?;
240 }
241 Ok(out.into_value())
242}
243
244#[allow(clippy::type_complexity)]
248fn iterate_entries<'js>(
249 ctx: &Ctx<'js>,
250 target: &Object<'js>,
251) -> rquickjs::Result<Vec<rquickjs::Result<(Value<'js>, Value<'js>)>>> {
252 let entries: rquickjs::Function<'js> = target.get("entries")?;
253 let iter: Value<'js> = entries.call((This(target.clone()),))?;
254 let array_ctor: Value<'js> = ctx.globals().get("Array")?;
255 let from: rquickjs::Function<'js> = array_ctor
256 .as_object()
257 .ok_or_else(|| rquickjs::Exception::throw_type(ctx, "Array is not an object"))?
258 .get("from")?;
259 let list: rquickjs::Array<'js> = from.call((This(array_ctor), iter))?;
260 Ok(
261 (0..list.len())
262 .map(|i| {
263 let pair: rquickjs::Array<'js> = list.get(i)?;
264 Ok((pair.get(0)?, pair.get(1)?))
265 })
266 .collect(),
267 )
268}