use crate::host::{fmt_number, with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
pub const STATIC_METHODS: &[&str] = &["from", "of", "isView"];
pub const UINT8_STATIC_METHODS: &[&str] = &["from", "of", "isView", "fromBase64", "fromHex"];
pub const UINT8_PROTOTYPE_METHODS: &[&str] = &["toBase64", "setFromBase64", "toHex", "setFromHex"];
pub fn static_methods(kind: &str) -> &'static [&'static str] {
if kind == "Uint8Array" {
UINT8_STATIC_METHODS
} else {
STATIC_METHODS
}
}
pub const PROTOTYPE_METHODS: &[&str] = &[
"at",
"copyWithin",
"entries",
"every",
"fill",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"forEach",
"includes",
"indexOf",
"join",
"keys",
"lastIndexOf",
"map",
"reduce",
"reduceRight",
"reverse",
"set",
"slice",
"some",
"sort",
"subarray",
"toReversed",
"toSorted",
"toString",
"values",
"with",
];
pub fn is_ctor(name: &str) -> bool {
ELEMENT_KINDS.contains(&name) || matches!(name, "ArrayBuffer" | "DataView")
}
pub const ELEMENT_KINDS: &[&str] = &[
"Uint8Array",
"Int8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
];
pub fn bytes_per_element(kind: &str) -> usize {
match kind {
"Int8Array" | "Uint8Array" | "Uint8ClampedArray" => 1,
"Int16Array" | "Uint16Array" => 2,
"Int32Array" | "Uint32Array" | "Float32Array" => 4,
"Float64Array" | "BigInt64Array" | "BigUint64Array" => 8,
_ => 1,
}
}
fn coerce(kind: &str, n: f64) -> f64 {
match kind {
"Int8Array" => (n as i64 as i8) as f64,
"Uint8Array" => (n as i64 as u8) as f64,
"Uint8ClampedArray" => {
if n.is_nan() {
0.0
} else {
n.round().clamp(0.0, 255.0)
}
}
"Int16Array" => (n as i64 as i16) as f64,
"Uint16Array" => (n as i64 as u16) as f64,
"Int32Array" => (n as i64 as i32) as f64,
"Uint32Array" => (n as i64 as u32) as f64,
"Float32Array" => n as f32 as f64,
_ => n, }
}
pub fn is_bigint_kind(kind: &str) -> bool {
matches!(kind, "BigInt64Array" | "BigUint64Array")
}
fn coerce_val(kind: &str, v: &Value) -> Result<Value, String> {
if !is_bigint_kind(kind) {
return Ok(Value::Float(coerce(kind, with_host(|h| h.to_number(v)))));
}
let big = crate::builtins::to_bigint(v)?;
Ok(with_host(|h| h.new_bigint(wrap_bigint(kind, big))))
}
fn wrap_bigint(kind: &str, b: num_bigint::BigInt) -> num_bigint::BigInt {
use num_traits::cast::ToPrimitive;
let modulus = num_bigint::BigInt::from(1u128 << 64);
let mut m = b % &modulus;
if m.sign() == num_bigint::Sign::Minus {
m += &modulus;
}
let raw = m.to_u64().unwrap_or(0);
if kind == "BigInt64Array" {
num_bigint::BigInt::from(raw as i64)
} else {
num_bigint::BigInt::from(raw)
}
}
fn bigint_of(v: &Value) -> num_bigint::BigInt {
with_host(|h| match h.get(v) {
Some(JsObj::BigInt(b)) => b.clone(),
_ => num_bigint::BigInt::from(0),
})
}
fn same_element(stored: &Value, needle: &Value, nan_matches: bool) -> bool {
if nan_matches {
if let (Value::Float(a), Value::Float(b)) = (stored, needle) {
if a.is_nan() && b.is_nan() {
return true;
}
}
}
with_host(|h| h.strict_eq(stored, needle))
}
fn zero_of(kind: &str) -> Value {
if is_bigint_kind(kind) {
with_host(|h| h.new_bigint(num_bigint::BigInt::from(0)))
} else {
Value::Float(0.0)
}
}
fn num(v: &Value) -> f64 {
with_host(|h| h.to_number(v))
}
pub fn elem_values(v: &Value) -> Vec<Value> {
let Some(tag) = super::native_tag(v) else {
return Vec::new();
};
if tag == "TypedArray" {
let kind = kind_of(v);
let bpe = bytes_per_element(&kind);
return (0..view_len(v))
.map(|i| {
view_bytes(v, i * bpe, bpe)
.map(|b| decode(&kind, &b))
.unwrap_or(Value::Undef)
})
.collect();
}
if tag != "Buffer" {
return Vec::new();
}
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
})
}
fn make(kind: &str, elems: Vec<Value>) -> Value {
let bpe = bytes_per_element(kind);
let len = elems.len();
let buf = new_array_buffer(len * bpe);
let view = make_view(kind, &buf, 0, len);
for (i, e) in elems.iter().enumerate() {
write_view_bytes(&view, i * bpe, &encode(kind, e));
}
view
}
fn make_view(kind: &str, buf: &Value, byte_off: usize, len: usize) -> Value {
with_host(|h| {
let bpe = bytes_per_element(kind);
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("TypedArray"));
m.insert("@@kind".into(), h.new_str(kind));
m.insert("@@buffer".into(), buf.clone());
m.insert("buffer".into(), buf.clone());
m.insert("length".into(), Value::Float(len as f64));
m.insert("byteLength".into(), Value::Float((len * bpe) as f64));
m.insert("byteOffset".into(), Value::Float(byte_off as f64));
m.insert("BYTES_PER_ELEMENT".into(), Value::Float(bpe as f64));
let obj = h.new_object(m);
h.ensure_native_protos();
if let Some(p) = h.native_proto(kind) {
h.set_proto(&obj, p);
}
for k in [
"buffer",
"length",
"byteLength",
"byteOffset",
"BYTES_PER_ELEMENT",
] {
h.hide_prop(&obj, k);
}
obj
})
}
fn integer_or_infinity(n: f64) -> f64 {
if n.is_nan() {
0.0
} else {
n.trunc() + 0.0
}
}
fn to_index(n: f64) -> Option<usize> {
let i = integer_or_infinity(n);
(0.0..=9_007_199_254_740_991.0)
.contains(&i)
.then_some(i as usize)
}
fn is_primitive(v: &Value) -> bool {
match v {
Value::Obj(_) => with_host(|h| {
matches!(
h.get(v),
Some(JsObj::Str(_))
| Some(JsObj::Null)
| Some(JsObj::BigInt(_))
| Some(JsObj::Symbol { .. })
)
}),
_ => true,
}
}
pub fn construct(kind: &str, args: &[Value]) -> Result<Value, String> {
if kind == "ArrayBuffer" {
let n = to_index(super::arg_num(args, 0))
.ok_or_else(|| crate::host::range_error("Invalid array buffer length"))?;
let max = match args.get(1) {
Some(opts) => {
crate::builtins::get_property(opts, "maxByteLength").unwrap_or(Value::Undef)
}
None => Value::Undef,
};
let max_len = match max {
Value::Undef => None,
_ => match to_index(with_host(|h| h.to_number(&max))) {
Some(m) if m >= n => Some(m),
_ => return Err(crate::host::range_error("Invalid array buffer max length")),
},
};
let ab = new_array_buffer(n);
if let Some(m) = max_len {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(&ab) {
p.insert("@@maxByteLength".into(), Value::Float(m as f64));
p.insert("maxByteLength".into(), Value::Float(m as f64));
p.insert("resizable".into(), Value::Bool(true));
}
h.hide_prop(&ab, "maxByteLength");
h.hide_prop(&ab, "resizable");
});
}
return Ok(ab);
}
if let Some(first) = args.first() {
if super::native_tag(first).as_deref() == Some("ArrayBuffer") {
if is_detached(first) {
return Err(crate::host::type_error(
"Cannot perform Construct on a detached ArrayBuffer",
));
}
let bpe = bytes_per_element(kind);
let total = buffer_byte_length(first);
let off_n = super::arg_num(args, 1);
let off = to_index(off_n).ok_or_else(|| {
crate::host::range_error(&format!(
"Start offset {} is outside the bounds of the buffer",
fmt_number(off_n)
))
})?;
if off % bpe != 0 {
return Err(crate::host::range_error(&format!(
"start offset of {kind} should be a multiple of {bpe}"
)));
}
let len = match args.get(2) {
Some(Value::Undef) | None => {
if total % bpe != 0 {
return Err(crate::host::range_error(&format!(
"byte length of {kind} should be a multiple of {bpe}"
)));
}
if off > total {
return Err(crate::host::range_error(&format!(
"Start offset {off} is outside the bounds of the buffer"
)));
}
(total - off) / bpe
}
Some(_) => {
let len_n = super::arg_num(args, 2);
let bad = || {
crate::host::range_error(&format!(
"Invalid typed array length: {}",
fmt_number(len_n)
))
};
let len = to_index(len_n).ok_or_else(bad)?;
if off + len * bpe > total {
return Err(bad());
}
len
}
};
return Ok(make_view(kind, first, off, len));
}
}
let elems = build_elems(kind, args)?;
Ok(make(kind, elems))
}
pub const DATAVIEW_METHODS: &[&str] = &[
"getInt8",
"getUint8",
"getInt16",
"getUint16",
"getInt32",
"getUint32",
"getFloat32",
"getFloat64",
"getBigInt64",
"getBigUint64",
"setInt8",
"setUint8",
"setInt16",
"setUint16",
"setInt32",
"setUint32",
"setFloat32",
"setFloat64",
"setBigInt64",
"setBigUint64",
];
pub fn construct_dataview(args: &[Value]) -> Result<Value, String> {
let buf = args.first().cloned().unwrap_or(Value::Undef);
if super::native_tag(&buf).as_deref() != Some("ArrayBuffer") {
return Err(crate::host::type_error(
"First argument to DataView constructor must be an ArrayBuffer",
));
}
let total = buffer_byte_length(&buf);
let off_n = super::arg_num(args, 1);
let outside = |n: f64| {
crate::host::range_error(&format!(
"Start offset {} is outside the bounds of the buffer",
fmt_number(integer_or_infinity(n))
))
};
let off = to_index(off_n).ok_or_else(|| outside(off_n))?;
if off > total {
return Err(outside(off_n));
}
let bad_len = |n: f64| {
crate::host::range_error(&format!(
"Invalid DataView length {}",
fmt_number(integer_or_infinity(n))
))
};
let len = match args.get(2) {
Some(Value::Undef) | None => total - off,
Some(_) => {
let len_n = super::arg_num(args, 2);
let len = to_index(len_n).ok_or_else(|| bad_len(len_n))?;
if off + len > total {
return Err(bad_len(len_n));
}
len
}
};
Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("DataView"));
m.insert("@@buffer".into(), buf.clone());
m.insert("buffer".into(), buf.clone());
m.insert("byteOffset".into(), Value::Float(off as f64));
m.insert("byteLength".into(), Value::Float(len as f64));
let obj = h.new_object(m);
for k in ["buffer", "byteOffset", "byteLength"] {
h.hide_prop(&obj, k);
}
h.ensure_native_protos();
if let Some(p) = h.ensure_ctor_proto("DataView") {
h.set_proto(&obj, p);
}
obj
}))
}
pub fn dataview_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
if view_detached(recv) {
return Err(detached_error("DataView.prototype", method, false));
}
let Some(spec) = method.get(3..) else {
return Err(crate::host::type_error(&format!(
"{method} is not a function"
)));
};
let width = match spec {
"Int8" | "Uint8" => 1,
"Int16" | "Uint16" => 2,
"Int32" | "Uint32" | "Float32" => 4,
"Float64" | "BigInt64" | "BigUint64" => 8,
_ => {
return Err(crate::host::type_error(&format!(
"{method} is not a function"
)))
}
};
let is_get = method.starts_with("get");
let requested = super::arg_num(args, 0);
let requested = if requested.is_nan() {
0.0
} else {
requested.trunc()
};
let span = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("byteLength").map(|l| h.to_number(l)).unwrap_or(0.0),
_ => 0.0,
});
if requested < 0.0 || requested + width as f64 > span {
return Err(crate::host::range_error(
"Offset is outside the bounds of the DataView",
));
}
let at = requested as usize;
let le = with_host(|h| {
h.truthy(
args.get(if is_get { 1 } else { 2 })
.unwrap_or(&Value::Undef),
)
});
if is_get {
let mut b = view_bytes(recv, at, width).unwrap_or_else(|| vec![0; width]);
if !le {
b.reverse();
}
return Ok(match spec {
"Int8" => Value::Float(b[0] as i8 as f64),
"Uint8" => Value::Float(b[0] as f64),
"Int16" => Value::Float(i16::from_le_bytes([b[0], b[1]]) as f64),
"Uint16" => Value::Float(u16::from_le_bytes([b[0], b[1]]) as f64),
"Int32" => Value::Float(i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
"Uint32" => Value::Float(u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
"Float32" => Value::Float(f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
"Float64" => Value::Float(f64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]))),
"BigInt64" => {
let raw = i64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
}
_ => {
let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
}
});
}
let val = args.get(1).cloned().unwrap_or(Value::Undef);
let mut b = match spec {
"BigInt64" | "BigUint64" => {
use num_traits::cast::ToPrimitive;
let big = crate::builtins::to_bigint(&val)?;
let raw = if spec == "BigInt64" {
big.to_i64().unwrap_or(0) as u64
} else {
big.to_u64().unwrap_or(0)
};
raw.to_le_bytes().to_vec()
}
_ => {
let n = with_host(|h| h.to_number(&val));
match spec {
"Int8" | "Uint8" => vec![n as i64 as u8],
"Int16" | "Uint16" => (n as i64 as u16).to_le_bytes().to_vec(),
"Int32" | "Uint32" => (n as i64 as u32).to_le_bytes().to_vec(),
"Float32" => (n as f32).to_le_bytes().to_vec(),
_ => n.to_le_bytes().to_vec(),
}
}
};
if !le {
b.reverse();
}
write_view_bytes(recv, at, &b);
Ok(Value::Undef)
}
pub fn buffer_resize(ab: &Value, args: &[Value]) -> Result<Value, String> {
let max = with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => p.get("@@maxByteLength").map(|m| h.to_number(m) as usize),
_ => None,
})
.ok_or_else(|| {
crate::host::type_error(
"ArrayBuffer.prototype.resize called on a non-resizable ArrayBuffer",
)
})?;
let n = super::arg_num(args, 0).max(0.0) as usize;
if n > max {
return Err(crate::host::range_error("Invalid array buffer length"));
}
let store = store_of(ab);
with_host(|h| {
if let Some(a) = store {
if let Some(JsObj::Array(items)) = h.get_mut(&a) {
items.resize(n, Value::Float(0.0));
}
}
if let Some(JsObj::Object(p)) = h.get_mut(ab) {
p.insert("byteLength".into(), Value::Float(n as f64));
}
});
Ok(Value::Undef)
}
pub fn write_buffer_bytes(ab: &Value, bytes: &[u8]) {
let Some(store) = store_of(ab) else { return };
with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(&store) {
*items = bytes.iter().map(|b| Value::Float(*b as f64)).collect();
}
if let Some(JsObj::Object(p)) = h.get_mut(ab) {
p.insert("byteLength".into(), Value::Float(bytes.len() as f64));
}
});
}
pub fn buffer_store(ab: &Value) -> Option<Value> {
store_of(ab)
}
pub fn buffer_bytes_snapshot(ab: &Value) -> Option<Vec<u8>> {
let store = store_of(ab)?;
with_host(|h| match h.get(&store) {
Some(JsObj::Array(items)) => {
Some(items.iter().map(|x| h.to_number(x) as i64 as u8).collect())
}
_ => None,
})
}
pub fn buffer_byte_length(ab: &Value) -> usize {
with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
},
_ => 0,
})
}
pub fn buffer_slice(ab: &Value, args: &[Value]) -> Value {
let total = buffer_byte_length(ab) as i64;
let idx = |v: Option<&Value>, dflt: i64| -> usize {
let n = match v {
None | Some(Value::Undef) => dflt,
Some(x) => with_host(|h| h.to_number(x)) as i64,
};
(if n < 0 { total + n } else { n }).clamp(0, total) as usize
};
let start = idx(args.first(), 0);
let end = idx(args.get(1), total).max(start);
let out = new_array_buffer(end - start);
let src = with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
Some(JsObj::Array(items)) => items[start..end].to_vec(),
_ => Vec::new(),
},
_ => Vec::new(),
});
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get(&out) {
if let Some(arr) = p.get("@@bytes").cloned() {
if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
*items = src;
}
}
}
});
out
}
fn build_elems(kind: &str, args: &[Value]) -> Result<Vec<Value>, String> {
match args.first() {
None | Some(Value::Undef) => Ok(Vec::new()),
Some(v) if is_primitive(v) => {
let n_raw = super::arg_num(args, 0);
let n = to_index(n_raw).ok_or_else(|| {
crate::host::range_error(&format!(
"Invalid typed array length: {}",
fmt_number(n_raw)
))
})?;
Ok(vec![zero_of(kind); n])
}
Some(v) => {
let items = match super::native_tag(v).as_deref() {
Some("TypedArray") | Some("Buffer") => elem_values(v),
_ => crate::host::iter_all(v).unwrap_or_default(),
};
items.iter().map(|x| coerce_val(kind, x)).collect()
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum LastChunk {
Loose,
Strict,
StopBeforePartial,
}
fn base64_options(opt: Option<&Value>) -> Result<(bool, LastChunk), String> {
let Some(o) = opt.filter(|v| !matches!(v, Value::Undef)) else {
return Ok((false, LastChunk::Loose));
};
if !with_host(|h| matches!(h.get(o), Some(JsObj::Object(_)))) {
return Err(crate::host::type_error("invalid_argument"));
}
let read = |k: &str| {
with_host(|h| match h.get(o) {
Some(JsObj::Object(p)) => p.get(k).filter(|v| !matches!(v, Value::Undef)).cloned(),
_ => None,
})
};
let url = match read("alphabet") {
None => false,
Some(v) => match with_host(|h| h.str_of(&v)).as_str() {
"base64" => false,
"base64url" => true,
other => return Err(crate::host::type_error(&format!("invalid option {other}"))),
},
};
let last = match read("lastChunkHandling") {
None => LastChunk::Loose,
Some(v) => match with_host(|h| h.str_of(&v)).as_str() {
"loose" => LastChunk::Loose,
"strict" => LastChunk::Strict,
"stop-before-partial" => LastChunk::StopBeforePartial,
other => return Err(crate::host::type_error(&format!("invalid option {other}"))),
},
};
Ok((url, last))
}
const B64_BAD: &str =
"SyntaxError: Found a character that cannot be part of a valid base64 string.";
const B64_SINGLE: &str =
"SyntaxError: The base64 input terminates with a single character, excluding padding (=).";
fn decode_base64_strict(s: &str, url: bool, last: LastChunk) -> Result<(Vec<u8>, usize), String> {
let value = |c: char| -> Option<u32> {
let table = if url {
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
} else {
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
};
table.find(c).map(|i| i as u32)
};
let chars: Vec<char> = s.chars().collect();
let mut out = Vec::new();
let mut chunk: Vec<u32> = Vec::new();
let mut consumed = 0usize;
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
if c.is_ascii_whitespace() {
i += 1;
continue;
}
if c == '=' {
let pads = chars[i..].iter().filter(|c| **c == '=').count();
let rest_ok = chars[i..]
.iter()
.all(|c| *c == '=' || c.is_ascii_whitespace());
let want = 4 - chunk.len();
if !rest_ok || chunk.len() < 2 || pads != want {
return Err(B64_BAD.into());
}
out.extend(flush_base64_chunk(&chunk));
return Ok((out, chars.len()));
}
let Some(v) = value(c) else {
return Err(B64_BAD.into());
};
chunk.push(v);
i += 1;
if chunk.len() == 4 {
out.extend(flush_base64_chunk(&chunk));
chunk.clear();
consumed = i;
}
}
match chunk.len() {
0 => Ok((out, consumed)),
1 if last != LastChunk::StopBeforePartial => Err(B64_SINGLE.into()),
_ if last == LastChunk::StopBeforePartial => Ok((out, consumed)),
1 => Ok((out, consumed)),
_ if last == LastChunk::Strict => Err(B64_SINGLE.into()),
_ => {
out.extend(flush_base64_chunk(&chunk));
Ok((out, chars.len()))
}
}
}
fn flush_base64_chunk(chunk: &[u32]) -> Vec<u8> {
let mut acc = 0u32;
for v in chunk {
acc = (acc << 6) | v;
}
let bytes = chunk.len() - 1;
acc <<= 6 * (4 - chunk.len());
let all = [(acc >> 16) as u8, (acc >> 8) as u8, acc as u8];
all[..bytes].to_vec()
}
const HEX_BAD: &str = "SyntaxError: Input string must contain hex characters in even length";
fn decode_hex_strict(s: &str) -> Result<Vec<u8>, String> {
let chars: Vec<char> = s.chars().collect();
if chars.len() % 2 != 0 || !chars.iter().all(|c| c.is_ascii_hexdigit()) {
return Err(HEX_BAD.into());
}
Ok(chars
.chunks(2)
.map(|p| {
let hi = p[0].to_digit(16).expect("checked");
let lo = p[1].to_digit(16).expect("checked");
(hi * 16 + lo) as u8
})
.collect())
}
fn base64_input(args: &[Value]) -> Result<String, String> {
let v = args.first().cloned().unwrap_or(Value::Undef);
let is_str = matches!(v, Value::Str(_))
|| with_host(|h| matches!(h.get(&v), Some(crate::host::JsObj::Str(_))));
if !is_str {
return Err(crate::host::type_error("input argument must be a string"));
}
Ok(with_host(|h| h.str_of(&v)))
}
fn from_base64_static(method: &str, args: &[Value]) -> Result<Value, String> {
let s = base64_input(args)?;
let bytes = if method == "fromHex" {
decode_hex_strict(&s)?
} else {
let (url, last) = base64_options(args.get(1))?;
decode_base64_strict(&s, url, last)?.0
};
Ok(make(
"Uint8Array",
bytes.iter().map(|b| Value::Float(*b as f64)).collect(),
))
}
pub fn static_call(kind: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
if matches!(method, "fromBase64" | "fromHex") {
if kind != "Uint8Array" {
return None;
}
return Some(from_base64_static(method, args));
}
Some(match method {
"of" => args
.iter()
.map(|x| coerce_val(kind, x))
.collect::<Result<Vec<Value>, String>>()
.map(|e| make(kind, e)),
"from" => from(kind, args),
"isView" => Ok(Value::Bool(with_host(|h| {
matches!(
h.get(&args.first().cloned().unwrap_or(Value::Undef)),
Some(crate::host::JsObj::Object(p))
if matches!(
p.get("@@native").map(|t| h.str_of(t)).as_deref(),
Some("TypedArray") | Some("Buffer") | Some("DataView")
)
)
}))),
_ => return None,
})
}
fn from(kind: &str, args: &[Value]) -> Result<Value, String> {
let src = args.first().cloned().unwrap_or(Value::Undef);
let map_fn = args
.get(1)
.cloned()
.filter(|f| with_host(|h| crate::host::is_callable(h, f)));
let items = if let Some(e) = elems_of(&src) {
e.into_iter().map(Value::Float).collect()
} else {
crate::host::iter_all(&src)
.unwrap_or_else(|_| crate::builtins::array_like_items(&src))
};
let mut out = Vec::with_capacity(items.len());
for (i, it) in items.into_iter().enumerate() {
let mapped = match &map_fn {
Some(f) => crate::host::invoke(f, vec![it, Value::Float(i as f64)], None)?,
None => it,
};
out.push(coerce_val(kind, &mapped)?);
}
Ok(make(kind, out))
}
pub fn elems_of(v: &Value) -> Option<Vec<f64>> {
let tag = super::native_tag(v)?;
if !matches!(tag.as_str(), "TypedArray" | "Buffer") {
return None;
}
let vals = elem_values(v);
Some(with_host(|h| vals.iter().map(|x| h.to_number(x)).collect()))
}
pub fn index_len(v: &Value) -> Option<usize> {
match super::native_tag(v)?.as_str() {
"TypedArray" => Some(view_len(v)),
"Buffer" => with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|a| h.get(a)) {
Some(JsObj::Array(items)) => Some(items.len()),
_ => None,
},
_ => None,
}),
_ => None,
}
}
pub fn has_index(v: &Value, key: &str) -> Option<bool> {
let len = index_len(v)?;
Some(key.parse::<usize>().map(|i| i < len).unwrap_or(false))
}
pub fn kind_of(recv: &Value) -> String {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p
.get("@@kind")
.map(|v| h.str_of(v))
.unwrap_or_else(|| "Uint8Array".into()),
_ => "Uint8Array".into(),
})
}
pub fn new_array_buffer(n: usize) -> Value {
with_host(|h| {
let arr = h.new_array(vec![Value::Float(0.0); n]);
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("ArrayBuffer"));
m.insert("@@bytes".into(), arr);
m.insert("byteLength".into(), Value::Float(n as f64));
m.insert("detached".into(), Value::Bool(false));
m.insert("resizable".into(), Value::Bool(false));
m.insert("maxByteLength".into(), Value::Float(n as f64));
let obj = h.new_object(m);
for k in ["byteLength", "detached", "resizable", "maxByteLength"] {
h.hide_prop(&obj, k);
}
if let Some(p) = h.ensure_ctor_proto("ArrayBuffer") {
h.set_proto(&obj, p);
}
obj
})
}
pub fn is_detached(ab: &Value) -> bool {
with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => p.get("detached").map(|v| h.truthy(v)).unwrap_or(false),
_ => false,
})
}
pub fn view_detached(v: &Value) -> bool {
with_host(|h| view_detached_h(h, v))
}
pub fn view_detached_h(h: &crate::host::JsHost, v: &Value) -> bool {
let buf = match h.get(v) {
Some(JsObj::Object(p)) => p.get("@@buffer").cloned(),
_ => None,
};
match buf.and_then(|b| match h.get(&b) {
Some(JsObj::Object(p)) => p.get("detached").cloned(),
_ => None,
}) {
Some(d) => h.truthy(&d),
None => false,
}
}
pub fn detach_buffer(ab: &Value) {
detach(ab)
}
fn detach(ab: &Value) {
with_host(|h| {
let empty = h.new_array(Vec::new());
if let Some(JsObj::Object(p)) = h.get_mut(ab) {
p.insert("@@bytes".into(), empty);
p.insert("byteLength".into(), Value::Float(0.0));
p.insert("detached".into(), Value::Bool(true));
}
h.hide_prop(ab, "byteLength");
h.hide_prop(ab, "detached");
});
}
pub fn buffer_transfer(ab: &Value, args: &[Value], fixed: bool) -> Result<Value, String> {
let method = if fixed {
"transferToFixedLength"
} else {
"transfer"
};
if is_detached(ab) {
return Err(crate::host::type_error(&format!(
"Cannot perform ArrayBuffer.prototype.{method} on a detached ArrayBuffer"
)));
}
let old = byte_len_of(ab);
let new_len = match args.first().filter(|v| !matches!(v, Value::Undef)) {
Some(v) => with_host(|h| h.to_number(v)).max(0.0) as usize,
None => old,
};
let mut bytes = view_bytes_of_buffer(ab, old);
bytes.resize(new_len, 0);
let out = new_array_buffer(new_len);
write_buffer_bytes(&out, &bytes);
if !fixed {
let resizable = with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => p.contains_key("@@maxByteLength"),
_ => false,
});
if resizable {
let max = with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => p.get("@@maxByteLength").cloned(),
_ => None,
});
if let Some(max) = max {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(&out) {
p.insert("@@maxByteLength".into(), max);
}
});
}
}
}
detach(ab);
Ok(out)
}
fn byte_len_of(ab: &Value) -> usize {
with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => {
p.get("byteLength").map(|l| h.to_number(l)).unwrap_or(0.0) as usize
}
_ => 0,
})
}
fn view_bytes_of_buffer(ab: &Value, n: usize) -> Vec<u8> {
let Some(store) = store_of(ab) else {
return Vec::new();
};
with_host(|h| match h.get(&store) {
Some(JsObj::Array(items)) => items
.iter()
.take(n)
.map(|x| h.to_number(x) as i64 as u8)
.collect(),
_ => Vec::new(),
})
}
pub fn detached_error(label: &str, method: &str, buffer_only: bool) -> String {
let tail = if buffer_only {
"a detached ArrayBuffer"
} else {
"a detached or out-of-bounds ArrayBuffer"
};
let method = match method {
"@@iterator" => "values",
other => other,
};
crate::host::type_error(&format!("Cannot perform {label}.{method} on {tail}"))
}
fn store_of(ab: &Value) -> Option<Value> {
with_host(|h| match h.get(ab) {
Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
_ => None,
})
}
fn view_base(v: &Value) -> Option<(Value, usize)> {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => {
let buf = p.get("@@buffer").cloned()?;
let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0);
Some((buf, off.max(0.0) as usize))
}
_ => None,
})
}
pub fn view_bytes(v: &Value, at: usize, n: usize) -> Option<Vec<u8>> {
let (buf, off) = view_base(v)?;
let store = store_of(&buf)?;
with_host(|h| match h.get(&store) {
Some(JsObj::Array(items)) => {
let start = off + at;
if start + n > items.len() {
return None;
}
Some(
items[start..start + n]
.iter()
.map(|x| h.to_number(x) as i64 as u8)
.collect(),
)
}
_ => None,
})
}
pub fn write_view_bytes(v: &Value, at: usize, bytes: &[u8]) -> bool {
let Some((buf, off)) = view_base(v) else {
return false;
};
let Some(store) = store_of(&buf) else {
return false;
};
with_host(|h| match h.get_mut(&store) {
Some(JsObj::Array(items)) => {
let start = off + at;
if start + bytes.len() > items.len() {
return false;
}
for (i, b) in bytes.iter().enumerate() {
items[start + i] = Value::Float(*b as f64);
}
true
}
_ => false,
})
}
fn decode(kind: &str, b: &[u8]) -> Value {
match kind {
"Int8Array" => Value::Float(b[0] as i8 as f64),
"Uint8Array" | "Uint8ClampedArray" => Value::Float(b[0] as f64),
"Int16Array" => Value::Float(i16::from_le_bytes([b[0], b[1]]) as f64),
"Uint16Array" => Value::Float(u16::from_le_bytes([b[0], b[1]]) as f64),
"Int32Array" => Value::Float(i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
"Uint32Array" => Value::Float(u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
"Float32Array" => Value::Float(f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64),
"BigInt64Array" => {
let raw = i64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
}
"BigUint64Array" => {
let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
with_host(|h| h.new_bigint(num_bigint::BigInt::from(raw)))
}
_ => Value::Float(f64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]))),
}
}
fn encode(kind: &str, v: &Value) -> Vec<u8> {
if is_bigint_kind(kind) {
use num_traits::cast::ToPrimitive;
let b = bigint_of(v);
let raw = if kind == "BigInt64Array" {
b.to_i64().unwrap_or(0) as u64
} else {
b.to_u64().unwrap_or(0)
};
return raw.to_le_bytes().to_vec();
}
let n = num(v);
match kind {
"Int8Array" => vec![n as i64 as i8 as u8],
"Uint8Array" | "Uint8ClampedArray" => vec![n as i64 as u8],
"Int16Array" => (n as i64 as i16).to_le_bytes().to_vec(),
"Uint16Array" => (n as i64 as u16).to_le_bytes().to_vec(),
"Int32Array" => (n as i64 as i32).to_le_bytes().to_vec(),
"Uint32Array" => (n as i64 as u32).to_le_bytes().to_vec(),
"Float32Array" => (n as f32).to_le_bytes().to_vec(),
_ => n.to_le_bytes().to_vec(),
}
}
pub fn elems_with_host(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
if let Some(JsObj::Object(p)) = h.get(v) {
if let Some(arr) = p.get("@@bytes") {
return match h.get(arr) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
};
}
}
let Some((kind, raws)) = raw_elems(h, v) else {
return Vec::new();
};
if is_bigint_kind(&kind) {
return vec![Value::Undef; raws.len()];
}
raws.iter().map(|b| decode(&kind, b)).collect()
}
fn raw_elems(h: &crate::host::JsHost, v: &Value) -> Option<(String, Vec<Vec<u8>>)> {
let JsObj::Object(p) = h.get(v)? else {
return None;
};
let kind = p
.get("@@kind")
.map(|k| h.str_of(k))
.unwrap_or_else(|| "Uint8Array".into());
let bpe = bytes_per_element(&kind);
let len = if view_detached_h(h, v) {
0
} else {
p.get("length").map(|l| h.to_number(l)).unwrap_or(0.0) as usize
};
let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0) as usize;
let store = match p.get("@@buffer").and_then(|b| h.get(b)) {
Some(JsObj::Object(bp)) => bp.get("@@bytes").and_then(|a| h.get(a)),
_ => None,
};
let JsObj::Array(bytes) = store? else {
return None;
};
let out = (0..len)
.map(|i| {
let start = off + i * bpe;
if start + bpe > bytes.len() {
return vec![0u8; bpe];
}
bytes[start..start + bpe]
.iter()
.map(|x| h.to_number(x) as i64 as u8)
.collect()
})
.collect();
Some((kind, out))
}
pub fn elems_mut_host(h: &mut crate::host::JsHost, v: &Value) -> Vec<Value> {
if let Some(JsObj::Object(p)) = h.get(v) {
if let Some(arr) = p.get("@@bytes").cloned() {
return match h.get(&arr) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
};
}
}
let Some((kind, raws)) = raw_elems(h, v) else {
return Vec::new();
};
raws.iter()
.map(|b| {
if !is_bigint_kind(&kind) {
return decode(&kind, b);
}
let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
h.new_bigint(if kind == "BigInt64Array" {
num_bigint::BigInt::from(raw as i64)
} else {
num_bigint::BigInt::from(raw)
})
})
.collect()
}
pub fn elems_display(h: &crate::host::JsHost, v: &Value) -> Vec<String> {
let Some((kind, raws)) = raw_elems(h, v) else {
return Vec::new();
};
raws.iter()
.map(|b| {
if !is_bigint_kind(&kind) {
return h.inspect(&decode(&kind, b));
}
let raw = u64::from_le_bytes(b[..8].try_into().unwrap_or([0; 8]));
if kind == "BigInt64Array" {
format!("{}n", raw as i64)
} else {
format!("{raw}n")
}
})
.collect()
}
fn view_len(v: &Value) -> usize {
if view_detached(v) {
return 0;
}
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get("length").map(|l| h.to_number(l)).unwrap_or(0.0) as usize,
_ => 0,
})
}
pub fn elem_get(recv: &Value, key: &str) -> Option<Value> {
let i: usize = key.parse().ok()?;
if i >= view_len(recv) {
return None;
}
let kind = kind_of(recv);
let bpe = bytes_per_element(&kind);
let bytes = view_bytes(recv, i * bpe, bpe)?;
Some(decode(&kind, &bytes))
}
pub fn elem_set(recv: &Value, key: &str, val: &Value) -> Result<bool, String> {
let Ok(i) = key.parse::<usize>() else {
return Ok(false);
};
let kind = kind_of(recv);
let n = coerce_val(&kind, val)?;
if i >= view_len(recv) {
return Ok(false);
}
let bpe = bytes_per_element(&kind);
Ok(write_view_bytes(recv, i * bpe, &encode(&kind, &n)))
}
fn species(recv: &Value, kind: &str, elems: Vec<Value>) -> Value {
if super::native_tag(recv).as_deref() == Some("Buffer") {
let bytes: Vec<u8> = elems.iter().map(|x| num(x) as i64 as u8).collect();
return super::buffer::from_bytes(&bytes);
}
make(kind, elems)
}
fn write_elems(recv: &Value, kind: &str, vals: &[Value]) -> Result<(), String> {
if super::native_tag(recv).as_deref() == Some("TypedArray") {
let bpe = bytes_per_element(kind);
let coerced: Vec<Value> = vals
.iter()
.map(|v| coerce_val(kind, v))
.collect::<Result<_, _>>()?;
for (i, v) in coerced.iter().enumerate() {
write_view_bytes(recv, i * bpe, &encode(kind, v));
}
return Ok(());
}
let field = "@@bytes";
let coerced: Vec<Value> = vals
.iter()
.map(|v| coerce_val(kind, v))
.collect::<Result<_, _>>()?;
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get(recv) {
if let Some(arr) = p.get(field).cloned() {
if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
for (i, v) in coerced.into_iter().enumerate() {
if i < items.len() {
items[i] = v;
}
}
}
}
}
});
Ok(())
}
fn sort_elements(elems: &mut Vec<Value>, kind: &str, cmp: Option<&Value>) -> Result<(), String> {
let cmp = cmp.cloned().unwrap_or(Value::Undef);
if with_host(|h| crate::host::is_callable(h, &cmp)) {
return crate::builtins::sort_values(elems, Some(&cmp));
}
if is_bigint_kind(kind) {
let keys: Vec<num_bigint::BigInt> = elems.iter().map(bigint_of).collect();
let mut idx: Vec<usize> = (0..elems.len()).collect();
idx.sort_by(|a, b| keys[*a].cmp(&keys[*b]));
*elems = idx.into_iter().map(|i| elems[i].clone()).collect();
} else {
elems.sort_by(|a, b| {
num(a)
.partial_cmp(&num(b))
.unwrap_or(std::cmp::Ordering::Equal)
});
}
Ok(())
}
fn rel_index(args: &[Value], idx: usize, len: usize, default: usize) -> usize {
if args.len() <= idx {
return default;
}
let n = super::arg_num(args, idx);
if n < 0.0 {
(len as f64 + n).max(0.0) as usize
} else {
(n as usize).min(len)
}
}
fn base64_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
let kind = kind_of(recv);
if kind != "Uint8Array" {
return Err(crate::host::type_error(&format!(
"Method Uint8Array.prototype.{method} called on incompatible receiver undefined"
)));
}
let bytes: Vec<u8> = elem_values(recv)
.iter()
.map(|v| with_host(|h| h.to_number(v)) as u8)
.collect();
match method {
"toBase64" => {
let (url, _) = base64_options(args.first())?;
let omit = args
.first()
.filter(|v| !matches!(v, Value::Undef))
.map(|o| {
with_host(|h| match h.get(o) {
Some(JsObj::Object(p)) => {
p.get("omitPadding").map(|v| h.truthy(v)).unwrap_or(false)
}
_ => false,
})
})
.unwrap_or(false);
let mut s = super::to_base64(&bytes);
if url {
s = s.replace('+', "-").replace('/', "_");
}
if omit {
s = s.trim_end_matches('=').to_string();
}
Ok(with_host(|h| h.new_str(s)))
}
"toHex" => Ok(with_host(|h| h.new_str(super::to_hex(&bytes)))),
"setFromBase64" | "setFromHex" => {
let s = base64_input(args)?;
let (decoded, read) = if method == "setFromHex" {
let d = decode_hex_strict(&s)?;
let fits = d.len().min(bytes.len());
(d[..fits].to_vec(), fits * 2)
} else {
let (url, last) = base64_options(args.get(1))?;
let whole = (bytes.len() / 3) * 4;
let head: String = s.chars().take(whole).collect();
let (mut d, mut consumed) = decode_base64_strict(&head, url, last)?;
if d.len() < bytes.len() {
let (full, full_read) = decode_base64_strict(&s, url, last)?;
if full.len() <= bytes.len() {
d = full;
consumed = full_read;
}
}
(d, consumed)
};
write_view_bytes(recv, 0, &decoded);
Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("read".to_string(), Value::Float(read as f64));
m.insert("written".to_string(), Value::Float(decoded.len() as f64));
h.new_object(m)
}))
}
_ => unreachable!("caller gates the method name"),
}
}
pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
if view_detached(recv) {
return Err(detached_error("%TypedArray%.prototype", method, false));
}
if matches!(
method,
"toBase64" | "toHex" | "setFromBase64" | "setFromHex"
) {
return base64_instance_call(recv, method, args);
}
let kind = kind_of(recv);
let elems = elem_values(recv);
let this_arg = args.get(1).filter(|v| !matches!(v, Value::Undef)).cloned();
let call_cb = |i: usize, v: &Value| -> Result<Value, String> {
crate::host::invoke(
&args.first().cloned().unwrap_or(Value::Undef),
vec![v.clone(), Value::Float(i as f64), recv.clone()],
this_arg.clone(),
)
};
match method {
"every" => {
for (i, v) in elems.iter().enumerate() {
let r = call_cb(i, v)?;
if !with_host(|h| h.truthy(&r)) {
return Ok(Value::Bool(false));
}
}
Ok(Value::Bool(true))
}
"some" => {
for (i, v) in elems.iter().enumerate() {
let r = call_cb(i, v)?;
if with_host(|h| h.truthy(&r)) {
return Ok(Value::Bool(true));
}
}
Ok(Value::Bool(false))
}
"forEach" => {
for (i, v) in elems.iter().enumerate() {
call_cb(i, v)?;
}
Ok(Value::Undef)
}
"map" => {
let mut out = Vec::with_capacity(elems.len());
for (i, v) in elems.iter().enumerate() {
let r = call_cb(i, v)?;
out.push(coerce_val(&kind, &r)?);
}
Ok(species(recv, &kind, out))
}
"filter" => {
let mut out = Vec::new();
for (i, v) in elems.iter().enumerate() {
let r = call_cb(i, v)?;
if with_host(|h| h.truthy(&r)) {
out.push(v.clone());
}
}
Ok(species(recv, &kind, out))
}
"find" | "findIndex" | "findLast" | "findLastIndex" => {
let last = method.starts_with("findLast");
let idxs: Vec<usize> = if last {
(0..elems.len()).rev().collect()
} else {
(0..elems.len()).collect()
};
for i in idxs {
let r = call_cb(i, &elems[i])?;
if with_host(|h| h.truthy(&r)) {
return Ok(if method.ends_with("Index") {
Value::Float(i as f64)
} else {
elems[i].clone()
});
}
}
Ok(if method.ends_with("Index") {
Value::Float(-1.0)
} else {
Value::Undef
})
}
"reduce" | "reduceRight" => {
let right = method == "reduceRight";
let order: Vec<usize> = if right {
(0..elems.len()).rev().collect()
} else {
(0..elems.len()).collect()
};
let cb = args.first().cloned().unwrap_or(Value::Undef);
let mut it = order.into_iter();
let mut acc = if args.len() >= 2 {
args[1].clone()
} else {
match it.next() {
Some(i) => elems[i].clone(),
None => {
return Err(crate::host::type_error(
"Reduce of empty array with no initial value",
))
}
}
};
for i in it {
acc = crate::host::invoke(
&cb,
vec![acc, elems[i].clone(), Value::Float(i as f64), recv.clone()],
None,
)?;
}
Ok(acc)
}
"reverse" => {
let mut out = elems.clone();
out.reverse();
write_elems(recv, &kind, &out)?;
Ok(recv.clone())
}
"sort" => {
let mut out = elems.clone();
sort_elements(&mut out, &kind, args.first())?;
write_elems(recv, &kind, &out)?;
Ok(recv.clone())
}
"copyWithin" => {
let len = elems.len();
let target = rel_index(args, 0, len, 0);
let start = rel_index(args, 1, len, 0);
let end = rel_index(args, 2, len, len);
let src: Vec<Value> = elems[start.min(end)..end.max(start)].to_vec();
let mut out = elems.clone();
for (k, v) in src.iter().enumerate() {
if target + k < len {
out[target + k] = v.clone();
}
}
write_elems(recv, &kind, &out)?;
Ok(recv.clone())
}
"at" => {
let n = super::arg_num(args, 0);
let i = if n < 0.0 { elems.len() as f64 + n } else { n };
if i < 0.0 || i >= elems.len() as f64 {
return Ok(Value::Undef);
}
Ok(elems[i as usize].clone())
}
"lastIndexOf" => {
let needle = args.first().cloned().unwrap_or(Value::Undef);
let from = (args.len() > 1).then(|| super::arg_num(args, 1));
let found = crate::builtins::search_start_last(from, elems.len()).and_then(|start| {
elems[..=start]
.iter()
.rposition(|x| same_element(x, &needle, false))
});
Ok(Value::Float(found.map(|p| p as f64).unwrap_or(-1.0)))
}
"keys" | "values" | "entries" | "@@iterator" => {
let items: Vec<Value> = with_host(|h| match method {
"keys" => (0..elems.len()).map(|i| Value::Float(i as f64)).collect(),
"values" | "@@iterator" => elems.clone(),
_ => elems
.iter()
.enumerate()
.map(|(i, v)| h.new_array(vec![Value::Float(i as f64), v.clone()]))
.collect(),
});
Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
}
"toString" | "join" => {
let sep = if method == "join" && !args.is_empty() {
super::arg_str(args, 0)
} else {
",".into()
};
let parts: Vec<String> = with_host(|h| elems.iter().map(|n| h.str_of(n)).collect());
Ok(with_host(|h| h.new_str(parts.join(&sep))))
}
"slice" | "subarray" => {
let len = elems.len();
let norm = |n: f64| -> usize {
if n < 0.0 {
(len as f64 + n).max(0.0) as usize
} else {
(n as usize).min(len)
}
};
let s = if args.is_empty() {
0
} else {
norm(super::arg_num(args, 0))
};
let e = if args.len() < 2 {
len
} else {
norm(super::arg_num(args, 1))
};
let (lo, hi) = (s.min(e), e.max(s));
if method == "subarray" && super::native_tag(recv).as_deref() == Some("TypedArray") {
if let Some((buf, off)) = view_base(recv) {
let bpe = bytes_per_element(&kind);
return Ok(make_view(&kind, &buf, off + lo * bpe, hi - lo));
}
}
Ok(species(recv, &kind, elems[lo..hi].to_vec()))
}
"indexOf" => {
let needle = args.first().cloned().unwrap_or(Value::Undef);
let start = crate::builtins::search_start(super::arg_num(args, 1), elems.len());
Ok(Value::Float(
elems
.iter()
.skip(start)
.position(|x| same_element(x, &needle, false))
.map(|p| (p + start) as f64)
.unwrap_or(-1.0),
))
}
"includes" => {
let needle = args.first().cloned().unwrap_or(Value::Undef);
let start = crate::builtins::search_start(super::arg_num(args, 1), elems.len());
Ok(Value::Bool(
elems
.iter()
.skip(start)
.any(|x| same_element(x, &needle, true)),
))
}
"fill" => {
let len = elems.len();
let v = coerce_val(&kind, args.first().unwrap_or(&Value::Undef))?;
let start = rel_index(args, 1, len, 0);
let end = rel_index(args, 2, len, len);
let mut out = elems.clone();
for slot in out.iter_mut().take(end).skip(start) {
*slot = v.clone();
}
write_elems(recv, &kind, &out)?;
Ok(recv.clone())
}
"toReversed" | "toSorted" => {
let mut out = elems.clone();
if method == "toReversed" {
out.reverse();
} else {
sort_elements(&mut out, &kind, args.first())?;
}
Ok(make(&kind, out))
}
"with" => {
let len = elems.len();
let n = super::arg_num(args, 0);
let i = if n < 0.0 { len as f64 + n } else { n };
if !(0.0..len as f64).contains(&i) {
return Err("RangeError: Invalid typed array index".into());
}
let mut out = elems.clone();
out[i as usize] = coerce_val(&kind, args.get(1).unwrap_or(&Value::Undef))?;
Ok(make(&kind, out))
}
"set" => {
let arg = args.first().cloned().unwrap_or(Value::Undef);
let src = match super::native_tag(&arg).as_deref() {
Some("TypedArray") | Some("Buffer") => elem_values(&arg),
_ => crate::host::iter_all(&arg)
.unwrap_or_else(|_| crate::builtins::array_like_items(&arg)),
};
let off = super::arg_num(args, 1);
let off = if off.is_nan() { 0.0 } else { off.trunc() };
if off < 0.0 || off + src.len() as f64 > view_len(recv) as f64 {
return Err(crate::host::range_error("offset is out of bounds"));
}
let off = off as usize;
let src: Vec<Value> = src
.iter()
.map(|v| coerce_val(&kind, v))
.collect::<Result<_, _>>()?;
let bpe = bytes_per_element(&kind);
let len = view_len(recv);
for (k, v) in src.into_iter().enumerate() {
if off + k < len {
write_view_bytes(recv, (off + k) * bpe, &encode(&kind, &v));
}
}
Ok(Value::Undef)
}
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
pub fn construct_weakref(args: &[Value]) -> Result<Value, String> {
let target = args.first().cloned().unwrap_or(Value::Undef);
Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("WeakRef"));
m.insert("@@target".into(), target);
h.new_object(m)
}))
}
pub fn weakref_call(recv: &Value, method: &str) -> Result<Value, String> {
match method {
"deref" => Ok(with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@target").cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
})),
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn is_object_value(v: &Value) -> bool {
matches!(v, Value::Obj(_))
&& with_host(|h| {
!matches!(
h.get(v),
Some(JsObj::Str(_))
| Some(JsObj::Symbol { .. })
| Some(JsObj::BigInt(_))
| Some(JsObj::Null)
)
})
}
pub fn construct_finalization_registry(args: &[Value]) -> Result<Value, String> {
let cb = args.first().cloned().unwrap_or(Value::Undef);
if !with_host(|h| crate::host::is_callable(h, &cb)) {
return Err(crate::host::type_error(
"FinalizationRegistry: cleanup must be callable",
));
}
Ok(with_host(|h| {
let tokens = h.new_array(Vec::new());
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("FinalizationRegistry"));
m.insert("@@fr_cb".into(), cb);
m.insert("@@fr_tokens".into(), tokens);
h.new_object(m)
}))
}
pub fn finalization_registry_call(
recv: &Value,
method: &str,
args: &[Value],
) -> Result<Value, String> {
match method {
"register" => {
let target = args.first().cloned().unwrap_or(Value::Undef);
let held = args.get(1).cloned().unwrap_or(Value::Undef);
let token = args.get(2).cloned().unwrap_or(Value::Undef);
if !is_object_value(&target) {
return Err(crate::host::type_error(
"FinalizationRegistry.prototype.register: invalid target",
));
}
if with_host(|h| h.strict_eq(&target, &held)) {
return Err(crate::host::type_error(
"FinalizationRegistry.prototype.register: target and holdings must not be same",
));
}
if !matches!(token, Value::Undef) {
if !is_object_value(&token) {
return Err(crate::host::type_error(&format!(
"Invalid unregisterToken ('{}')",
with_host(|h| h.str_of(&token))
)));
}
with_host(|h| {
let toks = registry_tokens(h, recv);
if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
items.push(token);
}
});
}
Ok(Value::Undef)
}
"unregister" => {
let token = args.first().cloned().unwrap_or(Value::Undef);
if !is_object_value(&token) {
return Err(crate::host::type_error(&format!(
"Invalid unregisterToken ('{}')",
with_host(|h| h.str_of(&token))
)));
}
Ok(Value::Bool(with_host(|h| {
let toks = registry_tokens(h, recv);
let kept: Vec<Value> = match h.get(&toks) {
Some(JsObj::Array(items)) => items
.iter()
.filter(|t| !h.strict_eq(t, &token))
.cloned()
.collect(),
_ => Vec::new(),
};
let removed = match h.get(&toks) {
Some(JsObj::Array(items)) => items.len() != kept.len(),
_ => false,
};
if let Some(JsObj::Array(items)) = h.get_mut(&toks) {
*items = kept;
}
removed
})))
}
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn registry_tokens(h: &crate::host::JsHost, recv: &Value) -> Value {
match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@fr_tokens").cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
}
}
pub fn construct_text_encoder() -> Result<Value, String> {
Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("TextEncoder"));
m.insert("@@encoding".into(), h.new_str("utf-8"));
h.new_object(m)
}))
}
pub fn text_encoder_call(_recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"encode" => {
let s = super::arg_str(args, 0);
Ok(make(
"Uint8Array",
s.as_bytes()
.iter()
.map(|b| Value::Float(*b as f64))
.collect(),
))
}
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn encoding_for_label(label: &str) -> Option<&'static str> {
Some(match label.trim().to_ascii_lowercase().as_str() {
"utf-8" | "utf8" | "unicode-1-1-utf-8" | "unicode11utf8" | "unicode20utf8"
| "x-unicode20utf8" => "utf-8",
"latin1" | "iso-8859-1" | "iso8859-1" | "iso88591" | "ascii" | "us-ascii" | "cp1252"
| "cp819" | "ibm819" | "l1" | "windows-1252" | "x-cp1252" => "windows-1252",
"utf-16le" | "utf-16" | "ucs-2" | "ucs2" | "unicodefeff" | "unicodefffe"
| "iso-10646-ucs-2" | "csunicode" => "utf-16le",
_ => return None,
})
}
pub fn construct_text_decoder(args: &[Value]) -> Result<Value, String> {
let label = if args.is_empty() || matches!(args[0], Value::Undef) {
"utf-8".to_string()
} else {
super::arg_str(args, 0)
};
let Some(encoding) = encoding_for_label(&label) else {
return Err(crate::host::coded_error(
"RangeError",
"ERR_ENCODING_NOT_SUPPORTED",
&format!("The \"{label}\" encoding is not supported"),
));
};
let flag = |key: &str| {
args.get(1)
.map(|o| crate::builtins::get_property(o, key).unwrap_or(Value::Undef))
.map(|v| with_host(|h| h.truthy(&v)))
.unwrap_or(false)
};
let (fatal, ignore_bom) = (flag("fatal"), flag("ignoreBOM"));
Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("TextDecoder"));
m.insert("@@encoding".into(), h.new_str(encoding.to_string()));
m.insert("@@fatal".into(), Value::Bool(fatal));
m.insert("@@ignoreBOM".into(), Value::Bool(ignore_bom));
h.new_object(m)
}))
}
const CP1252_HIGH: [char; 32] = [
'\u{20ac}', '\u{81}', '\u{201a}', '\u{192}', '\u{201e}', '\u{2026}', '\u{2020}', '\u{2021}',
'\u{2c6}', '\u{2030}', '\u{160}', '\u{2039}', '\u{152}', '\u{8d}', '\u{17d}', '\u{8f}',
'\u{90}', '\u{2018}', '\u{2019}', '\u{201c}', '\u{201d}', '\u{2022}', '\u{2013}', '\u{2014}',
'\u{2dc}', '\u{2122}', '\u{161}', '\u{203a}', '\u{153}', '\u{9d}', '\u{17e}', '\u{178}',
];
pub fn text_decoder_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"decode" => {
let bytes: Vec<u8> = elems_of(&args.first().cloned().unwrap_or(Value::Undef))
.unwrap_or_default()
.iter()
.map(|n| *n as u8)
.collect();
let slot = |key: &str| {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get(key).cloned(),
_ => None,
})
};
let enc = slot("@@encoding")
.map(|v| with_host(|h| h.str_of(&v)))
.unwrap_or_else(|| "utf-8".into());
let flag = |key: &str| matches!(slot(key), Some(Value::Bool(true)));
let s = match enc.as_str() {
"windows-1252" => bytes
.iter()
.map(|b| match b {
0x80..=0x9f => CP1252_HIGH[(b - 0x80) as usize],
_ => *b as char,
})
.collect(),
"utf-16le" => {
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&units)
}
_ if flag("@@fatal") => match std::str::from_utf8(&bytes) {
Ok(s) => s.to_string(),
Err(_) => {
return Err(crate::host::coded_error(
"TypeError",
"ERR_ENCODING_INVALID_ENCODED_DATA",
&format!("The encoded data was not valid for encoding {enc}"),
))
}
},
_ => String::from_utf8_lossy(&bytes).into_owned(),
};
let s = match s.strip_prefix('\u{feff}') {
Some(rest) if !flag("@@ignoreBOM") => rest.to_string(),
_ => s,
};
Ok(with_host(|h| h.new_str(s)))
}
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}