pub mod blob_bytes;
pub mod compression;
pub mod form_data;
pub mod js_iterator;
pub mod performance;
pub mod timers;
use base64::Engine as _;
use base64::engine::GeneralPurpose;
use base64::engine::general_purpose::GeneralPurposeConfig;
use rquickjs::function::{Constructor, Func, This};
use rquickjs::{Class, Ctx, Filter, Object, TypedArray, Value};
pub fn init(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
let globals = ctx.globals();
globals.set(
"btoa",
Func::from(|s: String| -> rquickjs::Result<String> {
let mut bytes = Vec::with_capacity(s.len());
for ch in s.chars() {
let c = ch as u32;
if c > 0xFF {
return Err(rquickjs::Error::new_from_js_message(
"btoa",
"InvalidCharacterError",
"string contains characters outside the Latin1 range".to_string(),
));
}
bytes.push(c as u8);
}
Ok(base64::engine::general_purpose::STANDARD.encode(bytes))
}),
)?;
globals.set(
"atob",
Func::from(|s: String| -> rquickjs::Result<String> {
let bytes = forgiving_base64_decode(&s)
.map_err(|m| rquickjs::Error::new_from_js_message("atob", "InvalidCharacterError", m.to_string()))?;
Ok(bytes.into_iter().map(|b| b as char).collect())
}),
)?;
globals.set("structuredClone", Func::from(structured_clone))?;
performance::init(ctx)?;
rquickjs::Class::<form_data::FormDataJs>::define(&globals)?;
compression::install(ctx)?;
Ok(())
}
fn forgiving_base64_decode(input: &str) -> Result<Vec<u8>, &'static str> {
let mut s: String = input
.chars()
.filter(|c| !matches!(c, '\t' | '\n' | '\u{0C}' | '\r' | ' '))
.collect();
if s.ends_with('=') {
s.pop();
if s.ends_with('=') {
s.pop();
}
}
if s.len() % 4 == 1 || s.contains('=') {
return Err("invalid base64 length");
}
if !s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') {
return Err("invalid base64 character");
}
let engine = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent)
.with_decode_allow_trailing_bits(true),
);
engine.decode(s.as_bytes()).map_err(|_| "invalid base64")
}
fn structured_clone<'js>(ctx: Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Value<'js>> {
let mut seen: Vec<(Value<'js>, Value<'js>)> = Vec::new();
let realm = Realm::read(&ctx)?;
clone_value(&ctx, &realm, &value, &mut seen)
}
struct Realm<'js> {
date: Value<'js>,
regexp: Value<'js>,
map: Value<'js>,
set: Value<'js>,
object_proto: Option<Object<'js>>,
}
impl<'js> Realm<'js> {
fn read(ctx: &Ctx<'js>) -> rquickjs::Result<Self> {
let globals = ctx.globals();
let object: Value<'js> = globals.get("Object")?;
Ok(Self {
date: globals.get("Date")?,
regexp: globals.get("RegExp")?,
map: globals.get("Map")?,
set: globals.get("Set")?,
object_proto: object
.as_object()
.and_then(|o| o.get::<_, Value<'js>>("prototype").ok())
.and_then(|v| v.as_object().cloned()),
})
}
}
fn data_clone_error(ctx: &Ctx<'_>, what: &str) -> rquickjs::Error {
let ex = crate::exceptions::DOMException::new_with_name(
ctx,
crate::exceptions::DOMExceptionName::DataCloneError,
format!("{what} could not be cloned"),
);
match ex.and_then(|ex| Class::instance(ctx.clone(), ex)) {
Ok(ex) => ctx.throw(ex.into_value()),
Err(e) => e,
}
}
fn clone_value<'js>(
ctx: &Ctx<'js>,
realm: &Realm<'js>,
value: &Value<'js>,
seen: &mut Vec<(Value<'js>, Value<'js>)>,
) -> rquickjs::Result<Value<'js>> {
if value.is_function() {
return Err(data_clone_error(ctx, "a function"));
}
if value.type_of() == rquickjs::Type::Symbol {
return Err(data_clone_error(ctx, "a symbol"));
}
let Some(obj) = value.as_object() else {
return Ok(value.clone());
};
if let Some((_, clone)) = seen.iter().find(|(orig, _)| orig.as_object() == Some(obj)) {
return Ok(clone.clone());
}
if let Some(arr) = value.as_array() {
let out = rquickjs::Array::new(ctx.clone())?;
seen.push((value.clone(), out.clone().into_value()));
for i in 0..arr.len() {
let item: Value<'js> = arr.get(i)?;
out.set(i, clone_value(ctx, realm, &item, seen)?)?;
}
return Ok(out.into_value());
}
let proto = obj.get_prototype();
if proto.is_none() || proto == realm.object_proto {
let out = Object::new(ctx.clone())?;
seen.push((value.clone(), out.clone().into_value()));
for entry in obj.own_props::<Value<'js>, Value<'js>>(Filter::new().enum_only().string()) {
let (key, v) = entry?;
out.set(key, clone_value(ctx, realm, &v, seen)?)?;
}
return Ok(out.into_value());
}
if obj.is_instance_of(&realm.date) {
let ctor = Constructor::from_value(realm.date.clone())?;
let time: f64 = obj
.get::<_, rquickjs::Function<'js>>("getTime")?
.call((This(obj.clone()),))?;
return ctor.construct::<_, Value<'js>>((time,));
}
if obj.is_instance_of(&realm.regexp) {
let ctor = Constructor::from_value(realm.regexp.clone())?;
let source: String = obj.get("source")?;
let flags: String = obj.get("flags")?;
return ctor.construct::<_, Value<'js>>((source, flags));
}
if let Some(buf) = rquickjs::ArrayBuffer::from_object(obj.clone()) {
let bytes = unsafe { buf.as_bytes() }.unwrap_or_default().to_vec();
return Ok(rquickjs::ArrayBuffer::new(ctx.clone(), bytes)?.into_value());
}
if let Ok(ta) = TypedArray::<u8>::from_value(value.clone()) {
let bytes = unsafe { ta.as_bytes() }.unwrap_or_default().to_vec();
return Ok(TypedArray::new(ctx.clone(), bytes)?.into_value());
}
if obj.is_instance_of(&realm.map) {
let ctor = Constructor::from_value(realm.map.clone())?;
let out: Value<'js> = ctor.construct(())?;
seen.push((value.clone(), out.clone()));
let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
let set: rquickjs::Function<'js> = out_obj.get("set")?;
for entry in iterate_entries(ctx, obj)? {
let (k, v) = entry?;
set.call::<_, ()>((
This(out_obj.clone()),
clone_value(ctx, realm, &k, seen)?,
clone_value(ctx, realm, &v, seen)?,
))?;
}
return Ok(out);
}
if obj.is_instance_of(&realm.set) {
let ctor = Constructor::from_value(realm.set.clone())?;
let out: Value<'js> = ctor.construct(())?;
seen.push((value.clone(), out.clone()));
let out_obj = out.as_object().cloned().unwrap_or_else(|| obj.clone());
let add: rquickjs::Function<'js> = out_obj.get("add")?;
for entry in iterate_entries(ctx, obj)? {
let (k, _) = entry?;
add.call::<_, ()>((This(out_obj.clone()), clone_value(ctx, realm, &k, seen)?))?;
}
return Ok(out);
}
Err(data_clone_error(ctx, "an object that is not a plain object"))
}
#[allow(clippy::type_complexity)]
fn iterate_entries<'js>(
ctx: &Ctx<'js>,
target: &Object<'js>,
) -> rquickjs::Result<Vec<rquickjs::Result<(Value<'js>, Value<'js>)>>> {
let entries: rquickjs::Function<'js> = target.get("entries")?;
let iter: Value<'js> = entries.call((This(target.clone()),))?;
let array_ctor: Value<'js> = ctx.globals().get("Array")?;
let from: rquickjs::Function<'js> = array_ctor
.as_object()
.ok_or_else(|| rquickjs::Exception::throw_type(ctx, "Array is not an object"))?
.get("from")?;
let list: rquickjs::Array<'js> = from.call((This(array_ctor), iter))?;
Ok(
(0..list.len())
.map(|i| {
let pair: rquickjs::Array<'js> = list.get(i)?;
Ok((pair.get(0)?, pair.get(1)?))
})
.collect(),
)
}