use super::{arg_str, from_base64, from_hex, to_base64, to_hex};
use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
pub const STATIC_METHODS: &[&str] = &[
"from",
"alloc",
"allocUnsafe",
"allocUnsafeSlow",
"concat",
"isBuffer",
"isEncoding",
"byteLength",
"compare",
"of",
];
pub const INSTANCE_METHODS: &[&str] = &[
"toString",
"set",
"toJSON",
"equals",
"slice",
"subarray",
"readUInt8",
"includes",
"indexOf",
"lastIndexOf",
"write",
"copy",
"fill",
"compare",
"readUInt16BE",
"readUInt16LE",
"writeUInt8",
"writeInt8",
"writeInt16BE",
"writeInt16LE",
"readFloatBE",
"readFloatLE",
"writeFloatBE",
"writeFloatLE",
"readDoubleBE",
"readDoubleLE",
"writeDoubleBE",
"writeDoubleLE",
"readBigInt64BE",
"readBigInt64LE",
"readBigUInt64BE",
"readBigUInt64LE",
"writeBigInt64BE",
"writeBigInt64LE",
"writeBigUInt64BE",
"writeBigUInt64LE",
"readIntBE",
"readIntLE",
"readUIntBE",
"readUIntLE",
"writeIntBE",
"writeIntLE",
"writeUIntBE",
"writeUIntLE",
"writeUInt16BE",
"writeUInt16LE",
"readUInt32BE",
"readUInt32LE",
"readInt8",
"readInt16BE",
"readInt16LE",
"readInt32BE",
"readInt32LE",
"writeUInt32BE",
"writeUInt32LE",
"writeInt32BE",
"writeInt32LE",
"at",
"values",
"keys",
"entries",
"swap16",
"swap32",
"swap64",
];
pub const MODULE_METHODS: &[&str] = &["atob", "btoa", "isAscii", "isUtf8", "transcode"];
pub fn module_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"atob" => {
let s = arg_str(args, 0);
let bytes = from_base64(&s);
let bin: String = bytes.iter().map(|b| *b as char).collect();
Ok(with_host(|h| h.new_str(bin)))
}
"btoa" => {
let s = arg_str(args, 0);
let bytes: Vec<u8> = s.chars().map(|c| c as u32 as u8).collect();
let b64 = to_base64(&bytes);
Ok(with_host(|h| h.new_str(b64)))
}
"isAscii" => {
let bytes = input_bytes(args.first());
Ok(Value::Bool(bytes.iter().all(|b| *b < 0x80)))
}
"isUtf8" => {
let bytes = input_bytes(args.first());
Ok(Value::Bool(std::str::from_utf8(&bytes).is_ok()))
}
"transcode" => {
let src = input_bytes(args.first());
let from = arg_str(args, 1);
let to = arg_str(args, 2);
let s = bytes_to_string(&src, &from);
let out = string_to_bytes(&s, &to);
Ok(from_bytes(&out))
}
_ => return None,
})
}
fn input_bytes(v: Option<&Value>) -> Vec<u8> {
match v {
None => Vec::new(),
Some(v) => {
if let Some(s) = with_host(|h| h.as_str(v)) {
s.into_bytes()
} else {
bytes_of(v)
}
}
}
}
fn bytes_to_string(bytes: &[u8], enc: &str) -> String {
match enc.to_ascii_lowercase().as_str() {
"ascii" | "latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
"utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&units)
}
_ => String::from_utf8_lossy(bytes).into_owned(),
}
}
fn string_to_bytes(s: &str, enc: &str) -> Vec<u8> {
match enc.to_ascii_lowercase().as_str() {
"ascii" => s
.chars()
.map(|c| if c.is_ascii() { c as u8 } else { b'?' })
.collect(),
"latin1" | "binary" => s.chars().map(|c| c as u32 as u8).collect(),
"utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
}
_ => s.as_bytes().to_vec(),
}
}
fn part_bytes(v: &Value) -> Vec<u8> {
match with_host(|h| h.as_str(v)) {
Some(s) => s.into_bytes(),
None => bytes_of(v),
}
}
fn gather_parts(parts: &Value) -> Vec<u8> {
let items = with_host(|h| match h.get(parts) {
Some(JsObj::Array(it)) => it.clone(),
_ => Vec::new(),
});
let mut out = Vec::new();
for it in &items {
out.extend(part_bytes(it));
}
out
}
fn opt_type(opts: Option<&Value>) -> String {
match opts {
Some(v) => with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get("type").map(|x| h.str_of(x)).unwrap_or_default(),
_ => String::new(),
}),
None => String::new(),
}
}
fn build_blob(tag: &str, bytes: &[u8], typ: &str, extra: IndexMap<String, Value>) -> Value {
with_host(|h| {
let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str(tag.to_string()));
m.insert("@@bytes".into(), arr);
m.insert("size".into(), Value::Float(bytes.len() as f64));
m.insert("type".into(), h.new_str(typ.to_string()));
for (k, v) in extra {
m.insert(k, v);
}
h.new_object(m)
})
}
pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
let typ = opt_type(args.get(1));
Ok(build_blob("Blob", &bytes, &typ, IndexMap::new()))
}
pub fn construct_file(args: &[Value]) -> Result<Value, String> {
let bytes = gather_parts(&args.first().cloned().unwrap_or(Value::Undef));
let name = arg_str(args, 1);
let typ = opt_type(args.get(2));
let last_modified = args
.get(2)
.map(|v| {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => {
p.get("lastModified").map(|x| h.to_number(x)).unwrap_or(0.0)
}
_ => 0.0,
})
})
.unwrap_or(0.0);
let extra = with_host(|h| {
let mut m = IndexMap::new();
m.insert("name".to_string(), h.new_str(name));
m.insert("lastModified".to_string(), Value::Float(last_modified));
m
});
Ok(build_blob("File", &bytes, &typ, extra))
}
pub const BLOB_METHODS: &[&str] = &["text", "arrayBuffer", "bytes", "slice"];
pub fn blob_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
let bytes = bytes_of(recv);
match method {
"text" => {
let s = String::from_utf8_lossy(&bytes).into_owned();
let sv = with_host(|h| h.new_str(s));
Ok(crate::host::promise_of(&sv))
}
"arrayBuffer" | "bytes" => {
let buf = from_bytes(&bytes);
Ok(crate::host::promise_of(&buf))
}
"slice" => {
let (s, e) = slice_bounds(args, bytes.len());
let typ = if args.len() > 2 {
arg_str(args, 2)
} else {
String::new()
};
Ok(build_blob("Blob", &bytes[s..e], &typ, IndexMap::new()))
}
_ => Err(crate::host::type_error(&format!(
"blob.{method} is not a function"
))),
}
}
pub fn share_array_buffer(ab: &Value, off: usize, len: usize) -> Value {
let store = crate::stdlib::typedarray::buffer_store(ab);
with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("Buffer"));
m.insert(
"@@bytes".into(),
store.unwrap_or_else(|| h.new_array(Vec::new())),
);
m.insert("@@buffer".into(), ab.clone());
m.insert("buffer".into(), ab.clone());
m.insert("length".into(), Value::Float(len as f64));
m.insert("byteLength".into(), Value::Float(len as f64));
m.insert("byteOffset".into(), Value::Float(off as f64));
m.insert("BYTES_PER_ELEMENT".into(), Value::Float(1.0));
let obj = h.new_object(m);
h.ensure_native_protos();
if let Some(p) = h.native_proto("Buffer") {
h.set_proto(&obj, p);
}
for k in [
"buffer",
"length",
"byteLength",
"byteOffset",
"BYTES_PER_ELEMENT",
] {
h.hide_prop(&obj, k);
}
obj
})
}
pub fn from_bytes(bytes: &[u8]) -> Value {
with_host(|h| {
let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("Buffer"));
m.insert("@@bytes".into(), arr);
m.insert("length".into(), Value::Float(bytes.len() as f64));
m.insert("byteLength".into(), Value::Float(bytes.len() as f64));
m.insert("byteOffset".into(), Value::Float(0.0));
m.insert("BYTES_PER_ELEMENT".into(), Value::Float(1.0));
let obj = h.new_object(m);
h.ensure_native_protos();
if let Some(p) = h.native_proto("Buffer") {
h.set_proto(&obj, p);
}
for k in ["length", "byteLength", "byteOffset", "BYTES_PER_ELEMENT"] {
h.hide_prop(&obj, k);
}
obj
})
}
fn window(recv: &Value) -> (usize, usize) {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => {
let off = p.get("byteOffset").map(|o| h.to_number(o)).unwrap_or(0.0) as usize;
let store = match p.get("@@bytes").and_then(|v| h.get(v)) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
};
let len = p
.get("length")
.map(|l| h.to_number(l) as usize)
.unwrap_or(store);
(off.min(store), len.min(store.saturating_sub(off)))
}
_ => (0, 0),
})
}
fn bytes_of(recv: &Value) -> Vec<u8> {
let (off, len) = window(recv);
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|v| h.get(v)) {
Some(JsObj::Array(items)) => items[off..off + len]
.iter()
.map(|v| h.to_number(v) as u8)
.collect(),
_ => Vec::new(),
},
_ => Vec::new(),
})
}
fn bytes_handle(recv: &Value) -> Option<Value> {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
_ => None,
})
}
pub fn byte_get(recv: &Value, index: &str) -> Value {
let i: usize = match index.parse() {
Ok(i) => i,
Err(_) => return Value::Undef,
};
let arr = match bytes_handle(recv) {
Some(a) => a,
None => return Value::Undef,
};
let (off, len) = window(recv);
if i >= len {
return Value::Undef;
}
with_host(|h| match h.get(&arr) {
Some(JsObj::Array(items)) => match items.get(off + i) {
Some(v) => Value::Float(h.to_number(v)),
None => Value::Undef,
},
_ => Value::Undef,
})
}
pub fn byte_set(recv: &Value, index: &str, val: &Value) -> bool {
if super::native_tag(recv).as_deref() != Some("Buffer") {
return false;
}
let i: usize = match index.parse() {
Ok(i) => i,
Err(_) => return false,
};
let arr = match bytes_handle(recv) {
Some(a) => a,
None => return false,
};
let b = with_host(|h| h.to_number(val)) as i64 as u8;
let (off, len) = window(recv);
if i >= len {
return true;
}
with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
if let Some(slot) = items.get_mut(off + i) {
*slot = Value::Float(b as f64);
}
}
});
true
}
const K_MAX_LENGTH: f64 = 9_007_199_254_740_991.0;
fn validate_size(args: &[Value]) -> Result<usize, String> {
let v = args.first().cloned().unwrap_or(Value::Undef);
if with_host(|h| h.type_of(&v)) != "number" {
return Err(crate::host::invalid_arg_type(
"size", "argument", "number", &v,
));
}
let n = with_host(|h| h.to_number(&v));
if n.is_nan() || !(0.0..=K_MAX_LENGTH).contains(&n) {
return Err(crate::host::coded_error(
"RangeError",
"ERR_OUT_OF_RANGE",
&format!(
"The value of \"size\" is out of range. It must be >= 0 && <= {}. Received {}",
crate::host::fmt_number(K_MAX_LENGTH),
out_of_range_received(n)
),
));
}
Ok(n as usize)
}
fn out_of_range_received(n: f64) -> String {
let shown = crate::host::fmt_number(n);
if n.fract() != 0.0 || n.abs() <= 4_294_967_296.0 {
return shown;
}
let start = usize::from(shown.starts_with('-'));
let mut i = shown.len();
let mut groups = String::new();
while i >= start + 4 {
groups = format!("_{}{groups}", &shown[i - 3..i]);
i -= 3;
}
format!("{}{groups}", &shown[..i])
}
pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"from" => from(args),
"alloc" => {
let n = match validate_size(args) {
Ok(n) => n,
Err(e) => return Some(Err(e)),
};
let pat = if args.len() > 1 {
let enc = if args.len() > 2 {
arg_str(args, 2)
} else {
"utf8".into()
};
fill_pattern(args, 1, &enc)
} else {
vec![0]
};
let bytes: Vec<u8> = if pat.is_empty() {
vec![0u8; n]
} else {
(0..n).map(|i| pat[i % pat.len()]).collect()
};
Ok(from_bytes(&bytes))
}
"allocUnsafe" | "allocUnsafeSlow" => validate_size(args).map(|n| from_bytes(&vec![0u8; n])),
"concat" => concat(args),
"of" => Ok(from_bytes(
&args
.iter()
.map(|v| crate::host::with_host(|h| h.to_number(v)) as u8)
.collect::<Vec<u8>>(),
)),
"isEncoding" => Ok(Value::Bool(matches!(
super::arg_str(args, 0).to_ascii_lowercase().as_str(),
"utf8"
| "utf-8"
| "ucs2"
| "ucs-2"
| "utf16le"
| "utf-16le"
| "latin1"
| "binary"
| "base64"
| "base64url"
| "hex"
| "ascii"
))),
"compare" => {
let a = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
let b = bytes_of(&args.get(1).cloned().unwrap_or(Value::Undef));
Ok(Value::Float(match a.cmp(&b) {
std::cmp::Ordering::Less => -1.0,
std::cmp::Ordering::Equal => 0.0,
std::cmp::Ordering::Greater => 1.0,
}))
}
"isBuffer" => Ok(Value::Bool(
super::native_tag(&args.first().cloned().unwrap_or(Value::Undef)).as_deref()
== Some("Buffer"),
)),
"byteLength" => {
if let Some(n) = view_byte_length(&args.first().cloned().unwrap_or(Value::Undef)) {
return Some(Ok(Value::Float(n)));
}
let enc = args
.get(1)
.map(|_| arg_str(args, 1))
.unwrap_or_else(|| "utf8".into());
Ok(Value::Float(
decode_str(&arg_str(args, 0), &enc).len() as f64
))
}
_ => return None,
})
}
pub fn view_bytes(v: &Value) -> Option<Vec<u8>> {
match super::native_tag(v).as_deref() {
Some("DataView") => {
let n = with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get("byteLength").map(|l| h.to_number(l) as usize),
_ => None,
})
.unwrap_or(0);
crate::stdlib::typedarray::view_bytes(v, 0, n)
}
Some("Buffer") | Some("TypedArray") | Some("ArrayBuffer") => bytes_like(v),
_ => None,
}
}
pub fn bytes_like(v: &Value) -> Option<Vec<u8>> {
if let Some(elems) = crate::stdlib::typedarray::elems_of(v) {
return Some(elems.iter().map(|x| *x as i64 as u8).collect());
}
if super::native_tag(v).as_deref() == Some("ArrayBuffer") {
return Some(crate::stdlib::typedarray::buffer_bytes_snapshot(v).unwrap_or_default());
}
with_host(|h| match h.get(v) {
Some(JsObj::Array(items)) => {
Some(items.iter().map(|x| h.to_number(x) as i64 as u8).collect())
}
_ => None,
})
}
fn view_byte_length(v: &Value) -> Option<f64> {
match super::native_tag(v).as_deref() {
Some("Buffer") | Some("TypedArray") | Some("ArrayBuffer") => {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get("byteLength").map(|b| h.to_number(b)),
_ => None,
})
}
_ => None,
}
}
fn from(args: &[Value]) -> Result<Value, String> {
let v = args.first().cloned().unwrap_or(Value::Undef);
if super::native_tag(&v).as_deref() == Some("ArrayBuffer") {
let total = crate::stdlib::typedarray::buffer_byte_length(&v);
let off = (super::arg_num(args, 1).max(0.0) as usize).min(total);
let len = match args.get(2) {
Some(Value::Undef) | None => total - off,
Some(_) => (super::arg_num(args, 2).max(0.0) as usize).min(total - off),
};
return Ok(share_array_buffer(&v, off, len));
}
if let Some(bytes) = bytes_like(&v) {
return Ok(from_bytes(&bytes));
}
if with_host(|h| h.as_str(&v)).is_some() || matches!(v, Value::Str(_)) {
let enc = if args.len() > 1 {
arg_str(args, 1)
} else {
"utf8".into()
};
return Ok(from_bytes(&decode_str(&arg_str(args, 0), &enc)));
}
if super::native_tag(&v).as_deref() == Some("DataView") {
return Ok(from_bytes(&[]));
}
if matches!(v, Value::Obj(_)) {
let len = crate::builtins::get_property(&v, "length").unwrap_or(Value::Undef);
if !matches!(len, Value::Undef) {
let n = with_host(|h| h.to_number(&len));
if n.is_finite() && n >= 0.0 {
let n = n as usize;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let e =
crate::builtins::get_property(&v, &i.to_string()).unwrap_or(Value::Undef);
let b = with_host(|h| h.to_number(&e));
out.push(if b.is_finite() { b as i64 as u8 } else { 0 });
}
return Ok(from_bytes(&out));
}
}
}
Err(crate::host::plain_coded_error(
"TypeError",
"ERR_INVALID_ARG_TYPE",
&format!(
"The first argument must be of type string or an instance of \
Buffer, ArrayBuffer, or Array or an Array-like Object. Received {}",
received_label(&v)
),
))
}
fn received_label(v: &Value) -> String {
if matches!(v, Value::Undef) {
return "undefined".into();
}
if with_host(|h| h.is_null(v)) {
return "null".into();
}
let ty = with_host(|h| h.type_of(v));
if ty == "object" || ty == "function" {
let ctor = with_host(|h| h.ctor_name(v));
let ctor = if ctor.is_empty() {
"Object".into()
} else {
ctor
};
return format!("an instance of {ctor}");
}
let shown = with_host(|h| h.inspect(v));
format!("type {ty} ({shown})")
}
fn concat(args: &[Value]) -> Result<Value, String> {
let list = with_host(
|h| match h.get(&args.first().cloned().unwrap_or(Value::Undef)) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
},
);
let mut out = Vec::new();
for b in &list {
out.extend(bytes_like(b).unwrap_or_default());
}
Ok(from_bytes(&out))
}
pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
let bytes = bytes_of(recv);
match method {
"toString" => {
let enc = match args.first() {
None | Some(Value::Undef) => "utf8".into(),
_ => arg_str(args, 0),
};
let len = bytes.len();
let clamp = |i: usize| -> usize {
let n = super::arg_num(args, i);
if n.is_nan() {
0
} else {
n.clamp(0.0, len as f64) as usize
}
};
let start = if args.len() > 1 { clamp(1) } else { 0 };
let end = if args.len() > 2 { clamp(2) } else { len };
let slice = if start < end {
&bytes[start..end]
} else {
&[][..]
};
Ok(with_host(|h| h.new_str(encode_bytes(slice, &enc))))
}
"toJSON" => Ok(with_host(|h| {
let data = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
let mut m = IndexMap::new();
m.insert("type".into(), h.new_str("Buffer"));
m.insert("data".into(), data);
h.new_object(m)
})),
"equals" => {
let other = bytes_like(&args.first().cloned().unwrap_or(Value::Undef));
Ok(Value::Bool(other.is_some_and(|o| bytes == o)))
}
"slice" | "subarray" => {
let (s, e) = slice_bounds(args, bytes.len());
Ok(from_bytes(&bytes[s..e]))
}
"readUInt8" => {
let i = read_offset(args, 1, bytes.len())?;
Ok(Value::Float(bytes[i] as f64))
}
"includes" | "indexOf" | "lastIndexOf" => {
let len = bytes.len();
let last = method == "lastIndexOf";
let (from, enc) = match args.get(1) {
None | Some(Value::Undef) => (None, arg_str(args, 2)),
Some(v) if with_host(|h| h.as_str(v)).is_some() => (None, arg_str(args, 1)),
_ => (Some(super::arg_num(args, 1)), arg_str(args, 2)),
};
let enc = if enc.is_empty() { "utf8".into() } else { enc };
let target = args.first().cloned().unwrap_or(Value::Undef);
let needle = match &target {
Value::Int(_) | Value::Float(_) => vec![super::arg_num(args, 0) as u8],
_ if bytes_like(&target).is_some() => bytes_like(&target).unwrap_or_default(),
_ => decode_str(&arg_str(args, 0), &enc),
};
let from = from.map(|n| {
if n.is_nan() {
0
} else if n < 0.0 {
(len as f64 + n).max(0.0) as usize
} else {
(n as usize).min(len)
}
});
let pos = if needle.is_empty() {
Some(from.unwrap_or(if last { len } else { 0 }).min(len))
} else if last {
let hi = (from.unwrap_or(len) + needle.len()).min(len);
bytes[..hi]
.windows(needle.len())
.rposition(|w| w == needle.as_slice())
} else {
let lo = from.unwrap_or(0);
bytes[lo..]
.windows(needle.len())
.position(|w| w == needle.as_slice())
.map(|p| p + lo)
};
if method == "includes" {
Ok(Value::Bool(pos.is_some()))
} else {
Ok(Value::Float(pos.map(|p| p as f64).unwrap_or(-1.0)))
}
}
"compare" => {
let other = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
Ok(Value::Float(match bytes.cmp(&other) {
std::cmp::Ordering::Less => -1.0,
std::cmp::Ordering::Equal => 0.0,
std::cmp::Ordering::Greater => 1.0,
}))
}
"readUInt16BE" => {
let i = read_offset(args, 2, bytes.len())?;
let v = ((bytes[i] as u16) << 8) | bytes[i + 1] as u16;
Ok(Value::Float(v as f64))
}
"readUInt16LE" => {
let i = read_offset(args, 2, bytes.len())?;
let v = (bytes[i] as u16) | ((bytes[i + 1] as u16) << 8);
Ok(Value::Float(v as f64))
}
"readUInt32BE" | "readUInt32LE" | "readInt32BE" | "readInt32LE" => {
let i = read_offset(args, 4, bytes.len())?;
let at = |k: usize| bytes[i + k] as u32;
let v = if method.ends_with("BE") {
(at(0) << 24) | (at(1) << 16) | (at(2) << 8) | at(3)
} else {
at(0) | (at(1) << 8) | (at(2) << 16) | (at(3) << 24)
};
Ok(Value::Float(if method.starts_with("readInt") {
v as i32 as f64
} else {
v as f64
}))
}
"readInt8" => {
let i = read_offset(args, 1, bytes.len())?;
Ok(Value::Float(bytes[i] as i8 as f64))
}
"readInt16BE" | "readInt16LE" => {
let i = read_offset(args, 2, bytes.len())?;
let at = |k: usize| bytes[i + k] as u16;
let v = if method.ends_with("BE") {
(at(0) << 8) | at(1)
} else {
at(0) | (at(1) << 8)
};
Ok(Value::Float(v as i16 as f64))
}
"at" => {
let i = super::arg_num(args, 0);
let idx = if i < 0.0 { i + bytes.len() as f64 } else { i };
Ok(match bytes.get(idx.max(-1.0) as usize) {
Some(b) if idx >= 0.0 => Value::Float(*b as f64),
_ => Value::Undef,
})
}
"values" | "keys" | "entries" | "@@iterator" => {
let items: Vec<Value> = with_host(|h| match method {
"keys" => (0..bytes.len()).map(|i| Value::Float(i as f64)).collect(),
"values" | "@@iterator" => bytes.iter().map(|b| Value::Float(*b as f64)).collect(),
_ => bytes
.iter()
.enumerate()
.map(|(i, b)| {
h.new_array(vec![Value::Float(i as f64), Value::Float(*b as f64)])
})
.collect(),
});
Ok(with_host(|h| {
h.alloc(JsObj::Iter {
items,
idx: 0,
array: None,
})
}))
}
"readFloatBE" | "readFloatLE" => {
let i = read_offset(args, 4, bytes.len())?;
let mut raw = [0u8; 4];
raw.copy_from_slice(&bytes[i..i + 4]);
if method.ends_with("LE") {
raw.reverse();
}
Ok(Value::Float(f32::from_be_bytes(raw) as f64))
}
"readDoubleBE" | "readDoubleLE" => {
let i = read_offset(args, 8, bytes.len())?;
let mut raw = [0u8; 8];
raw.copy_from_slice(&bytes[i..i + 8]);
if method.ends_with("LE") {
raw.reverse();
}
Ok(Value::Float(f64::from_be_bytes(raw)))
}
"writeFloatBE" | "writeFloatLE" => {
let mut raw = (super::arg_num(args, 0) as f32).to_be_bytes();
if method.ends_with("LE") {
raw.reverse();
}
let off = super::arg_num(args, 1).max(0.0) as usize;
store_bytes(recv, &bytes, off, &raw)?;
Ok(Value::Float((off + 4) as f64))
}
"writeDoubleBE" | "writeDoubleLE" => {
let mut raw = super::arg_num(args, 0).to_be_bytes();
if method.ends_with("LE") {
raw.reverse();
}
let off = super::arg_num(args, 1).max(0.0) as usize;
store_bytes(recv, &bytes, off, &raw)?;
Ok(Value::Float((off + 8) as f64))
}
"readBigInt64BE" | "readBigInt64LE" | "readBigUInt64BE" | "readBigUInt64LE" => {
let i = read_offset(args, 8, bytes.len())?;
let mut raw = [0u8; 8];
raw.copy_from_slice(&bytes[i..i + 8]);
if method.ends_with("LE") {
raw.reverse();
}
let n = if method.starts_with("readBigInt") {
num_bigint::BigInt::from(i64::from_be_bytes(raw))
} else {
num_bigint::BigInt::from(u64::from_be_bytes(raw))
};
Ok(with_host(|h| h.alloc(JsObj::BigInt(n))))
}
"writeBigInt64BE" | "writeBigInt64LE" | "writeBigUInt64BE" | "writeBigUInt64LE" => {
let v = args.first().cloned().unwrap_or(Value::Undef);
let n = with_host(|h| match h.get(&v) {
Some(JsObj::BigInt(b)) => b.clone(),
_ => num_bigint::BigInt::from(h.to_number(&v) as i64),
});
let bits = num_traits::ToPrimitive::to_i64(&n)
.map(|x| x as u64)
.or_else(|| num_traits::ToPrimitive::to_u64(&n))
.unwrap_or(0);
let mut raw = bits.to_be_bytes();
if method.ends_with("LE") {
raw.reverse();
}
let off = super::arg_num(args, 1).max(0.0) as usize;
store_bytes(recv, &bytes, off, &raw)?;
Ok(Value::Float((off + 8) as f64))
}
"readIntBE" | "readIntLE" | "readUIntBE" | "readUIntLE" => {
let off = super::arg_num(args, 0).max(0.0) as usize;
let width = (super::arg_num(args, 1).max(1.0) as usize).min(6);
if off + width > bytes.len() {
return Err(range_error_out_of_bounds());
}
let mut acc: u64 = 0;
for k in 0..width {
let b = if method.ends_with("BE") {
bytes[off + k]
} else {
bytes[off + width - 1 - k]
};
acc = (acc << 8) | b as u64;
}
let signed = method.starts_with("readInt");
let out = if signed {
let shift = 64 - (width * 8);
((acc << shift) as i64 >> shift) as f64
} else {
acc as f64
};
Ok(Value::Float(out))
}
"writeIntBE" | "writeIntLE" | "writeUIntBE" | "writeUIntLE" => {
let val = super::arg_num(args, 0) as i64 as u64;
let off = super::arg_num(args, 1).max(0.0) as usize;
let width = (super::arg_num(args, 2).max(1.0) as usize).min(6);
let mut raw: Vec<u8> = (0..width)
.map(|k| (val >> (8 * (width - 1 - k))) as u8)
.collect();
if method.ends_with("LE") {
raw.reverse();
}
store_bytes(recv, &bytes, off, &raw)?;
Ok(Value::Float((off + width) as f64))
}
"writeInt8" => {
let off = super::arg_num(args, 1).max(0.0) as usize;
store_bytes(recv, &bytes, off, &[super::arg_num(args, 0) as i64 as u8])?;
Ok(Value::Float((off + 1) as f64))
}
"writeInt16BE" | "writeInt16LE" => {
let val = super::arg_num(args, 0) as i64 as u16;
let mut raw = val.to_be_bytes();
if method.ends_with("LE") {
raw.reverse();
}
let off = super::arg_num(args, 1).max(0.0) as usize;
store_bytes(recv, &bytes, off, &raw)?;
Ok(Value::Float((off + 2) as f64))
}
"writeUInt8" => {
let off = super::arg_num(args, 1).max(0.0) as usize;
store_bytes(recv, &bytes, off, &[super::arg_num(args, 0) as u8])?;
Ok(Value::Float((off + 1) as f64))
}
"writeUInt16BE" | "writeUInt16LE" => {
let mut b = bytes.clone();
let val = super::arg_num(args, 0) as u16;
let off = super::arg_num(args, 1).max(0.0) as usize;
let (hi, lo) = ((val >> 8) as u8, (val & 0xff) as u8);
let (b0, b1) = if method == "writeUInt16BE" {
(hi, lo)
} else {
(lo, hi)
};
let _ = &mut b;
store_bytes(recv, &bytes, off, &[b0, b1])?;
Ok(Value::Float((off + 2) as f64))
}
"writeUInt32BE" | "writeUInt32LE" | "writeInt32BE" | "writeInt32LE" => {
let mut b = bytes.clone();
let val = super::arg_num(args, 0) as i64 as u32;
let off = super::arg_num(args, 1).max(0.0) as usize;
let be = [
(val >> 24) as u8,
(val >> 16) as u8,
(val >> 8) as u8,
val as u8,
];
let out: Vec<u8> = if method.ends_with("BE") {
be.to_vec()
} else {
be.iter().rev().copied().collect()
};
let _ = &mut b;
store_bytes(recv, &bytes, off, &out)?;
Ok(Value::Float((off + 4) as f64))
}
"write" => {
let mut b = bytes.clone();
let Some((off, max, enc)) = write_args(args, b.len()) else {
return Err(crate::host::range_error(&format!(
"The value of \"offset\" is out of range. It must be >= 0 && <= {}. Received {}",
b.len(),
super::arg_num(args, 1)
)));
};
let src = truncate_chars(&arg_str(args, 0), &enc, max);
let n = src.len().min(b.len().saturating_sub(off));
b[off..off + n].copy_from_slice(&src[..n]);
set_bytes(recv, &b);
Ok(Value::Float(n as f64))
}
"swap16" | "swap32" | "swap64" => {
let group = match method {
"swap16" => 2,
"swap32" => 4,
_ => 8,
};
if bytes.len() % group != 0 {
return Err(crate::host::coded_error(
"RangeError",
"ERR_INVALID_BUFFER_SIZE",
&format!("Buffer size must be a multiple of {}-bits", group * 8),
));
}
let mut b = bytes.clone();
for c in b.chunks_mut(group) {
c.reverse();
}
set_bytes(recv, &b);
Ok(recv.clone())
}
"fill" => {
let mut b = bytes.clone();
let len = b.len();
let (start, end, enc) = if arg_is_str(args, 1) {
(0, len, arg_str(args, 1))
} else if arg_is_str(args, 2) {
let s = (super::arg_num(args, 1).max(0.0) as usize).min(len);
(s, len, arg_str(args, 2))
} else {
let s = if args.len() > 1 {
(super::arg_num(args, 1).max(0.0) as usize).min(len)
} else {
0
};
let e = if args.len() > 2 {
(super::arg_num(args, 2).max(0.0) as usize).min(len)
} else {
len
};
let enc = if args.len() > 3 {
arg_str(args, 3)
} else {
"utf8".into()
};
(s, e, enc)
};
let pat = fill_pattern(args, 0, &enc);
if !pat.is_empty() {
for (k, slot) in b[start..end.max(start)].iter_mut().enumerate() {
*slot = pat[k % pat.len()];
}
}
set_bytes(recv, &b);
Ok(recv.clone())
}
"copy" => {
let target = args.first().cloned().unwrap_or(Value::Undef);
let mut tb = bytes_of(&target);
let tstart = if args.len() > 1 {
super::arg_num(args, 1).max(0.0) as usize
} else {
0
};
let sstart = if args.len() > 2 {
super::arg_num(args, 2).max(0.0) as usize
} else {
0
};
let send = if args.len() > 3 {
(super::arg_num(args, 3) as usize).min(bytes.len())
} else {
bytes.len()
};
let mut n = 0;
for (k, &byte) in bytes[sstart..send.max(sstart)].iter().enumerate() {
if tstart + k < tb.len() {
tb[tstart + k] = byte;
n += 1;
}
}
set_bytes(&target, &tb);
Ok(Value::Float(n as f64))
}
"set" => {
let src = bytes_of(&args.first().cloned().unwrap_or(Value::Undef));
let offset = if args.len() > 1 {
super::arg_num(args, 1).max(0.0) as usize
} else {
0
};
if offset + src.len() > bytes.len() {
return Err(crate::host::range_error("offset is out of bounds"));
}
let mut out = bytes.clone();
out[offset..offset + src.len()].copy_from_slice(&src);
set_bytes(recv, &out);
Ok(Value::Undef)
}
_ if crate::stdlib::typedarray::PROTOTYPE_METHODS.contains(&method) => {
crate::stdlib::typedarray::instance_call(recv, method, args)
}
_ => Err(crate::host::type_error(&format!(
"buffer.{method} is not a function"
))),
}
}
fn fill_pattern(args: &[Value], idx: usize, enc: &str) -> Vec<u8> {
match args.get(idx) {
None => vec![0],
Some(v) => {
let is_str = matches!(v, Value::Str(_))
|| with_host(|h| matches!(h.get(v), Some(JsObj::Str(_))));
if is_str {
decode_str(&arg_str(args, idx), enc)
} else {
vec![super::arg_num(args, idx) as u8]
}
}
}
}
fn arg_is_str(args: &[Value], i: usize) -> bool {
args.get(i)
.is_some_and(|v| with_host(|h| h.as_str(v)).is_some())
}
fn store_bytes(recv: &Value, bytes: &[u8], off: usize, out: &[u8]) -> Result<(), String> {
if off + out.len() > bytes.len() {
return Err(range_error_out_of_bounds());
}
let mut b = bytes.to_vec();
b[off..off + out.len()].copy_from_slice(out);
set_bytes(recv, &b);
Ok(())
}
fn range_error_out_of_bounds() -> String {
crate::host::plain_coded_error(
"RangeError",
"ERR_OUT_OF_RANGE",
"Attempt to access memory outside buffer bounds",
)
}
fn set_bytes(recv: &Value, new: &[u8]) {
let (off, _) = window(recv);
with_host(|h| {
let arr = match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@bytes").cloned(),
_ => None,
};
if let Some(a) = arr {
if let Some(JsObj::Array(items)) = h.get_mut(&a) {
for (i, b) in new.iter().enumerate() {
if off + i < items.len() {
items[off + i] = Value::Float(*b as f64);
}
}
}
}
});
}
fn slice_bounds(args: &[Value], len: usize) -> (usize, usize) {
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))
};
(s.min(e), e.max(s))
}
pub(crate) fn decode_str(s: &str, enc: &str) -> Vec<u8> {
match enc.to_ascii_lowercase().as_str() {
"hex" => from_hex(s),
"base64" | "base64url" => from_base64(s),
"ascii" | "latin1" | "binary" => s.encode_utf16().map(|u| u as u8).collect(),
"utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
}
_ => s.as_bytes().to_vec(),
}
}
pub(crate) fn encode_bytes(bytes: &[u8], enc: &str) -> String {
match enc.to_ascii_lowercase().as_str() {
"hex" => to_hex(bytes),
"base64" => to_base64(bytes),
"base64url" => super::to_base64url(bytes),
"ascii" => bytes.iter().map(|b| (*b & 0x7f) as char).collect(),
"latin1" | "binary" => bytes.iter().map(|b| *b as char).collect(),
"utf16le" | "utf-16le" | "ucs2" | "ucs-2" => {
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
crate::utf16::to_string_lossy(&units)
}
_ => String::from_utf8_lossy(bytes).into_owned(),
}
}
fn write_args(args: &[Value], len: usize) -> Option<(usize, usize, String)> {
let num = |i: usize| super::arg_num(args, i);
if args.len() < 2 {
return Some((0, len, "utf8".into()));
}
if arg_is_str(args, 1) {
return Some((0, len, arg_str(args, 1)));
}
let off = num(1);
if !(0.0..=len as f64).contains(&off) {
return None;
}
let off = off as usize;
if args.len() < 3 {
return Some((off, len - off, "utf8".into()));
}
if arg_is_str(args, 2) {
return Some((off, len - off, arg_str(args, 2)));
}
let max = len - off;
let n = (num(2).max(0.0) as usize).min(max);
let enc = if args.len() > 3 {
arg_str(args, 3)
} else {
"utf8".into()
};
Some((off, n, enc))
}
fn truncate_chars(s: &str, enc: &str, max: usize) -> Vec<u8> {
let bytes = decode_str(s, enc);
if bytes.len() <= max {
return bytes;
}
match enc.to_ascii_lowercase().as_str() {
"utf16le" | "utf-16le" | "ucs2" | "ucs-2" => bytes[..max - max % 2].to_vec(),
"hex" | "base64" | "base64url" | "ascii" | "latin1" | "binary" => bytes[..max].to_vec(),
_ => {
let mut end = max;
while end > 0 && (bytes[end] & 0xC0) == 0x80 {
end -= 1;
}
bytes[..end].to_vec()
}
}
}
fn read_offset(args: &[Value], size: usize, len: usize) -> Result<usize, String> {
if len < size {
return Err(crate::host::coded_error(
"RangeError",
"ERR_BUFFER_OUT_OF_BOUNDS",
"Attempt to access memory outside buffer bounds",
));
}
let max = len - size;
let raw = match args.first() {
None | Some(Value::Undef) => 0.0,
Some(_) => super::arg_num(args, 0),
};
if raw.fract() != 0.0 || raw.is_nan() {
return Err(crate::host::coded_error(
"RangeError",
"ERR_OUT_OF_RANGE",
&format!(
"The value of \"offset\" is out of range. It must be an integer. Received {}",
crate::host::fmt_number(raw)
),
));
}
let off = raw;
if off < 0.0 || off > max as f64 {
return Err(crate::host::coded_error(
"RangeError",
"ERR_OUT_OF_RANGE",
&format!(
"The value of \"offset\" is out of range. It must be >= 0 and <= {max}. \
Received {}",
crate::host::fmt_number(raw)
),
));
}
Ok(off as usize)
}