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 = buf.as_bytes().unwrap_or_default().to_vec();
174 return Ok(rquickjs::ArrayBuffer::new(ctx.clone(), bytes)?.into_value());
175 }
176 if let Ok(ta) = TypedArray::<u8>::from_value(value.clone()) {
177 let bytes = ta.as_bytes().unwrap_or_default().to_vec();
178 return Ok(TypedArray::new(ctx.clone(), bytes)?.into_value());
179 }
180
181 if let Some(arr) = value.as_array() {
182 let out = rquickjs::Array::new(ctx.clone())?;
183 seen.push((value.clone(), out.clone().into_value()));
184 for i in 0..arr.len() {
185 let item: Value<'js> = arr.get(i)?;
186 out.set(i, clone_value(ctx, &item, seen)?)?;
187 }
188 return Ok(out.into_value());
189 }
190
191 if is_a("Map")? {
192 let ctor: rquickjs::function::Constructor<'js> = globals.get("Map")?;
193 let out: Value<'js> = ctor.construct(())?;
194 seen.push((value.clone(), out.clone()));
195 let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
196 let set: rquickjs::Function<'js> = out_obj.get("set")?;
197 for entry in iterate_entries(ctx, obj)? {
198 let (k, v) = entry?;
199 set.call::<_, ()>((
200 This(out_obj.clone()),
201 clone_value(ctx, &k, seen)?,
202 clone_value(ctx, &v, seen)?,
203 ))?;
204 }
205 return Ok(out);
206 }
207 if is_a("Set")? {
208 let ctor: rquickjs::function::Constructor<'js> = globals.get("Set")?;
209 let out: Value<'js> = ctor.construct(())?;
210 seen.push((value.clone(), out.clone()));
211 let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
212 let add: rquickjs::Function<'js> = out_obj.get("add")?;
213 for entry in iterate_entries(ctx, obj)? {
214 let (k, _) = entry?;
215 add.call::<_, ()>((This(out_obj.clone()), clone_value(ctx, &k, seen)?))?;
216 }
217 return Ok(out);
218 }
219
220 let object_ctor: Value<'js> = globals.get("Object")?;
223 let proto = obj.get_prototype();
224 let object_proto = object_ctor
225 .as_object()
226 .and_then(|o| o.get::<_, Value<'js>>("prototype").ok())
227 .and_then(|v| v.as_object().cloned());
228 if proto.is_some() && proto != object_proto {
229 return Err(data_clone_error(ctx, "an object that is not a plain object"));
230 }
231
232 let out = Object::new(ctx.clone())?;
233 seen.push((value.clone(), out.clone().into_value()));
234 for key in obj.keys::<String>() {
235 let key = key?;
236 let v: Value<'js> = obj.get(&key)?;
237 out.set(key, clone_value(ctx, &v, seen)?)?;
238 }
239 Ok(out.into_value())
240}
241
242#[allow(clippy::type_complexity)]
246fn iterate_entries<'js>(
247 ctx: &Ctx<'js>,
248 target: &Object<'js>,
249) -> rquickjs::Result<Vec<rquickjs::Result<(Value<'js>, Value<'js>)>>> {
250 let entries: rquickjs::Function<'js> = target.get("entries")?;
251 let iter: Value<'js> = entries.call((This(target.clone()),))?;
252 let array_ctor: Value<'js> = ctx.globals().get("Array")?;
253 let from: rquickjs::Function<'js> = array_ctor
254 .as_object()
255 .ok_or_else(|| rquickjs::Exception::throw_type(ctx, "Array is not an object"))?
256 .get("from")?;
257 let list: rquickjs::Array<'js> = from.call((This(array_ctor), iter))?;
258 Ok(
259 (0..list.len())
260 .map(|i| {
261 let pair: rquickjs::Array<'js> = list.get(i)?;
262 Ok((pair.get(0)?, pair.get(1)?))
263 })
264 .collect(),
265 )
266}