Browse Source

Implement resizable buffers (#3634)

pull/3646/head
José Julián Espina 9 months ago committed by GitHub
parent
commit
a9aeaa5ba3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 14
      core/engine/src/builtins/array/array_iterator.rs
  2. 31
      core/engine/src/builtins/array/mod.rs
  3. 500
      core/engine/src/builtins/array_buffer/mod.rs
  4. 351
      core/engine/src/builtins/array_buffer/shared.rs
  5. 4
      core/engine/src/builtins/array_buffer/tests.rs
  6. 148
      core/engine/src/builtins/array_buffer/utils.rs
  7. 5
      core/engine/src/builtins/atomics/futex.rs
  8. 225
      core/engine/src/builtins/atomics/mod.rs
  9. 322
      core/engine/src/builtins/dataview/mod.rs
  10. 1962
      core/engine/src/builtins/typed_array/builtin.rs
  11. 4
      core/engine/src/builtins/typed_array/mod.rs
  12. 303
      core/engine/src/builtins/typed_array/object.rs
  13. 2
      core/engine/src/context/hooks.rs
  14. 5
      core/engine/src/object/builtins/jsarraybuffer.rs
  15. 94
      core/engine/src/object/builtins/jsdataview.rs
  16. 5
      core/engine/src/object/builtins/jssharedarraybuffer.rs
  17. 1
      test262_config.toml

14
core/engine/src/builtins/array/array_iterator.rs

@ -117,15 +117,17 @@ impl ArrayIterator {
}
let len = if let Some(f) = array_iterator.array.downcast_ref::<TypedArray>() {
if f.is_detached() {
let buf = f.viewed_array_buffer().as_buffer();
let Some(buf) = buf
.bytes(std::sync::atomic::Ordering::SeqCst)
.filter(|buf| !f.is_out_of_bounds(buf.len()))
else {
return Err(JsNativeError::typ()
.with_message(
"Cannot get value from typed array that has a detached array buffer",
)
.with_message("Cannot get value from out of bounds typed array")
.into());
}
};
f.array_length()
f.array_length(buf.len())
} else {
array_iterator.array.length_of_array_like(context)?
};

31
core/engine/src/builtins/array/mod.rs

@ -36,7 +36,7 @@ use crate::{
value::{IntegerOrInfinity, JsValue},
Context, JsArgs, JsResult, JsString,
};
use std::cmp::{max, min, Ordering};
use std::cmp::{min, Ordering};
use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
@ -3216,18 +3216,19 @@ impl Array {
) -> JsResult<u64> {
// 1. Let relativeStart be ? ToIntegerOrInfinity(start).
let relative_start = arg.to_integer_or_infinity(context)?;
match relative_start {
let start = match relative_start {
// 2. If relativeStart is -∞, let k be 0.
IntegerOrInfinity::NegativeInfinity => Ok(0),
IntegerOrInfinity::NegativeInfinity => 0,
// 3. Else if relativeStart < 0, let k be max(len + relativeStart, 0).
IntegerOrInfinity::Integer(i) if i < 0 => Ok(max(len as i64 + i, 0) as u64),
// Both `as` casts are safe as both variables are non-negative
IntegerOrInfinity::Integer(i) if i < 0 => len.checked_add_signed(i).unwrap_or(0),
// 4. Else, let k be min(relativeStart, len).
IntegerOrInfinity::Integer(i) => Ok(min(i, len as i64) as u64),
IntegerOrInfinity::Integer(i) => min(i as u64, len),
// Special case - positive infinity. `len` is always smaller than +inf, thus from (4)
IntegerOrInfinity::PositiveInfinity => Ok(len),
}
IntegerOrInfinity::PositiveInfinity => len,
};
Ok(start)
}
/// Represents the algorithm to calculate `relativeEnd` (or `final`) in array functions.
@ -3242,18 +3243,20 @@ impl Array {
} else {
// 1. cont, else let relativeEnd be ? ToIntegerOrInfinity(end).
let relative_end = value.to_integer_or_infinity(context)?;
match relative_end {
let end = match relative_end {
// 2. If relativeEnd is -∞, let final be 0.
IntegerOrInfinity::NegativeInfinity => Ok(0),
IntegerOrInfinity::NegativeInfinity => 0,
// 3. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
IntegerOrInfinity::Integer(i) if i < 0 => Ok(max(len as i64 + i, 0) as u64),
IntegerOrInfinity::Integer(i) if i < 0 => len.checked_add_signed(i).unwrap_or(0),
// 4. Else, let final be min(relativeEnd, len).
// Both `as` casts are safe as both variables are non-negative
IntegerOrInfinity::Integer(i) => Ok(min(i, len as i64) as u64),
IntegerOrInfinity::Integer(i) => min(i as u64, len),
// Special case - positive infinity. `len` is always smaller than +inf, thus from (4)
IntegerOrInfinity::PositiveInfinity => Ok(len),
}
IntegerOrInfinity::PositiveInfinity => len,
};
Ok(end)
}
}

500
core/engine/src/builtins/array_buffer/mod.rs

@ -19,6 +19,7 @@ mod tests;
use std::ops::{Deref, DerefMut};
pub use shared::SharedArrayBuffer;
use std::sync::atomic::Ordering;
use crate::{
builtins::BuiltInObject,
@ -30,7 +31,6 @@ use crate::{
realm::Realm,
string::common::StaticJsStrings,
symbol::JsSymbol,
value::IntegerOrInfinity,
Context, JsArgs, JsData, JsResult, JsString, JsValue,
};
use boa_gc::{Finalize, GcRef, GcRefMut, Trace};
@ -39,7 +39,7 @@ use boa_profiler::Profiler;
use self::utils::{SliceRef, SliceRefMut};
use super::{
typed_array::TypedArray, BuiltInBuilder, BuiltInConstructor, DataView, IntrinsicObject,
typed_array::TypedArray, Array, BuiltInBuilder, BuiltInConstructor, DataView, IntrinsicObject,
};
#[derive(Debug, Clone, Copy)]
@ -53,15 +53,31 @@ where
B: Deref<Target = ArrayBuffer>,
S: Deref<Target = SharedArrayBuffer>,
{
pub(crate) fn data(&self) -> Option<SliceRef<'_>> {
/// Gets the inner data of the buffer.
pub(crate) fn bytes(&self, ordering: Ordering) -> Option<SliceRef<'_>> {
match self {
Self::Buffer(buf) => buf.deref().data().map(SliceRef::Slice),
Self::SharedBuffer(buf) => Some(SliceRef::AtomicSlice(buf.deref().data())),
Self::Buffer(buf) => buf.deref().bytes().map(SliceRef::Slice),
Self::SharedBuffer(buf) => Some(SliceRef::AtomicSlice(buf.deref().bytes(ordering))),
}
}
pub(crate) fn is_detached(&self) -> bool {
self.data().is_none()
/// Gets the inner data of the buffer without accessing the current atomic length.
///
/// Returns `None` if the buffer is detached or if the provided `len` is bigger than
/// the allocated buffer.
#[track_caller]
pub(crate) fn bytes_with_len(&self, len: usize) -> Option<SliceRef<'_>> {
match self {
Self::Buffer(buf) => buf.deref().bytes_with_len(len).map(SliceRef::Slice),
Self::SharedBuffer(buf) => Some(SliceRef::AtomicSlice(buf.deref().bytes_with_len(len))),
}
}
pub(crate) fn is_fixed_len(&self) -> bool {
match self {
Self::Buffer(buf) => buf.is_fixed_len(),
Self::SharedBuffer(buf) => buf.is_fixed_len(),
}
}
}
@ -76,10 +92,28 @@ where
B: DerefMut<Target = ArrayBuffer>,
S: DerefMut<Target = SharedArrayBuffer>,
{
pub(crate) fn data_mut(&mut self) -> Option<SliceRefMut<'_>> {
pub(crate) fn bytes(&mut self, ordering: Ordering) -> Option<SliceRefMut<'_>> {
match self {
Self::Buffer(buf) => buf.deref_mut().bytes_mut().map(SliceRefMut::Slice),
Self::SharedBuffer(buf) => {
Some(SliceRefMut::AtomicSlice(buf.deref_mut().bytes(ordering)))
}
}
}
/// Gets the mutable inner data of the buffer without accessing the current atomic length.
///
/// Returns `None` if the buffer is detached or if the provided `len` is bigger than
/// the allocated buffer.
pub(crate) fn bytes_with_len(&mut self, len: usize) -> Option<SliceRefMut<'_>> {
match self {
Self::Buffer(buf) => buf.deref_mut().data_mut().map(SliceRefMut::Slice),
Self::SharedBuffer(buf) => Some(SliceRefMut::AtomicSlice(buf.deref_mut().data())),
Self::Buffer(buf) => buf
.deref_mut()
.bytes_with_len_mut(len)
.map(SliceRefMut::Slice),
Self::SharedBuffer(buf) => Some(SliceRefMut::AtomicSlice(
buf.deref_mut().bytes_with_len(len),
)),
}
}
}
@ -153,7 +187,7 @@ impl BufferObject {
let lhs = lhs.borrow();
let rhs = rhs.borrow();
std::ptr::eq(lhs.data.data().as_ptr(), rhs.data.data().as_ptr())
std::ptr::eq(lhs.data.as_ptr(), rhs.data.as_ptr())
}
_ => false,
}
@ -166,6 +200,9 @@ pub struct ArrayBuffer {
/// The `[[ArrayBufferData]]` internal slot.
data: Option<Vec<u8>>,
/// The `[[ArrayBufferMaxByteLength]]` internal slot.
max_byte_len: Option<u64>,
/// The `[[ArrayBufferDetachKey]]` internal slot.
detach_key: JsValue,
}
@ -174,6 +211,7 @@ impl ArrayBuffer {
pub(crate) fn from_data(data: Vec<u8>, detach_key: JsValue) -> Self {
Self {
data: Some(data),
max_byte_len: None,
detach_key,
}
}
@ -182,14 +220,38 @@ impl ArrayBuffer {
self.data.as_ref().map_or(0, Vec::len)
}
pub(crate) fn data(&self) -> Option<&[u8]> {
pub(crate) fn bytes(&self) -> Option<&[u8]> {
self.data.as_deref()
}
pub(crate) fn data_mut(&mut self) -> Option<&mut [u8]> {
pub(crate) fn bytes_mut(&mut self) -> Option<&mut [u8]> {
self.data.as_deref_mut()
}
pub(crate) fn vec_mut(&mut self) -> Option<&mut Vec<u8>> {
self.data.as_mut()
}
/// Gets the inner bytes of the buffer without accessing the current atomic length.
#[track_caller]
pub(crate) fn bytes_with_len(&self, len: usize) -> Option<&[u8]> {
if let Some(s) = self.data.as_deref() {
Some(&s[..len])
} else {
None
}
}
/// Gets the mutable inner bytes of the buffer without accessing the current atomic length.
#[track_caller]
pub(crate) fn bytes_with_len_mut(&mut self, len: usize) -> Option<&mut [u8]> {
if let Some(s) = self.data.as_deref_mut() {
Some(&mut s[..len])
} else {
None
}
}
/// Detaches the inner data of this `ArrayBuffer`, returning the original buffer if still
/// present.
///
@ -206,7 +268,7 @@ impl ArrayBuffer {
Ok(self.data.take())
}
/// `25.1.2.2 IsDetachedBuffer ( arrayBuffer )`
/// `IsDetachedBuffer ( arrayBuffer )`
///
/// More information:
/// - [ECMAScript reference][spec]
@ -217,6 +279,10 @@ impl ArrayBuffer {
// 2. Return false.
self.data.is_none()
}
pub(crate) fn is_fixed_len(&self) -> bool {
self.max_byte_len.is_none()
}
}
impl IntrinsicObject for ArrayBuffer {
@ -233,20 +299,41 @@ impl IntrinsicObject for ArrayBuffer {
.name(js_string!("get byteLength"))
.build();
let get_resizable = BuiltInBuilder::callable(realm, Self::get_resizable)
.name(js_string!("get resizable"))
.build();
let get_max_byte_length = BuiltInBuilder::callable(realm, Self::get_max_byte_length)
.name(js_string!("get maxByteLength"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_accessor(
JsSymbol::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.static_method(Self::is_view, js_string!("isView"), 1)
.accessor(
js_string!("byteLength"),
Some(get_byte_length),
None,
flag_attributes,
)
.static_accessor(
JsSymbol::species(),
Some(get_species),
.accessor(
js_string!("resizable"),
Some(get_resizable),
None,
Attribute::CONFIGURABLE,
flag_attributes,
)
.static_method(Self::is_view, js_string!("isView"), 1)
.accessor(
js_string!("maxByteLength"),
Some(get_max_byte_length),
None,
flag_attributes,
)
.method(Self::resize, js_string!("resize"), 1)
.method(Self::slice, js_string!("slice"), 2)
.property(
JsSymbol::to_string_tag(),
@ -271,7 +358,7 @@ impl BuiltInConstructor for ArrayBuffer {
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::array_buffer;
/// `25.1.3.1 ArrayBuffer ( length )`
/// `ArrayBuffer ( length )`
///
/// More information:
/// - [ECMAScript reference][spec]
@ -290,29 +377,20 @@ impl BuiltInConstructor for ArrayBuffer {
}
// 2. Let byteLength be ? ToIndex(length).
let byte_length = args.get_or_undefined(0).to_index(context)?;
let byte_len = args.get_or_undefined(0).to_index(context)?;
// 3. Let requestedMaxByteLength be ? GetArrayBufferMaxByteLengthOption(options).
let max_byte_len = get_max_byte_len(args.get_or_undefined(1), context)?;
// 3. Return ? AllocateArrayBuffer(NewTarget, byteLength).
Ok(Self::allocate(new_target, byte_length, context)?
// 4. Return ? AllocateArrayBuffer(NewTarget, byteLength, requestedMaxByteLength).
Ok(Self::allocate(new_target, byte_len, max_byte_len, context)?
.upcast()
.into())
}
}
impl ArrayBuffer {
/// `25.1.4.3 get ArrayBuffer [ @@species ]`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer-@@species
#[allow(clippy::unnecessary_wraps)]
fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// 1. Return the this value.
Ok(this.clone())
}
/// `25.1.4.1 ArrayBuffer.isView ( arg )`
/// `ArrayBuffer.isView ( arg )`
///
/// More information:
/// - [ECMAScript reference][spec]
@ -331,7 +409,19 @@ impl ArrayBuffer {
.into())
}
/// `25.1.5.1 get ArrayBuffer.prototype.byteLength`
/// `get ArrayBuffer [ @@species ]`
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer-@@species
#[allow(clippy::unnecessary_wraps)]
fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
// 1. Return the this value.
Ok(this.clone())
}
/// `get ArrayBuffer.prototype.byteLength`
///
/// More information:
/// - [ECMAScript reference][spec]
@ -350,7 +440,7 @@ impl ArrayBuffer {
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ()
.with_message("ArrayBuffer.byteLength called with non `ArrayBuffer` object")
.with_message("get ArrayBuffer.prototype.byteLength called with invalid `this`")
})?;
// 4. If IsDetachedBuffer(O) is true, return +0𝔽.
@ -359,7 +449,127 @@ impl ArrayBuffer {
Ok(buf.len().into())
}
/// `25.1.5.3 ArrayBuffer.prototype.slice ( start, end )`
/// [`get ArrayBuffer.prototype.maxByteLength`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.maxbytelength
pub(crate) fn get_max_byte_length(
this: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
// 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
let buf = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ().with_message(
"get ArrayBuffer.prototype.maxByteLength called with invalid `this`",
)
})?;
// 4. If IsDetachedBuffer(O) is true, return +0𝔽.
let Some(data) = buf.bytes() else {
return Ok(JsValue::from(0));
};
// 5. If IsFixedLengthArrayBuffer(O) is true, then
// a. Let length be O.[[ArrayBufferByteLength]].
// 6. Else,
// a. Let length be O.[[ArrayBufferMaxByteLength]].
// 7. Return 𝔽(length).
Ok(buf.max_byte_len.unwrap_or(data.len() as u64).into())
}
/// [`get ArrayBuffer.prototype.resizable`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.resizable
pub(crate) fn get_resizable(
this: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
// 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
let buf = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ()
.with_message("get ArrayBuffer.prototype.resizable called with invalid `this`")
})?;
// 4. If IsFixedLengthArrayBuffer(O) is false, return true; otherwise return false.
Ok(JsValue::from(!buf.is_fixed_len()))
}
/// [`ArrayBuffer.prototype.resize ( newLength )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.prototype.resize
pub(crate) fn resize(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]).
// 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
let buf = this
.as_object()
.and_then(|o| o.clone().downcast::<Self>().ok())
.ok_or_else(|| {
JsNativeError::typ()
.with_message("ArrayBuffer.prototype.resize called with invalid `this`")
})?;
let Some(max_byte_len) = buf.borrow().data.max_byte_len else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer.resize: cannot resize a fixed-length buffer")
.into());
};
// 4. Let newByteLength be ? ToIndex(newLength).
let new_byte_length = args.get_or_undefined(0).to_index(context)?;
let mut buf = buf.borrow_mut();
// 5. If IsDetachedBuffer(O) is true, throw a TypeError exception.
let Some(buf) = buf.data.vec_mut() else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer.resize: cannot resize a detached buffer")
.into());
};
// 6. If newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception.
if new_byte_length > max_byte_len {
return Err(JsNativeError::range()
.with_message(
"ArrayBuffer.resize: new byte length exceeds buffer's maximum byte length",
)
.into());
}
// TODO: 7. Let hostHandled be ? HostResizeArrayBuffer(O, newByteLength).
// 8. If hostHandled is handled, return undefined.
// Used in engines to handle WASM buffers in a special way, but we don't
// have a WASM interpreter in place yet.
// 9. Let oldBlock be O.[[ArrayBufferData]].
// 10. Let newBlock be ? CreateByteDataBlock(newByteLength).
// 11. Let copyLength be min(newByteLength, O.[[ArrayBufferByteLength]]).
// 12. Perform CopyDataBlockBytes(newBlock, 0, oldBlock, 0, copyLength).
// 13. NOTE: Neither creation of the new Data Block nor copying from the old Data Block are observable.
// Implementations may implement this method as in-place growth or shrinkage.
// 14. Set O.[[ArrayBufferData]] to newBlock.
// 15. Set O.[[ArrayBufferByteLength]] to newByteLength.
buf.resize(new_byte_length as usize, 0);
// 16. Return undefined.
Ok(JsValue::undefined())
}
/// `ArrayBuffer.prototype.slice ( start, end )`
///
/// More information:
/// - [ECMAScript reference][spec]
@ -369,89 +579,92 @@ impl ArrayBuffer {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
// 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
let obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("ArrayBuffer.slice called with non-object value")
})?;
let buf = obj.downcast_ref::<Self>().ok_or_else(|| {
JsNativeError::typ().with_message("ArrayBuffer.slice called with invalid object")
let buf = this
.as_object()
.and_then(|o| o.clone().downcast::<Self>().ok())
.ok_or_else(|| {
JsNativeError::typ()
.with_message("ArrayBuffer.slice called with invalid `this` value")
})?;
let len = {
let buf = buf.borrow();
// 4. If IsDetachedBuffer(O) is true, throw a TypeError exception.
if buf.is_detached() {
if buf.data.is_detached() {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer.slice called with detached buffer")
.into());
}
// 5. Let len be O.[[ArrayBufferByteLength]].
buf.data.len() as u64
};
let SliceRange {
start: first,
length: new_len,
} = get_slice_range(
buf.len() as u64,
args.get_or_undefined(0),
args.get_or_undefined(1),
context,
)?;
// 6. Let relativeStart be ? ToIntegerOrInfinity(start).
// 7. If relativeStart = -∞, let first be 0.
// 8. Else if relativeStart < 0, let first be max(len + relativeStart, 0).
// 9. Else, let first be min(relativeStart, len).
let first = Array::get_relative_start(context, args.get_or_undefined(0), len)?;
// 10. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
// 11. If relativeEnd = -∞, let final be 0.
// 12. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
// 13. Else, let final be min(relativeEnd, len).
let final_ = Array::get_relative_end(context, args.get_or_undefined(1), len)?;
// 14. Let newLen be max(final - first, 0).
let new_len = final_.saturating_sub(first);
// 15. Let ctor be ? SpeciesConstructor(O, %ArrayBuffer%).
let ctor = obj.species_constructor(StandardConstructors::array_buffer, context)?;
let ctor = buf
.clone()
.upcast()
.species_constructor(StandardConstructors::array_buffer, context)?;
// 16. Let new be ? Construct(ctor, « 𝔽(newLen) »).
let new = ctor.construct(&[new_len.into()], Some(&ctor), context)?;
{
// 17. Perform ? RequireInternalSlot(new, [[ArrayBufferData]]).
// 18. If IsSharedArrayBuffer(new) is true, throw a TypeError exception.
let new = new.downcast_ref::<Self>().ok_or_else(|| {
JsNativeError::typ().with_message("ArrayBuffer constructor returned invalid object")
})?;
// 19. If IsDetachedBuffer(new) is true, throw a TypeError exception.
if new.is_detached() {
let Ok(new) = new.downcast::<Self>() else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer constructor returned detached ArrayBuffer")
.with_message("ArrayBuffer constructor returned invalid object")
.into());
}
}
};
// 20. If SameValue(new, O) is true, throw a TypeError exception.
if this
.as_object()
.map(|obj| JsObject::equals(obj, &new))
.unwrap_or_default()
{
if JsObject::equals(&buf, &new) {
return Err(JsNativeError::typ()
.with_message("new ArrayBuffer is the same as this ArrayBuffer")
.into());
}
{
let mut new = new
.downcast_mut::<Self>()
.expect("Already checked that `new_obj` was an `ArrayBuffer`");
// 19. If IsDetachedBuffer(new) is true, throw a TypeError exception.
// 25. Let toBuf be new.[[ArrayBufferData]].
let mut new = new.borrow_mut();
let Some(to_buf) = new.data.bytes_mut() else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer constructor returned detached ArrayBuffer")
.into());
};
// 21. If new.[[ArrayBufferByteLength]] < newLen, throw a TypeError exception.
if (new.len() as u64) < new_len {
if (to_buf.len() as u64) < new_len {
return Err(JsNativeError::typ()
.with_message("new ArrayBuffer length too small")
.into());
}
// 22. NOTE: Side-effects of the above steps may have detached O.
// 24. Let fromBuf be O.[[ArrayBufferData]].
let Some(from_buf) = buf.data() else {
// 23. If IsDetachedBuffer(O) is true, throw a TypeError exception.
// 24. Let fromBuf be O.[[ArrayBufferData]].
let buf = buf.borrow();
let Some(from_buf) = buf.data.bytes() else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer detached while ArrayBuffer.slice was running")
.into());
};
// 25. Let toBuf be new.[[ArrayBufferData]].
let to_buf = new
.data
.as_mut()
.expect("ArrayBuffer cannot be detached here");
// 26. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen).
let first = first as usize;
let new_len = new_len as usize;
@ -459,10 +672,10 @@ impl ArrayBuffer {
}
// 27. Return new.
Ok(new.into())
Ok(new.upcast().into())
}
/// `25.1.2.1 AllocateArrayBuffer ( constructor, byteLength )`
/// `AllocateArrayBuffer ( constructor, byteLength )`
///
/// More information:
/// - [ECMAScript reference][spec]
@ -470,86 +683,75 @@ impl ArrayBuffer {
/// [spec]: https://tc39.es/ecma262/#sec-allocatearraybuffer
pub(crate) fn allocate(
constructor: &JsValue,
byte_length: u64,
byte_len: u64,
max_byte_len: Option<u64>,
context: &mut Context,
) -> JsResult<JsObject<ArrayBuffer>> {
// 1. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] »).
// 1. Let slots be « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] ».
// 2. If maxByteLength is present and maxByteLength is not empty, let allocatingResizableBuffer be true; otherwise let allocatingResizableBuffer be false.
// 3. If allocatingResizableBuffer is true, then
// a. If byteLength > maxByteLength, throw a RangeError exception.
// b. Append [[ArrayBufferMaxByteLength]] to slots.
if let Some(max_byte_len) = max_byte_len {
if byte_len > max_byte_len {
return Err(JsNativeError::range()
.with_message("`length` cannot be bigger than `maxByteLength`")
.into());
}
}
// 4. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", slots).
let prototype = get_prototype_from_constructor(
constructor,
StandardConstructors::array_buffer,
context,
)?;
// 2. Let block be ? CreateByteDataBlock(byteLength).
let block = create_byte_data_block(byte_length, context)?;
// 5. Let block be ? CreateByteDataBlock(byteLength).
// Preemptively allocate for `max_byte_len` if possible.
// a. If it is not possible to create a Data Block block consisting of maxByteLength bytes, throw a RangeError exception.
// b. NOTE: Resizable ArrayBuffers are designed to be implementable with in-place growth. Implementations may
// throw if, for example, virtual memory cannot be reserved up front.
let block = create_byte_data_block(byte_len, max_byte_len, context)?;
// 3. Set obj.[[ArrayBufferData]] to block.
// 4. Set obj.[[ArrayBufferByteLength]] to byteLength.
let obj = JsObject::new(
context.root_shape(),
prototype,
Self {
// 6. Set obj.[[ArrayBufferData]] to block.
// 7. Set obj.[[ArrayBufferByteLength]] to byteLength.
data: Some(block),
// 8. If allocatingResizableBuffer is true, then
// c. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength.
max_byte_len,
detach_key: JsValue::Undefined,
},
);
// 5. Return obj.
// 9. Return obj.
Ok(obj)
}
}
/// Utility struct to return the result of the [`get_slice_range`] function.
#[derive(Debug, Clone, Copy)]
struct SliceRange {
start: u64,
length: u64,
}
/// Gets the slice copy range from the original length, the relative start and the end.
fn get_slice_range(
len: u64,
relative_start: &JsValue,
end: &JsValue,
context: &mut Context,
) -> JsResult<SliceRange> {
// 5. Let len be O.[[ArrayBufferByteLength]].
// 6. Let relativeStart be ? ToIntegerOrInfinity(start).
let relative_start = relative_start.to_integer_or_infinity(context)?;
let first = match relative_start {
// 7. If relativeStart is -∞, let first be 0.
IntegerOrInfinity::NegativeInfinity => 0,
// 8. Else if relativeStart < 0, let first be max(len + relativeStart, 0).
IntegerOrInfinity::Integer(i) if i < 0 => len.checked_add_signed(i).unwrap_or(0),
// 9. Else, let first be min(relativeStart, len).
IntegerOrInfinity::Integer(i) => std::cmp::min(i as u64, len),
IntegerOrInfinity::PositiveInfinity => len,
/// Abstract operation [`GetArrayBufferMaxByteLengthOption ( options )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-getarraybuffermaxbytelengthoption
fn get_max_byte_len(options: &JsValue, context: &mut Context) -> JsResult<Option<u64>> {
// 1. If options is not an Object, return empty.
let Some(options) = options.as_object() else {
return Ok(None);
};
// 10. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
let r#final = if end.is_undefined() {
len
} else {
match end.to_integer_or_infinity(context)? {
// 11. If relativeEnd is -∞, let final be 0.
IntegerOrInfinity::NegativeInfinity => 0,
// 12. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
IntegerOrInfinity::Integer(i) if i < 0 => len.checked_add_signed(i).unwrap_or(0),
// 13. Else, let final be min(relativeEnd, len).
IntegerOrInfinity::Integer(i) => std::cmp::min(i as u64, len),
IntegerOrInfinity::PositiveInfinity => len,
}
};
// 2. Let maxByteLength be ? Get(options, "maxByteLength").
let max_byte_len = options.get(js_string!("maxByteLength"), context)?;
// 14. Let newLen be max(final - first, 0).
let new_len = r#final.saturating_sub(first);
// 3. If maxByteLength is undefined, return empty.
if max_byte_len.is_undefined() {
return Ok(None);
}
Ok(SliceRange {
start: first,
length: new_len,
})
// 4. Return ? ToIndex(maxByteLength).
max_byte_len.to_index(context).map(Some)
}
/// `CreateByteDataBlock ( size )` abstract operation.
@ -558,25 +760,37 @@ fn get_slice_range(
/// integer). For more information, check the [spec][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-createbytedatablock
pub(crate) fn create_byte_data_block(size: u64, context: &mut Context) -> JsResult<Vec<u8>> {
if size > context.host_hooks().max_buffer_size(context) {
pub(crate) fn create_byte_data_block(
size: u64,
max_buffer_size: Option<u64>,
context: &mut Context,
) -> JsResult<Vec<u8>> {
let alloc_size = max_buffer_size.unwrap_or(size);
assert!(size <= alloc_size);
if alloc_size > context.host_hooks().max_buffer_size(context) {
return Err(JsNativeError::range()
.with_message(
"cannot allocate a buffer that exceeds the maximum buffer size".to_string(),
)
.into());
}
// 1. Let db be a new Data Block value consisting of size bytes. If it is impossible to
// create such a Data Block, throw a RangeError exception.
let size = size.try_into().map_err(|e| {
let alloc_size = alloc_size.try_into().map_err(|e| {
JsNativeError::range().with_message(format!("couldn't allocate the data block: {e}"))
})?;
let mut data_block = Vec::new();
data_block.try_reserve(size).map_err(|e| {
data_block.try_reserve_exact(alloc_size).map_err(|e| {
JsNativeError::range().with_message(format!("couldn't allocate the data block: {e}"))
})?;
// since size <= alloc_size, then `size` must also fit inside a `usize`.
let size = size as usize;
// 2. Set all of the bytes of db to 0.
data_block.resize(size, 0);

351
core/engine/src/builtins/array_buffer/shared.rs

@ -1,15 +1,18 @@
#![allow(unstable_name_collisions)]
use std::{alloc, sync::Arc};
use std::{
alloc,
sync::{atomic::Ordering, Arc},
};
use boa_profiler::Profiler;
use portable_atomic::AtomicU8;
use portable_atomic::{AtomicU8, AtomicUsize};
use boa_gc::{Finalize, Trace};
use sptr::Strict;
use crate::{
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
builtins::{Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
object::internal_methods::get_prototype_from_constructor,
@ -19,7 +22,7 @@ use crate::{
Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use super::{get_slice_range, utils::copy_shared_to_shared, SliceRange};
use super::{get_max_byte_len, utils::copy_shared_to_shared};
/// The internal representation of a `SharedArrayBuffer` object.
///
@ -27,10 +30,23 @@ use super::{get_slice_range, utils::copy_shared_to_shared, SliceRange};
/// running different JS code at the same time.
#[derive(Debug, Clone, Trace, Finalize, JsData)]
pub struct SharedArrayBuffer {
/// The `[[ArrayBufferData]]` internal slot.
// Shared buffers cannot be detached.
#[unsafe_ignore_trace]
data: Arc<Box<[AtomicU8]>>,
data: Arc<Inner>,
}
#[derive(Debug, Default)]
struct Inner {
// Technically we should have an `[[ArrayBufferData]]` internal slot,
// `[[ArrayBufferByteLengthData]]` and `[[ArrayBufferMaxByteLength]]` slots for growable arrays
// or `[[ArrayBufferByteLength]]` for fixed arrays, but we can save some work
// by just using this representation instead.
//
// The maximum buffer length is represented by `buffer.len()`, and `current_len` has the current
// buffer length, or `None` if this is a fixed buffer; in this case, `buffer.len()` will be
// the true length of the buffer.
buffer: Box<[AtomicU8]>,
current_len: Option<AtomicUsize>,
}
impl SharedArrayBuffer {
@ -41,14 +57,33 @@ impl SharedArrayBuffer {
data: Arc::default(),
}
}
/// Gets the length of this `SharedArrayBuffer`.
pub(crate) fn len(&self) -> usize {
self.data.len()
pub(crate) fn len(&self, ordering: Ordering) -> usize {
self.data
.current_len
.as_ref()
.map_or_else(|| self.data.buffer.len(), |len| len.load(ordering))
}
/// Gets the inner bytes of this `SharedArrayBuffer`.
pub(crate) fn data(&self) -> &[AtomicU8] {
&self.data
pub(crate) fn bytes(&self, ordering: Ordering) -> &[AtomicU8] {
&self.data.buffer[..self.len(ordering)]
}
/// Gets the inner data of the buffer without accessing the current atomic length.
#[track_caller]
pub(crate) fn bytes_with_len(&self, len: usize) -> &[AtomicU8] {
&self.data.buffer[..len]
}
/// Gets a pointer to the internal shared buffer.
pub(crate) fn as_ptr(&self) -> *const AtomicU8 {
(*self.data.buffer).as_ptr()
}
pub(crate) fn is_fixed_len(&self) -> bool {
self.data.current_len.is_none()
}
}
@ -66,20 +101,41 @@ impl IntrinsicObject for SharedArrayBuffer {
.name(js_string!("get byteLength"))
.build();
let get_growable = BuiltInBuilder::callable(realm, Self::get_growable)
.name(js_string!("get growable"))
.build();
let get_max_byte_length = BuiltInBuilder::callable(realm, Self::get_max_byte_length)
.name(js_string!("get maxByteLength"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_accessor(
JsSymbol::species(),
Some(get_species),
None,
Attribute::CONFIGURABLE,
)
.accessor(
js_string!("byteLength"),
Some(get_byte_length),
None,
flag_attributes,
)
.static_accessor(
JsSymbol::species(),
Some(get_species),
.accessor(
js_string!("growable"),
Some(get_growable),
None,
Attribute::CONFIGURABLE,
flag_attributes,
)
.accessor(
js_string!("maxByteLength"),
Some(get_max_byte_length),
None,
flag_attributes,
)
.method(Self::slice, js_string!("slice"), 2)
.method(Self::grow, js_string!("grow"), 1)
.property(
JsSymbol::to_string_tag(),
Self::NAME,
@ -122,10 +178,13 @@ impl BuiltInConstructor for SharedArrayBuffer {
}
// 2. Let byteLength be ? ToIndex(length).
let byte_length = args.get_or_undefined(0).to_index(context)?;
let byte_len = args.get_or_undefined(0).to_index(context)?;
// 3. Let requestedMaxByteLength be ? GetArrayBufferMaxByteLengthOption(options).
let max_byte_len = get_max_byte_len(args.get_or_undefined(1), context)?;
// 3. Return ? AllocateSharedArrayBuffer(NewTarget, byteLength, requestedMaxByteLength).
Ok(Self::allocate(new_target, byte_length, context)?
// 4. Return ? AllocateSharedArrayBuffer(NewTarget, byteLength, requestedMaxByteLength).
Ok(Self::allocate(new_target, byte_len, max_byte_len, context)?
.upcast()
.into())
}
@ -166,12 +225,158 @@ impl SharedArrayBuffer {
.with_message("SharedArrayBuffer.byteLength called with invalid value")
})?;
// TODO: 4. Let length be ArrayBufferByteLength(O, seq-cst).
// 4. Let length be ArrayBufferByteLength(O, seq-cst).
let len = buf.bytes(Ordering::SeqCst).len() as u64;
// 5. Return 𝔽(length).
let len = buf.data().len() as u64;
Ok(len.into())
}
/// [`get SharedArrayBuffer.prototype.growable`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-get-sharedarraybuffer.prototype.growable
pub(crate) fn get_growable(
this: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
// 3. If IsSharedArrayBuffer(O) is false, throw a TypeError exception.
let buf = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ()
.with_message("get SharedArrayBuffer.growable called with invalid `this`")
})?;
// 4. If IsFixedLengthArrayBuffer(O) is false, return true; otherwise return false.
Ok(JsValue::from(!buf.is_fixed_len()))
}
/// [`get SharedArrayBuffer.prototype.maxByteLength`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-get-sharedarraybuffer.prototype.maxbytelength
pub(crate) fn get_max_byte_length(
this: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
// 3. If IsSharedArrayBuffer(O) is false, throw a TypeError exception.
let buf = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| {
JsNativeError::typ()
.with_message("get SharedArrayBuffer.maxByteLength called with invalid value")
})?;
// 4. If IsFixedLengthArrayBuffer(O) is true, then
// a. Let length be O.[[ArrayBufferByteLength]].
// 5. Else,
// a. Let length be O.[[ArrayBufferMaxByteLength]].
// 6. Return 𝔽(length).
Ok(buf.data.buffer.len().into())
}
/// [`SharedArrayBuffer.prototype.grow ( newLength )`][spec].
///
/// [spec]: https://tc39.es/ecma262/sec-sharedarraybuffer.prototype.grow
pub(crate) fn grow(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 3. If IsSharedArrayBuffer(O) is false, throw a TypeError exception.
let Some(buf) = this
.as_object()
.and_then(|o| o.clone().downcast::<Self>().ok())
else {
return Err(JsNativeError::typ()
.with_message("SharedArrayBuffer.grow called with non-object value")
.into());
};
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]).
if buf.borrow().data.is_fixed_len() {
return Err(JsNativeError::typ()
.with_message("SharedArrayBuffer.grow: cannot grow a fixed-length buffer")
.into());
}
// 4. Let newByteLength be ? ToIndex(newLength).
let new_byte_len = args.get_or_undefined(0).to_index(context)?;
// TODO: 5. Let hostHandled be ? HostGrowSharedArrayBuffer(O, newByteLength).
// 6. If hostHandled is handled, return undefined.
// Used in engines to handle WASM buffers in a special way, but we don't
// have a WASM interpreter in place yet.
// 7. Let isLittleEndian be the value of the [[LittleEndian]] field of the surrounding agent's Agent Record.
// 8. Let byteLengthBlock be O.[[ArrayBufferByteLengthData]].
// 9. Let currentByteLengthRawBytes be GetRawBytesFromSharedBlock(byteLengthBlock, 0, biguint64, true, seq-cst).
// 10. Let newByteLengthRawBytes be NumericToRawBytes(biguint64, ℤ(newByteLength), isLittleEndian).
let buf = buf.borrow();
let buf = &buf.data;
// d. If newByteLength < currentByteLength or newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception.
// Extracting this condition outside the CAS since throwing early doesn't affect the correct
// behaviour of the loop.
if new_byte_len > buf.data.buffer.len() as u64 {
return Err(JsNativeError::range()
.with_message(
"SharedArrayBuffer.grow: new length cannot be bigger than `maxByteLength`",
)
.into());
}
let new_byte_len = new_byte_len as usize;
// If we used let-else above to avoid the expect, we would carry a borrow through the `to_index`
// call, which could mutably borrow. Another alternative would be to clone the whole
// `SharedArrayBuffer`, but it's better to avoid contention with the counter in the `Arc` pointer.
let atomic_len = buf
.data
.current_len
.as_ref()
.expect("already checked that the buffer is not fixed-length");
// 11. Repeat,
// a. NOTE: This is a compare-and-exchange loop to ensure that parallel, racing grows of the same buffer are
// totally ordered, are not lost, and do not silently do nothing. The loop exits if it was able to attempt
// to grow uncontended.
// b. Let currentByteLength be ℝ(RawBytesToNumeric(biguint64, currentByteLengthRawBytes, isLittleEndian)).
// c. If newByteLength = currentByteLength, return undefined.
// d. If newByteLength < currentByteLength or newByteLength > O.[[ArrayBufferMaxByteLength]], throw a
// RangeError exception.
// e. Let byteLengthDelta be newByteLength - currentByteLength.
// f. If it is impossible to create a new Shared Data Block value consisting of byteLengthDelta bytes, throw
// a RangeError exception.
// g. NOTE: No new Shared Data Block is constructed and used here. The observable behaviour of growable
// SharedArrayBuffers is specified by allocating a max-sized Shared Data Block at construction time, and
// this step captures the requirement that implementations that run out of memory must throw a RangeError.
// h. Let readByteLengthRawBytes be AtomicCompareExchangeInSharedBlock(byteLengthBlock, 0, 8,
// currentByteLengthRawBytes, newByteLengthRawBytes).
// i. If ByteListEqual(readByteLengthRawBytes, currentByteLengthRawBytes) is true, return undefined.
// j. Set currentByteLengthRawBytes to readByteLengthRawBytes.
// We require SEQ-CST operations because readers of the buffer also use SEQ-CST operations.
atomic_len
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |prev_byte_len| {
(prev_byte_len <= new_byte_len).then_some(new_byte_len)
})
.map_err(|_| {
JsNativeError::range()
.with_message("SharedArrayBuffer.grow: failed to grow buffer to new length")
})?;
Ok(JsValue::undefined())
}
/// `SharedArrayBuffer.prototype.slice ( start, end )`
///
/// More information:
@ -181,32 +386,45 @@ impl SharedArrayBuffer {
fn slice(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
let obj = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("ArrayBuffer.slice called with non-object value")
})?;
// 3. If IsSharedArrayBuffer(O) is false, throw a TypeError exception.
let buf = obj.downcast_ref::<Self>().ok_or_else(|| {
JsNativeError::typ().with_message("ArrayBuffer.slice called with invalid object")
let buf = this
.as_object()
.and_then(|o| o.clone().downcast::<Self>().ok())
.ok_or_else(|| {
JsNativeError::typ()
.with_message("SharedArrayBuffer.slice called with invalid `this` value")
})?;
let SliceRange {
start: first,
length: new_len,
} = get_slice_range(
buf.len() as u64,
args.get_or_undefined(0),
args.get_or_undefined(1),
context,
)?;
// 4. Let len be ArrayBufferByteLength(O, seq-cst).
let len = buf.borrow().data.len(Ordering::SeqCst);
// 5. Let relativeStart be ? ToIntegerOrInfinity(start).
// 6. If relativeStart = -∞, let first be 0.
// 7. Else if relativeStart < 0, let first be max(len + relativeStart, 0).
// 8. Else, let first be min(relativeStart, len).
let first = Array::get_relative_start(context, args.get_or_undefined(0), len as u64)?;
// 9. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
// 10. If relativeEnd = -∞, let final be 0.
// 11. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
// 12. Else, let final be min(relativeEnd, len).
let final_ = Array::get_relative_end(context, args.get_or_undefined(1), len as u64)?;
// 13. Let newLen be max(final - first, 0).
let new_len = final_.saturating_sub(first);
// 14. Let ctor be ? SpeciesConstructor(O, %SharedArrayBuffer%).
let ctor = obj.species_constructor(StandardConstructors::shared_array_buffer, context)?;
let ctor = buf
.clone()
.upcast()
.species_constructor(StandardConstructors::shared_array_buffer, context)?;
// 15. Let new be ? Construct(ctor, « 𝔽(newLen) »).
let new = ctor.construct(&[new_len.into()], Some(&ctor), context)?;
{
let buf = buf.borrow();
let buf = &buf.data;
// 16. Perform ? RequireInternalSlot(new, [[ArrayBufferData]]).
// 17. If IsSharedArrayBuffer(new) is false, throw a TypeError exception.
let new = new.downcast_ref::<Self>().ok_or_else(|| {
@ -215,33 +433,37 @@ impl SharedArrayBuffer {
})?;
// 18. If new.[[ArrayBufferData]] is O.[[ArrayBufferData]], throw a TypeError exception.
if std::ptr::eq(buf.data().as_ptr(), new.data().as_ptr()) {
if std::ptr::eq(buf.as_ptr(), new.as_ptr()) {
return Err(JsNativeError::typ()
.with_message("cannot reuse the same `SharedArrayBuffer` for a slice operation")
.with_message("cannot reuse the same SharedArrayBuffer for a slice operation")
.into());
}
// TODO: 19. If ArrayBufferByteLength(new, seq-cst) < newLen, throw a TypeError exception.
if (new.len() as u64) < new_len {
// 19. If ArrayBufferByteLength(new, seq-cst) < newLen, throw a TypeError exception.
if (new.len(Ordering::SeqCst) as u64) < new_len {
return Err(JsNativeError::typ()
.with_message("invalid size of constructed shared array")
.with_message("invalid size of constructed SharedArrayBuffer")
.into());
}
let first = first as usize;
let new_len = new_len as usize;
// 20. Let fromBuf be O.[[ArrayBufferData]].
let from_buf = buf.data();
let from_buf = &buf.bytes_with_len(len)[first..];
// 21. Let toBuf be new.[[ArrayBufferData]].
let to_buf = new.data();
let to_buf = new;
// 22. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen).
let first = first as usize;
let new_len = new_len as usize;
// Sanity check to ensure there is enough space inside `from_buf` for
// `new_len` elements.
debug_assert!(from_buf.len() >= new_len);
// 22. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen).
// SAFETY: `get_slice_range` will always return indices that are in-bounds.
// This also means that the newly created buffer will have at least `new_len` elements
// to write to.
unsafe { copy_shared_to_shared(&from_buf[first..], to_buf, new_len) }
unsafe { copy_shared_to_shared(from_buf.as_ptr(), to_buf.as_ptr(), new_len) }
}
// 23. Return new.
@ -256,10 +478,10 @@ impl SharedArrayBuffer {
/// [spec]: https://tc39.es/ecma262/#sec-allocatesharedarraybuffer
pub(crate) fn allocate(
constructor: &JsValue,
byte_length: u64,
byte_len: u64,
max_byte_len: Option<u64>,
context: &mut Context,
) -> JsResult<JsObject<SharedArrayBuffer>> {
// TODO:
// 1. Let slots be « [[ArrayBufferData]] ».
// 2. If maxByteLength is present and maxByteLength is not empty, let allocatingGrowableBuffer
// be true; otherwise let allocatingGrowableBuffer be false.
@ -268,6 +490,13 @@ impl SharedArrayBuffer {
// b. Append [[ArrayBufferByteLengthData]] and [[ArrayBufferMaxByteLength]] to slots.
// 4. Else,
// a. Append [[ArrayBufferByteLength]] to slots.
if let Some(max_byte_len) = max_byte_len {
if byte_len > max_byte_len {
return Err(JsNativeError::range()
.with_message("`length` cannot be bigger than `maxByteLength`")
.into());
}
}
// 5. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%SharedArrayBuffer.prototype%", slots).
let prototype = get_prototype_from_constructor(
@ -276,24 +505,36 @@ impl SharedArrayBuffer {
context,
)?;
// TODO: 6. If allocatingGrowableBuffer is true, let allocLength be maxByteLength;
// 6. If allocatingGrowableBuffer is true, let allocLength be maxByteLength;
// otherwise let allocLength be byteLength.
let alloc_len = max_byte_len.unwrap_or(byte_len);
// 7. Let block be ? CreateSharedByteDataBlock(allocLength).
// 8. Set obj.[[ArrayBufferData]] to block.
let data = create_shared_byte_data_block(byte_length, context)?;
let block = create_shared_byte_data_block(alloc_len, context)?;
// TODO:
// 9. If allocatingGrowableBuffer is true, then
// `byte_len` must fit inside an `usize` thanks to the checks inside
// `create_shared_byte_data_block`.
// a. Assert: byteLength ≤ maxByteLength.
// b. Let byteLengthBlock be ? CreateSharedByteDataBlock(8).
// c. Perform SetValueInBuffer(byteLengthBlock, 0, biguint64, ℤ(byteLength), true, seq-cst).
// d. Set obj.[[ArrayBufferByteLengthData]] to byteLengthBlock.
// e. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength.
let current_len = max_byte_len.map(|_| AtomicUsize::new(byte_len as usize));
// 10. Else,
// a. Set obj.[[ArrayBufferByteLength]] to byteLength.
let obj = JsObject::new(context.root_shape(), prototype, Self { data });
let obj = JsObject::new(
context.root_shape(),
prototype,
Self {
data: Arc::new(Inner {
buffer: block,
current_len,
}),
},
);
// 11. Return obj.
Ok(obj)
@ -310,7 +551,7 @@ impl SharedArrayBuffer {
pub(crate) fn create_shared_byte_data_block(
size: u64,
context: &mut Context,
) -> JsResult<Arc<Box<[AtomicU8]>>> {
) -> JsResult<Box<[AtomicU8]>> {
if size > context.host_hooks().max_buffer_size(context) {
return Err(JsNativeError::range()
.with_message(
@ -327,7 +568,7 @@ pub(crate) fn create_shared_byte_data_block(
if size == 0 {
// Must ensure we don't allocate a zero-sized buffer.
return Ok(Arc::new(Box::new([])));
return Ok(Box::default());
}
// 2. Let execution be the [[CandidateExecution]] field of the surrounding agent's Agent Record.
@ -371,5 +612,5 @@ pub(crate) fn create_shared_byte_data_block(
assert_eq!(buffer.as_ptr().addr() % std::mem::align_of::<u64>(), 0);
// 3. Return db.
Ok(Arc::new(buffer))
Ok(buffer)
}

4
core/engine/src/builtins/array_buffer/tests.rs

@ -4,10 +4,10 @@ use crate::Context;
fn create_byte_data_block() {
let context = &mut Context::default();
// Sunny day
assert!(super::create_byte_data_block(100, context).is_ok());
assert!(super::create_byte_data_block(100, None, context).is_ok());
// Rainy day
assert!(super::create_byte_data_block(u64::MAX, context).is_err());
assert!(super::create_byte_data_block(u64::MAX, None, context).is_err());
}
#[test]

148
core/engine/src/builtins/array_buffer/utils.rs

@ -1,6 +1,6 @@
#![allow(unstable_name_collisions)]
use std::{ptr, slice::SliceIndex, sync::atomic};
use std::{ptr, slice::SliceIndex, sync::atomic::Ordering};
use portable_atomic::AtomicU8;
@ -11,6 +11,18 @@ use crate::{
use super::ArrayBuffer;
#[derive(Clone, Copy)]
pub(crate) enum BytesConstPtr {
Bytes(*const u8),
AtomicBytes(*const AtomicU8),
}
#[derive(Clone, Copy)]
pub(crate) enum BytesMutPtr {
Bytes(*mut u8),
AtomicBytes(*const AtomicU8),
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum SliceRef<'a> {
Slice(&'a [u8]),
@ -49,6 +61,14 @@ impl SliceRef<'_> {
}
}
/// Gets a pointer to the underlying slice.
pub(crate) fn as_ptr(&self) -> BytesConstPtr {
match self {
SliceRef::Slice(s) => BytesConstPtr::Bytes(s.as_ptr()),
SliceRef::AtomicSlice(s) => BytesConstPtr::AtomicBytes(s.as_ptr()),
}
}
/// [`GetValueFromBuffer ( arrayBuffer, byteIndex, type, isTypedArray, order [ , isLittleEndian ] )`][spec]
///
/// The start offset is determined by the input buffer instead of a `byteIndex` parameter.
@ -62,9 +82,9 @@ impl SliceRef<'_> {
pub(crate) unsafe fn get_value(
&self,
kind: TypedArrayKind,
order: atomic::Ordering,
order: Ordering,
) -> TypedArrayElement {
unsafe fn read_elem<T: Element>(buffer: SliceRef<'_>, order: atomic::Ordering) -> T {
unsafe fn read_elem<T: Element>(buffer: SliceRef<'_>, order: Ordering) -> T {
// <https://tc39.es/ecma262/#sec-getvaluefrombuffer>
// 1. Assert: IsDetachedBuffer(arrayBuffer) is false.
@ -116,10 +136,7 @@ impl SliceRef<'_> {
}
}
/// `25.1.2.4 CloneArrayBuffer ( srcBuffer, srcByteOffset, srcLength )`
///
/// More information:
/// - [ECMAScript reference][spec]
/// [`CloneArrayBuffer ( srcBuffer, srcByteOffset, srcLength )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-clonearraybuffer
pub(crate) fn clone(&self, context: &mut Context) -> JsResult<JsObject<ArrayBuffer>> {
@ -135,6 +152,7 @@ impl SliceRef<'_> {
.constructor()
.into(),
self.len() as u64,
None,
context,
)?;
@ -145,14 +163,20 @@ impl SliceRef<'_> {
let mut target_buffer = target_buffer.borrow_mut();
let target_block = target_buffer
.data
.data_mut()
.bytes_mut()
.expect("ArrayBuffer cannot be detached here");
// 5. Perform CopyDataBlockBytes(targetBlock, 0, srcBlock, srcByteOffset, srcLength).
// SAFETY: Both buffers are of the same length, `buffer.len()`, which makes this operation
// safe.
unsafe { memcpy(*self, SliceRefMut::Slice(target_block), self.len()) }
unsafe {
memcpy(
self.as_ptr(),
BytesMutPtr::Bytes(target_block.as_mut_ptr()),
self.len(),
);
}
}
// 6. Return targetBuffer.
@ -180,7 +204,6 @@ pub(crate) enum SliceRefMut<'a> {
impl SliceRefMut<'_> {
/// Gets the byte length of this `SliceRefMut`.
#[cfg(debug_assertions)]
pub(crate) fn len(&self) -> usize {
match self {
Self::Slice(buf) => buf.len(),
@ -213,6 +236,14 @@ impl SliceRefMut<'_> {
}
}
/// Gets a pointer to the underlying slice.
pub(crate) fn as_ptr(&mut self) -> BytesMutPtr {
match self {
Self::Slice(s) => BytesMutPtr::Bytes(s.as_mut_ptr()),
Self::AtomicSlice(s) => BytesMutPtr::AtomicBytes(s.as_ptr()),
}
}
/// `25.1.2.12 SetValueInBuffer ( arrayBuffer, byteIndex, type, value, isTypedArray, order [ , isLittleEndian ] )`
///
/// The start offset is determined by the input buffer instead of a `byteIndex` parameter.
@ -230,12 +261,8 @@ impl SliceRefMut<'_> {
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-setvalueinbuffer
pub(crate) unsafe fn set_value(&mut self, value: TypedArrayElement, order: atomic::Ordering) {
unsafe fn write_elem<T: Element>(
buffer: SliceRefMut<'_>,
value: T,
order: atomic::Ordering,
) {
pub(crate) unsafe fn set_value(&mut self, value: TypedArrayElement, order: Ordering) {
unsafe fn write_elem<T: Element>(buffer: SliceRefMut<'_>, value: T, order: Ordering) {
// <https://tc39.es/ecma262/#sec-setvalueinbuffer>
// 1. Assert: IsDetachedBuffer(arrayBuffer) is false.
@ -309,15 +336,16 @@ impl<'a> From<&'a [AtomicU8]> for SliceRefMut<'a> {
///
/// - Both `src` and `dest` must have at least `count` bytes to read and write,
/// respectively.
pub(super) unsafe fn copy_shared_to_shared(src: &[AtomicU8], dest: &[AtomicU8], count: usize) {
pub(super) unsafe fn copy_shared_to_shared(
src: *const AtomicU8,
dest: *const AtomicU8,
count: usize,
) {
// TODO: this could be optimized with batches of writes using `u32/u64` stores instead.
for i in 0..count {
// SAFETY: The invariants of this operation are ensured by the caller of the function.
unsafe {
dest.get_unchecked(i).store(
src.get_unchecked(i).load(atomic::Ordering::Relaxed),
atomic::Ordering::Relaxed,
);
(*dest.add(i)).store((*src.add(i)).load(Ordering::Relaxed), Ordering::Relaxed);
}
}
}
@ -328,60 +356,48 @@ pub(super) unsafe fn copy_shared_to_shared(src: &[AtomicU8], dest: &[AtomicU8],
///
/// - Both `src` and `dest` must have at least `count` bytes to read and write,
/// respectively.
unsafe fn copy_shared_to_shared_backwards(src: &[AtomicU8], dest: &[AtomicU8], count: usize) {
unsafe fn copy_shared_to_shared_backwards(
src: *const AtomicU8,
dest: *const AtomicU8,
count: usize,
) {
for i in (0..count).rev() {
// SAFETY: The invariants of this operation are ensured by the caller of the function.
unsafe {
dest.get_unchecked(i).store(
src.get_unchecked(i).load(atomic::Ordering::Relaxed),
atomic::Ordering::Relaxed,
);
(*dest.add(i)).store((*src.add(i)).load(Ordering::Relaxed), Ordering::Relaxed);
}
}
}
/// Copies `count` bytes from the buffer `src` into the buffer `dest`, using the atomic ordering `order`
/// if any of the buffers are atomic.
/// Copies `count` bytes from the buffer `src` into the buffer `dest`, using the atomic ordering
/// `Ordering::Relaxed` if any of the buffers are atomic.
///
/// # Safety
///
/// - Both `src` and `dest` must have at least `count` bytes to read and write, respectively.
/// - The region of memory referenced by `src` must not overlap with the region of memory
/// referenced by `dest`. This is guaranteed if either of them are slices
/// (you cannot borrow and mutably borrow a slice at the same time), but cannot be guaranteed
/// for atomic slices.
pub(crate) unsafe fn memcpy(src: SliceRef<'_>, dest: SliceRefMut<'_>, count: usize) {
#[cfg(debug_assertions)]
{
assert!(src.len() >= count);
assert!(dest.len() >= count);
let src_range = src.addr()..src.addr() + src.len();
let dest_range = dest.addr()..dest.addr() + dest.len();
assert!(!src_range.contains(&dest_range.start));
assert!(!src_range.contains(&dest_range.end));
}
/// referenced by `dest`.
pub(crate) unsafe fn memcpy(src: BytesConstPtr, dest: BytesMutPtr, count: usize) {
// TODO: this could be optimized with batches of writes using `u32/u64` stores instead.
match (src, dest) {
// SAFETY: The invariants of this operation are ensured by the caller of the function.
(SliceRef::Slice(src), SliceRefMut::Slice(dest)) => unsafe {
ptr::copy_nonoverlapping(src.as_ptr(), dest.as_mut_ptr(), count);
(BytesConstPtr::Bytes(src), BytesMutPtr::Bytes(dest)) => unsafe {
ptr::copy_nonoverlapping(src, dest, count);
},
// SAFETY: The invariants of this operation are ensured by the caller of the function.
(SliceRef::Slice(src), SliceRefMut::AtomicSlice(dest)) => unsafe {
(BytesConstPtr::Bytes(src), BytesMutPtr::AtomicBytes(dest)) => unsafe {
for i in 0..count {
dest.get_unchecked(i)
.store(*src.get_unchecked(i), atomic::Ordering::Relaxed);
(*dest.add(i)).store(*src.add(i), Ordering::Relaxed);
}
},
// SAFETY: The invariants of this operation are ensured by the caller of the function.
(SliceRef::AtomicSlice(src), SliceRefMut::Slice(dest)) => unsafe {
(BytesConstPtr::AtomicBytes(src), BytesMutPtr::Bytes(dest)) => unsafe {
for i in 0..count {
*dest.get_unchecked_mut(i) = src.get_unchecked(i).load(atomic::Ordering::Relaxed);
*dest.add(i) = (*src.add(i)).load(Ordering::Relaxed);
}
},
// SAFETY: The invariants of this operation are ensured by the caller of the function.
(SliceRef::AtomicSlice(src), SliceRefMut::AtomicSlice(dest)) => unsafe {
(BytesConstPtr::AtomicBytes(src), BytesMutPtr::AtomicBytes(dest)) => unsafe {
copy_shared_to_shared(src, dest, count);
},
}
@ -391,28 +407,20 @@ pub(crate) unsafe fn memcpy(src: SliceRef<'_>, dest: SliceRefMut<'_>, count: usi
///
/// # Safety
///
/// - `buffer` must contain at least `from + count` bytes to be read.
/// - `buffer` must contain at least `to + count` bytes to be written.
pub(crate) unsafe fn memmove(buffer: SliceRefMut<'_>, from: usize, to: usize, count: usize) {
#[cfg(debug_assertions)]
{
assert!(from + count <= buffer.len());
assert!(to + count <= buffer.len());
}
match buffer {
/// - `ptr` must be valid from the offset `ptr + from` for `count` reads of bytes.
/// - `ptr` must be valid from the offset `ptr + to` for `count` writes of bytes.
pub(crate) unsafe fn memmove(ptr: BytesMutPtr, from: usize, to: usize, count: usize) {
match ptr {
// SAFETY: The invariants of this operation are ensured by the caller of the function.
SliceRefMut::Slice(buf) => unsafe {
let ptr = buf.as_mut_ptr();
let src_ptr = ptr.add(from);
let dest_ptr = ptr.add(to);
ptr::copy(src_ptr, dest_ptr, count);
BytesMutPtr::Bytes(ptr) => unsafe {
let src = ptr.add(from);
let dest = ptr.add(to);
ptr::copy(src, dest, count);
},
// SAFETY: The invariants of this operation are ensured by the caller of the function.
SliceRefMut::AtomicSlice(buf) => unsafe {
let src = buf.get_unchecked(from..);
let dest = buf.get_unchecked(to..);
BytesMutPtr::AtomicBytes(ptr) => unsafe {
let src = ptr.add(from);
let dest = ptr.add(to);
// Let's draw a simple array.
//
// | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
@ -453,7 +461,7 @@ pub(crate) unsafe fn memmove(buffer: SliceRefMut<'_>, from: usize, to: usize, co
// | 0 | 1 | 0 | 1 | 2 | 3 | 6 | 7 | 8 |
// ^ ^
// from to
if from < to && to < from + count {
if src < dest {
copy_shared_to_shared_backwards(src, dest, count);
} else {
copy_shared_to_shared(src, dest, count);

5
core/engine/src/builtins/atomics/futex.rs

@ -273,6 +273,7 @@ pub(super) enum AtomicsWaitResult {
// our implementation guarantees that `SharedArrayBuffer` is always aligned to `u64` at minimum.
pub(super) unsafe fn wait<E: Element + PartialEq>(
buffer: &SharedArrayBuffer,
buf_len: usize,
offset: usize,
check: E,
timeout: Option<Duration>,
@ -287,7 +288,7 @@ pub(super) unsafe fn wait<E: Element + PartialEq>(
let time_info = timeout.map(|timeout| (Instant::now(), timeout));
let buffer = &buffer.data()[offset..];
let buffer = &buffer.bytes_with_len(buf_len)[offset..];
// 13. Let elementType be TypedArrayElementType(typedArray).
// 14. Let w be GetValueFromBuffer(buffer, indexedPosition, elementType, true, SeqCst).
@ -380,7 +381,7 @@ pub(super) unsafe fn wait<E: Element + PartialEq>(
/// Notifies at most `count` agents waiting on the memory address pointed to by `buffer[offset..]`.
pub(super) fn notify(buffer: &SharedArrayBuffer, offset: usize, count: u64) -> JsResult<u64> {
let addr = buffer.data()[offset..].as_ptr().addr();
let addr = buffer.as_ptr().addr() + offset;
// 7. Let WL be GetWaiterList(block, indexedPosition).
// 8. Perform EnterCriticalSection(WL).

225
core/engine/src/builtins/atomics/mod.rs

@ -20,11 +20,11 @@ use crate::{
sys::time::Duration, value::IntegerOrInfinity, Context, JsArgs, JsNativeError, JsResult,
JsString, JsValue,
};
use boa_gc::GcRef;
use boa_profiler::Profiler;
use super::{
array_buffer::BufferRef,
array_buffer::{BufferObject, BufferRef},
typed_array::{Atomic, ContentType, Element, TypedArray, TypedArrayElement, TypedArrayKind},
BuiltInBuilder, IntrinsicObject,
};
@ -75,19 +75,35 @@ macro_rules! atomic_op {
let index = args.get_or_undefined(1);
let value = args.get_or_undefined(2);
let ii = validate_integer_typed_array(array, false)?;
let pos = validate_atomic_access(&ii, index, context)?;
let value = ii.kind().get_element(value, context)?;
// AtomicReadModifyWrite ( typedArray, index, value, op )
// <https://tc39.es/ecma262/#sec-atomicreadmodifywrite>
// 1. Let buffer be ? ValidateIntegerTypedArray(typedArray).
let (ta, buf_len) = validate_integer_typed_array(array, false)?;
// 2. Let indexedPosition be ? ValidateAtomicAccess(typedArray, index).
let access = validate_atomic_access(&ta, buf_len, index, context)?;
// 3. If typedArray.[[ContentType]] is BigInt, let v be ? ToBigInt(value).
// 4. Otherwise, let v be 𝔽(? ToIntegerOrInfinity(value)).
// 7. Let elementType be TypedArrayElementType(typedArray).
let value = access.kind.get_element(value, context)?;
// revalidate
let mut buffer = ii.viewed_array_buffer().as_buffer_mut();
let Some(mut data) = buffer.data_mut() else {
// 5. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
// 6. NOTE: The above check is not redundant with the check in ValidateIntegerTypedArray because the call
// to ToBigInt or ToIntegerOrInfinity on the preceding lines can have arbitrary side effects, which could
// cause the buffer to become detached.
let ta = ta.borrow();
let ta = &ta.data;
let mut buffer = ta.viewed_array_buffer().as_buffer_mut();
let Some(mut data) = buffer.bytes_with_len(buf_len) else {
return Err(JsNativeError::typ()
.with_message("cannot execute atomic operation in detached buffer")
.into());
};
let data = data.subslice_mut(pos..);
let data = data.subslice_mut(access.byte_offset..);
// 8. Return GetModifySetValueInBuffer(buffer, indexedPosition, elementType, v, op).
// SAFETY: The integer indexed object guarantees that the buffer is aligned.
// The call to `validate_atomic_access` guarantees that the index is in-bounds.
let value: TypedArrayElement = unsafe {
@ -161,24 +177,26 @@ impl Atomics {
let index = args.get_or_undefined(1);
// 1. Let indexedPosition be ? ValidateAtomicAccessOnIntegerTypedArray(typedArray, index).
let ii = validate_integer_typed_array(array, false)?;
let pos = validate_atomic_access(&ii, index, context)?;
let (ta, buf_len) = validate_integer_typed_array(array, false)?;
let access = validate_atomic_access(&ta, buf_len, index, context)?;
// 2. Perform ? RevalidateAtomicAccess(typedArray, indexedPosition).
let buffer = ii.viewed_array_buffer().as_buffer();
let Some(data) = buffer.data() else {
let ta = ta.borrow();
let ta = &ta.data;
let buffer = ta.viewed_array_buffer().as_buffer();
let Some(data) = buffer.bytes_with_len(buf_len) else {
return Err(JsNativeError::typ()
.with_message("cannot execute atomic operation in detached buffer")
.into());
};
let data = data.subslice(pos..);
let data = data.subslice(access.byte_offset..);
// 3. Let buffer be typedArray.[[ViewedArrayBuffer]].
// 4. Let elementType be TypedArrayElementType(typedArray).
// 5. Return GetValueFromBuffer(buffer, indexedPosition, elementType, true, seq-cst).
// SAFETY: The integer indexed object guarantees that the buffer is aligned.
// The call to `validate_atomic_access` guarantees that the index is in-bounds.
let value = unsafe { data.get_value(ii.kind(), Ordering::SeqCst) };
let value = unsafe { data.get_value(access.kind, Ordering::SeqCst) };
Ok(value.into())
}
@ -192,12 +210,12 @@ impl Atomics {
let value = args.get_or_undefined(2);
// 1. Let indexedPosition be ? ValidateAtomicAccessOnIntegerTypedArray(typedArray, index).
let ii = validate_integer_typed_array(array, false)?;
let pos = validate_atomic_access(&ii, index, context)?;
let (ta, buf_len) = validate_integer_typed_array(array, false)?;
let access = validate_atomic_access(&ta, buf_len, index, context)?;
// bit of a hack to preserve the converted value
// 2. If typedArray.[[ContentType]] is bigint, let v be ? ToBigInt(value).
let converted: JsValue = if ii.kind().content_type() == ContentType::BigInt {
let converted: JsValue = if access.kind.content_type() == ContentType::BigInt {
value.to_bigint(context)?.into()
} else {
// 3. Otherwise, let v be 𝔽(? ToIntegerOrInfinity(value)).
@ -208,16 +226,18 @@ impl Atomics {
}
.into()
};
let value = ii.kind().get_element(&converted, context)?;
let value = access.kind.get_element(&converted, context)?;
// 4. Perform ? RevalidateAtomicAccess(typedArray, indexedPosition).
let mut buffer = ii.viewed_array_buffer().as_buffer_mut();
let Some(mut buffer) = buffer.data_mut() else {
let ta = ta.borrow();
let ta = &ta.data;
let mut buffer = ta.viewed_array_buffer().as_buffer_mut();
let Some(mut buffer) = buffer.bytes_with_len(buf_len) else {
return Err(JsNativeError::typ()
.with_message("cannot execute atomic operation in detached buffer")
.into());
};
let mut data = buffer.subslice_mut(pos..);
let mut data = buffer.subslice_mut(access.byte_offset..);
// 5. Let buffer be typedArray.[[ViewedArrayBuffer]].
// 6. Let elementType be TypedArrayElementType(typedArray).
@ -244,9 +264,8 @@ impl Atomics {
// 1. Let indexedPosition be ? ValidateAtomicAccessOnIntegerTypedArray(typedArray, index).
// 2. Let buffer be typedArray.[[ViewedArrayBuffer]].
// 3. Let block be buffer.[[ArrayBufferData]].
let ii = validate_integer_typed_array(array, false)?;
let pos = validate_atomic_access(&ii, index, context)?;
let typed_array_kind = ii.kind();
let (ta, buf_len) = validate_integer_typed_array(array, false)?;
let access = validate_atomic_access(&ta, buf_len, index, context)?;
// 4. If typedArray.[[ContentType]] is bigint, then
// a. Let expected be ? ToBigInt(expectedValue).
@ -254,19 +273,19 @@ impl Atomics {
// 5. Else,
// a. Let expected be 𝔽(? ToIntegerOrInfinity(expectedValue)).
// b. Let replacement be 𝔽(? ToIntegerOrInfinity(replacementValue)).
let exp = typed_array_kind.get_element(expected, context)?.to_bytes();
let rep = typed_array_kind
.get_element(replacement, context)?
.to_bytes();
let exp = access.kind.get_element(expected, context)?.to_bits();
let rep = access.kind.get_element(replacement, context)?.to_bits();
// 6. Perform ? RevalidateAtomicAccess(typedArray, indexedPosition).
let mut buffer = ii.viewed_array_buffer().as_buffer_mut();
let Some(mut data) = buffer.data_mut() else {
let ta = ta.borrow();
let ta = &ta.data;
let mut buffer = ta.viewed_array_buffer().as_buffer_mut();
let Some(mut buffer) = buffer.bytes_with_len(buf_len) else {
return Err(JsNativeError::typ()
.with_message("cannot execute atomic operation in detached buffer")
.into());
};
let data = data.subslice_mut(pos..);
let data = buffer.subslice_mut(access.byte_offset..);
// 7. Let elementType be TypedArrayElementType(typedArray).
// 8. Let elementSize be TypedArrayElementSize(typedArray).
@ -280,10 +299,11 @@ impl Atomics {
// b. If ByteListEqual(rawBytesRead, expectedBytes) is true, then
// i. Store the individual bytes of replacementBytes into block, starting at block[indexedPosition].
// 14. Return RawBytesToNumeric(elementType, rawBytesRead, isLittleEndian).
// SAFETY: The integer indexed object guarantees that the buffer is aligned.
// The call to `validate_atomic_access` guarantees that the index is in-bounds.
let value: TypedArrayElement = unsafe {
match typed_array_kind {
match access.kind {
TypedArrayKind::Int8 => i8::read_mut(data)
.compare_exchange(exp as i8, rep as i8, Ordering::SeqCst)
.into(),
@ -321,23 +341,6 @@ impl Atomics {
// =========== Atomics.ops start ===========
// Most of the operations here follow the same list of steps:
//
// AtomicReadModifyWrite ( typedArray, index, value, op )
// <https://tc39.es/ecma262/#sec-atomicreadmodifywrite>
//
// 1. Let buffer be ? ValidateIntegerTypedArray(typedArray).
// 2. Let indexedPosition be ? ValidateAtomicAccess(typedArray, index).
// 3. If typedArray.[[ContentType]] is BigInt, let v be ? ToBigInt(value).
// 4. Otherwise, let v be 𝔽(? ToIntegerOrInfinity(value)).
// 5. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
// 6. NOTE: The above check is not redundant with the check in ValidateIntegerTypedArray because the call to ToBigInt or ToIntegerOrInfinity on the preceding lines can have arbitrary side effects, which could cause the buffer to become detached.
// 7. Let elementType be TypedArrayElementType(typedArray).
// 8. Return GetModifySetValueInBuffer(buffer, indexedPosition, elementType, v, op).
//
// However, our impementation differs significantly from this, which is why these steps are
// just here for documentation purposes.
atomic_op! {
/// [`Atomics.add ( typedArray, index, value )`][spec]
///
@ -383,28 +386,32 @@ impl Atomics {
/// [`Atomics.wait ( typedArray, index, value, timeout )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-atomics.wait
// TODO: rewrite this to support Atomics.waitAsync
fn wait(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let array = args.get_or_undefined(0);
let index = args.get_or_undefined(1);
let value = args.get_or_undefined(2);
let timeout = args.get_or_undefined(3);
// 1. Let buffer be ? ValidateIntegerTypedArray(typedArray, true).
let ii = validate_integer_typed_array(array, true)?;
let buffer = ii.viewed_array_buffer().as_buffer();
// 1. Let taRecord be ? ValidateIntegerTypedArray(typedArray, true).
let (ta, buf_len) = validate_integer_typed_array(array, true)?;
// 2. Let buffer be taRecord.[[Object]].[[ViewedArrayBuffer]].
// 2. If IsSharedArrayBuffer(buffer) is false, throw a TypeError exception.
let BufferRef::SharedBuffer(buffer) = buffer else {
let buffer = match ta.borrow().data.viewed_array_buffer() {
BufferObject::SharedBuffer(buf) => buf.clone(),
BufferObject::Buffer(_) => {
return Err(JsNativeError::typ()
.with_message("cannot use `ArrayBuffer` for an atomic wait")
.into());
.into())
}
};
// 3. Let indexedPosition be ? ValidateAtomicAccess(typedArray, index).
let offset = validate_atomic_access(&ii, index, context)?;
let access = validate_atomic_access(&ta, buf_len, index, context)?;
// spec expects the evaluation of this first, then the timeout.
let value = if ii.kind() == TypedArrayKind::BigInt64 {
let value = if access.kind == TypedArrayKind::BigInt64 {
// 4. If typedArray.[[TypedArrayName]] is "BigInt64Array", let v be ? ToBigInt64(value).
value.to_big_int64(context)?
} else {
@ -435,11 +442,23 @@ impl Atomics {
// SAFETY: the validity of `addr` is verified by our call to `validate_atomic_access`.
let result = unsafe {
if ii.kind() == TypedArrayKind::BigInt64 {
futex::wait(&buffer, offset, value, timeout)?
if access.kind == TypedArrayKind::BigInt64 {
futex::wait(
&buffer.borrow().data,
buf_len,
access.byte_offset,
value,
timeout,
)?
} else {
// value must fit into `i32` since it came from an `i32` above.
futex::wait(&buffer, offset, value as i32, timeout)?
futex::wait(
&buffer.borrow().data,
buf_len,
access.byte_offset,
value as i32,
timeout,
)?
}
};
@ -460,8 +479,8 @@ impl Atomics {
let count = args.get_or_undefined(2);
// 1. Let indexedPosition be ? ValidateAtomicAccessOnIntegerTypedArray(typedArray, index, true).
let ii = validate_integer_typed_array(array, true)?;
let offset = validate_atomic_access(&ii, index, context)?;
let (ta, buf_len) = validate_integer_typed_array(array, true)?;
let access = validate_atomic_access(&ta, buf_len, index, context)?;
// 2. If count is undefined, then
let count = if count.is_undefined() {
@ -481,11 +500,12 @@ impl Atomics {
// 4. Let buffer be typedArray.[[ViewedArrayBuffer]].
// 5. Let block be buffer.[[ArrayBufferData]].
// 6. If IsSharedArrayBuffer(buffer) is false, return +0𝔽.
let BufferRef::SharedBuffer(shared) = ii.viewed_array_buffer().as_buffer() else {
let ta = ta.borrow();
let BufferRef::SharedBuffer(shared) = ta.data.viewed_array_buffer().as_buffer() else {
return Ok(0.into());
};
let count = futex::notify(&shared, offset, count)?;
let count = futex::notify(&shared, access.byte_offset, count)?;
// 12. Let n be the number of elements in S.
// 13. Return 𝔽(n).
@ -493,41 +513,33 @@ impl Atomics {
}
}
/// [`ValidateIntegerTypedArray ( typedArray [ , waitable ] )`][spec]
/// [`ValidateIntegerTypedArray ( typedArray, waitable )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-validateintegertypedarray
fn validate_integer_typed_array(
array: &JsValue,
waitable: bool,
) -> JsResult<GcRef<'_, TypedArray>> {
// 1. If waitable is not present, set waitable to false.
// 2. Perform ? ValidateTypedArray(typedArray).
let ii = array
.as_object()
.and_then(JsObject::downcast_ref::<TypedArray>)
.ok_or_else(|| JsNativeError::typ().with_message("value is not a typed array object"))?;
if ii.is_detached() {
return Err(JsNativeError::typ()
.with_message("Buffer of the typed array is detached")
.into());
}
) -> JsResult<(JsObject<TypedArray>, usize)> {
// 1. Let taRecord be ? ValidateTypedArray(typedArray, unordered).
// 2. NOTE: Bounds checking is not a synchronizing operation when typedArray's backing buffer is a growable SharedArrayBuffer.
let ta_record = TypedArray::validate(array, Ordering::Relaxed)?;
// 3. Let buffer be typedArray.[[ViewedArrayBuffer]].
{
let array = ta_record.0.borrow();
// 3. If waitable is true, then
if waitable {
// 4. If waitable is true, then
// a. If typedArray.[[TypedArrayName]] is neither "Int32Array" nor "BigInt64Array", throw a TypeError exception.
if ![TypedArrayKind::Int32, TypedArrayKind::BigInt64].contains(&ii.kind()) {
if ![TypedArrayKind::Int32, TypedArrayKind::BigInt64].contains(&array.data.kind()) {
return Err(JsNativeError::typ()
.with_message("can only atomically wait using Int32 or BigInt64 arrays")
.into());
}
} else {
// 5. Else,
// 4. Else,
// a. Let type be TypedArrayElementType(typedArray).
// b. If IsUnclampedIntegerElementType(type) is false and IsBigIntElementType(type) is
// false, throw a TypeError exception.
if !ii.kind().supports_atomic_ops() {
// b. If IsUnclampedIntegerElementType(type) is false and IsBigIntElementType(type) is false, throw a TypeError exception.
if !array.data.kind().supports_atomic_ops() {
return Err(JsNativeError::typ()
.with_message(
"platform doesn't support atomic operations on the provided `TypedArray`",
@ -535,21 +547,40 @@ fn validate_integer_typed_array(
.into());
}
}
}
// 5. Return taRecord.
Ok(ta_record)
}
// 6. Return buffer.
Ok(ii)
struct AtomicAccess {
byte_offset: usize,
kind: TypedArrayKind,
}
/// [`ValidateAtomicAccess ( iieoRecord, requestIndex )`][spec]
/// [`ValidateAtomicAccess ( taRecord, requestIndex )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-validateatomicaccess
fn validate_atomic_access(
array: &TypedArray,
array: &JsObject<TypedArray>,
buf_len: usize,
request_index: &JsValue,
context: &mut Context,
) -> JsResult<usize> {
) -> JsResult<AtomicAccess> {
// 5. Let typedArray be taRecord.[[Object]].
let (length, kind, offset) = {
let array = array.borrow();
let array = &array.data;
// 1. Let length be typedArray.[[ArrayLength]].
let length = array.array_length();
// 6. Let elementSize be TypedArrayElementSize(typedArray).
// 7. Let offset be typedArray.[[ByteOffset]].
(
array.array_length(buf_len),
array.kind(),
array.byte_offset(),
)
};
// 2. Let accessIndex be ? ToIndex(requestIndex).
let access_index = request_index.to_index(context)?;
@ -564,12 +595,10 @@ fn validate_atomic_access(
.into());
}
// 5. Let elementSize be TypedArrayElementSize(typedArray).
let element_size = array.kind().element_size();
// 6. Let offset be typedArray.[[ByteOffset]].
let offset = array.byte_offset();
// 7. Return (accessIndex × elementSize) + offset.
Ok(((access_index * element_size) + offset) as usize)
// 8. Return (accessIndex × elementSize) + offset.
let offset = ((access_index * kind.element_size()) + offset) as usize;
Ok(AtomicAccess {
byte_offset: offset,
kind,
})
}

322
core/engine/src/builtins/dataview/mod.rs

@ -7,7 +7,7 @@
//! [spec]: https://tc39.es/ecma262/#sec-dataview-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
use std::mem;
use std::{mem, sync::atomic::Ordering};
use crate::{
builtins::BuiltInObject,
@ -27,7 +27,7 @@ use bytemuck::{bytes_of, bytes_of_mut};
use super::{
array_buffer::{
utils::{memcpy, SliceRef, SliceRefMut},
utils::{memcpy, BytesConstPtr, BytesMutPtr},
BufferObject,
},
typed_array::{self, TypedArrayElement},
@ -38,10 +38,61 @@ use super::{
#[derive(Debug, Clone, Trace, Finalize, JsData)]
pub struct DataView {
pub(crate) viewed_array_buffer: BufferObject,
pub(crate) byte_length: u64,
pub(crate) byte_length: Option<u64>,
pub(crate) byte_offset: u64,
}
impl DataView {
/// Abstract operation [`GetViewByteLength ( viewRecord )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-getviewbytelength
fn byte_length(&self, buf_byte_len: usize) -> u64 {
// 1. Assert: IsViewOutOfBounds(viewRecord) is false.
debug_assert!(!self.is_out_of_bounds(buf_byte_len));
// 2. Let view be viewRecord.[[Object]].
// 3. If view.[[ByteLength]] is not auto, return view.[[ByteLength]].
if let Some(byte_length) = self.byte_length {
return byte_length;
}
// 4. Assert: IsFixedLengthArrayBuffer(view.[[ViewedArrayBuffer]]) is false.
// 5. Let byteOffset be view.[[ByteOffset]].
// 6. Let byteLength be viewRecord.[[CachedBufferByteLength]].
// 7. Assert: byteLength is not detached.
// 8. Return byteLength - byteOffset.
buf_byte_len as u64 - self.byte_offset
}
/// Abstract operation [`IsViewOutOfBounds ( viewRecord )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-isviewoutofbounds
fn is_out_of_bounds(&self, buf_byte_len: usize) -> bool {
let buf_byte_len = buf_byte_len as u64;
// 1. Let view be viewRecord.[[Object]].
// 2. Let bufferByteLength be viewRecord.[[CachedBufferByteLength]].
// 3. Assert: IsDetachedBuffer(view.[[ViewedArrayBuffer]]) is true if and only if bufferByteLength is detached.
// 4. If bufferByteLength is detached, return true.
// handled by the caller
// 5. Let byteOffsetStart be view.[[ByteOffset]].
// 6. If view.[[ByteLength]] is auto, then
// a. Let byteOffsetEnd be bufferByteLength.
// 7. Else,
// a. Let byteOffsetEnd be byteOffsetStart + view.[[ByteLength]].
let byte_offset_end = self
.byte_length
.map_or(buf_byte_len, |byte_length| byte_length + self.byte_offset);
// 8. If byteOffsetStart > bufferByteLength or byteOffsetEnd > bufferByteLength, return true.
// 9. NOTE: 0-length DataViews are not considered out-of-bounds.
// 10. Return false.
self.byte_offset > buf_byte_len || byte_offset_end > buf_byte_len
}
}
impl IntrinsicObject for DataView {
fn init(realm: &Realm) {
let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE;
@ -120,7 +171,7 @@ impl BuiltInConstructor for DataView {
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::data_view;
/// `25.3.2.1 DataView ( buffer [ , byteOffset [ , byteLength ] ] )`
/// `DataView ( buffer [ , byteOffset [ , byteLength ] ] )`
///
/// The `DataView` view provides a low-level interface for reading and writing multiple number
/// types in a binary `ArrayBuffer`, without having to care about the platform's endianness.
@ -136,93 +187,124 @@ impl BuiltInConstructor for DataView {
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let byte_length = args.get_or_undefined(2);
// 1. If NewTarget is undefined, throw a TypeError exception.
if new_target.is_undefined() {
return Err(JsNativeError::typ()
.with_message("new target is undefined")
.with_message("cannot call `DataView` constructor without `new`")
.into());
}
let byte_len = args.get_or_undefined(2);
// 2. Perform ? RequireInternalSlot(buffer, [[ArrayBufferData]]).
let buffer_obj = args
let buffer = args
.get_or_undefined(0)
.as_object()
.and_then(|o| o.clone().into_buffer_object().ok())
.ok_or_else(|| JsNativeError::typ().with_message("buffer must be an ArrayBuffer"))?;
let (offset, view_byte_length) = {
let buffer = buffer_obj.as_buffer();
// 3. Let offset be ? ToIndex(byteOffset).
let offset = args.get_or_undefined(1).to_index(context)?;
let (buf_byte_len, is_fixed_len) = {
let buffer = buffer.as_buffer();
// 4. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
let Some(buffer) = buffer.data() else {
let Some(slice) = buffer.bytes(Ordering::SeqCst) else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer is detached")
.into());
};
// 5. Let bufferByteLength be buffer.[[ArrayBufferByteLength]].
let buffer_byte_length = buffer.len() as u64;
// 5. Let bufferByteLength be ArrayBufferByteLength(buffer, seq-cst).
let buf_len = slice.len() as u64;
// 6. If offset > bufferByteLength, throw a RangeError exception.
if offset > buffer_byte_length {
if offset > buf_len {
return Err(JsNativeError::range()
.with_message("Start offset is outside the bounds of the buffer")
.into());
}
// 7. If byteLength is undefined, then
let view_byte_length = if byte_length.is_undefined() {
// a. Let viewByteLength be bufferByteLength - offset.
buffer_byte_length - offset
// 7. Let bufferIsFixedLength be IsFixedLengthArrayBuffer(buffer).
(buf_len, buffer.is_fixed_len())
};
// 8. If byteLength is undefined, then
let view_byte_len = if byte_len.is_undefined() {
// a. If bufferIsFixedLength is true, then
// i. Let viewByteLength be bufferByteLength - offset.
// b. Else,
// i. Let viewByteLength be auto.
is_fixed_len.then_some(buf_byte_len - offset)
} else {
// 8.a. Let viewByteLength be ? ToIndex(byteLength).
let view_byte_length = byte_length.to_index(context)?;
// 8.b. If offset + viewByteLength > bufferByteLength, throw a RangeError exception.
if offset + view_byte_length > buffer_byte_length {
// 9. Else,
// a. Let viewByteLength be ? ToIndex(byteLength).
let byte_len = byte_len.to_index(context)?;
// b. If offset + viewByteLength > bufferByteLength, throw a RangeError exception.
if offset + byte_len > buf_byte_len {
return Err(JsNativeError::range()
.with_message("Invalid data view length")
.into());
}
view_byte_length
};
(offset, view_byte_length)
Some(byte_len)
};
// 9. Let O be ? OrdinaryCreateFromConstructor(NewTarget, "%DataView.prototype%", « [[DataView]], [[ViewedArrayBuffer]], [[ByteLength]], [[ByteOffset]] »).
// 10. Let O be ? OrdinaryCreateFromConstructor(NewTarget, "%DataView.prototype%",
// « [[DataView]], [[ViewedArrayBuffer]], [[ByteLength]], [[ByteOffset]] »).
let prototype =
get_prototype_from_constructor(new_target, StandardConstructors::data_view, context)?;
// 10. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if buffer_obj.as_buffer().is_detached() {
// 11. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
// 12. Set bufferByteLength to ArrayBufferByteLength(buffer, seq-cst).
let Some(buf_byte_len) = buffer
.as_buffer()
.bytes(Ordering::SeqCst)
.map(|s| s.len() as u64)
else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer can't be detached")
.into());
};
// 13. If offset > bufferByteLength, throw a RangeError exception.
if offset > buf_byte_len {
return Err(JsNativeError::range()
.with_message("DataView offset outside of buffer array bounds")
.into());
}
// 14. If byteLength is not undefined, then
if let Some(view_byte_len) = view_byte_len.filter(|_| !byte_len.is_undefined()) {
// a. If offset + viewByteLength > bufferByteLength, throw a RangeError exception.
if offset + view_byte_len > buf_byte_len {
return Err(JsNativeError::range()
.with_message("DataView offset outside of buffer array bounds")
.into());
}
}
let obj = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(),
prototype,
Self {
// 11. Set O.[[ViewedArrayBuffer]] to buffer.
viewed_array_buffer: buffer_obj,
// 12. Set O.[[ByteLength]] to viewByteLength.
byte_length: view_byte_length,
// 13. Set O.[[ByteOffset]] to offset.
// 15. Set O.[[ViewedArrayBuffer]] to buffer.
viewed_array_buffer: buffer,
// 16. Set O.[[ByteLength]] to viewByteLength.
byte_length: view_byte_len,
// 17. Set O.[[ByteOffset]] to offset.
byte_offset: offset,
},
);
// 14. Return O.
// 18. Return O.
Ok(obj.into())
}
}
impl DataView {
/// `25.3.4.1 get DataView.prototype.buffer`
/// `get DataView.prototype.buffer`
///
/// The buffer accessor property represents the `ArrayBuffer` or `SharedArrayBuffer` referenced
/// by the `DataView` at construction time.
@ -251,7 +333,7 @@ impl DataView {
Ok(buffer.into())
}
/// `25.3.4.1 get DataView.prototype.byteLength`
/// `get DataView.prototype.byteLength`
///
/// The `byteLength` accessor property represents the length (in bytes) of the dataview.
///
@ -268,26 +350,32 @@ impl DataView {
) -> JsResult<JsValue> {
// 1. Let O be the this value.
// 2. Perform ? RequireInternalSlot(O, [[DataView]]).
// 3. Assert: O has a [[ViewedArrayBuffer]] internal slot.
let view = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| JsNativeError::typ().with_message("`this` is not a DataView"))?;
// 3. Assert: O has a [[ViewedArrayBuffer]] internal slot.
// 4. Let buffer be O.[[ViewedArrayBuffer]].
// 4. Let viewRecord be MakeDataViewWithBufferWitnessRecord(O, seq-cst).
// 5. If IsViewOutOfBounds(viewRecord) is true, throw a TypeError exception.
let buffer = view.viewed_array_buffer.as_buffer();
// 5. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if buffer.is_detached() {
let Some(slice) = buffer
.bytes(Ordering::SeqCst)
.filter(|s| !view.is_out_of_bounds(s.len()))
else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer is detached")
.with_message("view out of bounds for its inner buffer")
.into());
}
// 6. Let size be O.[[ByteLength]].
let size = view.byte_length;
};
// 6. Let size be GetViewByteLength(viewRecord).
let size = view.byte_length(slice.len());
// 7. Return 𝔽(size).
Ok(size.into())
}
/// `25.3.4.1 get DataView.prototype.byteOffset`
/// `get DataView.prototype.byteOffset`
///
/// The `byteOffset` accessor property represents the offset (in bytes) of this view from the
/// start of its `ArrayBuffer` or `SharedArrayBuffer`.
@ -309,22 +397,28 @@ impl DataView {
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| JsNativeError::typ().with_message("`this` is not a DataView"))?;
// 3. Assert: O has a [[ViewedArrayBuffer]] internal slot.
// 4. Let buffer be O.[[ViewedArrayBuffer]].
let buffer = view.viewed_array_buffer.as_buffer();
// 5. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if buffer.is_detached() {
// 4. Let viewRecord be MakeDataViewWithBufferWitnessRecord(O, seq-cst).
// 5. If IsViewOutOfBounds(viewRecord) is true, throw a TypeError exception.
if buffer
.bytes(Ordering::SeqCst)
.filter(|b| !view.is_out_of_bounds(b.len()))
.is_none()
{
return Err(JsNativeError::typ()
.with_message("Buffer is detached")
.with_message("data view is outside the bounds of its inner buffer")
.into());
}
// 6. Let offset be O.[[ByteOffset]].
let offset = view.byte_offset;
// 7. Return 𝔽(offset).
Ok(offset.into())
}
/// `25.3.1.1 GetViewValue ( view, requestIndex, isLittleEndian, type )`
/// `GetViewValue ( view, requestIndex, isLittleEndian, type )`
///
/// The abstract operation `GetViewValue` takes arguments view, requestIndex, `isLittleEndian`,
/// and type. It is used by functions on `DataView` instances to retrieve values from the
@ -346,48 +440,56 @@ impl DataView {
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| JsNativeError::typ().with_message("`this` is not a DataView"))?;
// 3. Let getIndex be ? ToIndex(requestIndex).
let get_index = request_index.to_index(context)?;
// 4. Set isLittleEndian to ! ToBoolean(isLittleEndian).
// 4. Set isLittleEndian to ToBoolean(isLittleEndian).
let is_little_endian = is_little_endian.to_boolean();
// 5. Let buffer be view.[[ViewedArrayBuffer]].
// 6. Let viewRecord be MakeDataViewWithBufferWitnessRecord(view, unordered).
// 7. NOTE: Bounds checking is not a synchronizing operation when view's backing buffer is a growable SharedArrayBuffer.
// 8. If IsViewOutOfBounds(viewRecord) is true, throw a TypeError exception.
let buffer = view.viewed_array_buffer.as_buffer();
// 6. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
let Some(data) = buffer.data() else {
let Some(data) = buffer
.bytes(Ordering::Relaxed)
.filter(|buf| !view.is_out_of_bounds(buf.len()))
else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer is detached")
.with_message("view out of bounds for its inner buffer")
.into());
};
// 7. Let viewOffset be view.[[ByteOffset]].
// 5. Let viewOffset be view.[[ByteOffset]].
let view_offset = view.byte_offset;
// 8. Let viewSize be view.[[ByteLength]].
let view_size = view.byte_length;
// 9. Let viewSize be GetViewByteLength(viewRecord).
let view_size = view.byte_length(data.len());
// 9. Let elementSize be the Element Size value specified in Table 72 for Element Type type.
// 10. Let elementSize be the Element Size value specified in Table 71 for Element Type type.
let element_size = mem::size_of::<T>() as u64;
// 10. If getIndex + elementSize > viewSize, throw a RangeError exception.
// 11. If getIndex + elementSize > viewSize, throw a RangeError exception.
if get_index + element_size > view_size {
return Err(JsNativeError::range()
.with_message("Offset is outside the bounds of the DataView")
.into());
}
// 11. Let bufferIndex be getIndex + viewOffset.
// 12. Let bufferIndex be getIndex + viewOffset.
let buffer_index = (get_index + view_offset) as usize;
// 12. Return GetValueFromBuffer(buffer, bufferIndex, type, false, Unordered, isLittleEndian).
let src = data.subslice(buffer_index..);
debug_assert!(src.len() >= mem::size_of::<T>());
// 13. Return GetValueFromBuffer(view.[[ViewedArrayBuffer]], bufferIndex, type, false, unordered, isLittleEndian).
// SAFETY: All previous checks ensure the element fits in the buffer.
let value: TypedArrayElement = unsafe {
let mut value = T::zeroed();
memcpy(
data.subslice(buffer_index..),
SliceRefMut::Slice(bytes_of_mut(&mut value)),
src.as_ptr(),
BytesMutPtr::Bytes(bytes_of_mut(&mut value).as_mut_ptr()),
mem::size_of::<T>(),
);
@ -402,7 +504,7 @@ impl DataView {
Ok(value.into())
}
/// `25.3.4.5 DataView.prototype.getBigInt64 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getBigInt64 ( byteOffset [ , littleEndian ] )`
///
/// The `getBigInt64()` method gets a signed 64-bit integer (long long) at the specified byte
/// offset from the start of the `DataView`.
@ -425,7 +527,7 @@ impl DataView {
Self::get_view_value::<i64>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.6 DataView.prototype.getBigUint64 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getBigUint64 ( byteOffset [ , littleEndian ] )`
///
/// The `getBigUint64()` method gets an unsigned 64-bit integer (unsigned long long) at the
/// specified byte offset from the start of the `DataView`.
@ -448,7 +550,7 @@ impl DataView {
Self::get_view_value::<u64>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.7 DataView.prototype.getBigUint64 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getBigUint64 ( byteOffset [ , littleEndian ] )`
///
/// The `getFloat32()` method gets a signed 32-bit float (float) at the specified byte offset
/// from the start of the `DataView`.
@ -471,7 +573,7 @@ impl DataView {
Self::get_view_value::<f32>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.8 DataView.prototype.getFloat64 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getFloat64 ( byteOffset [ , littleEndian ] )`
///
/// The `getFloat64()` method gets a signed 64-bit float (double) at the specified byte offset
/// from the start of the `DataView`.
@ -494,7 +596,7 @@ impl DataView {
Self::get_view_value::<f64>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.9 DataView.prototype.getInt8 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getInt8 ( byteOffset [ , littleEndian ] )`
///
/// The `getInt8()` method gets a signed 8-bit integer (byte) at the specified byte offset
/// from the start of the `DataView`.
@ -517,7 +619,7 @@ impl DataView {
Self::get_view_value::<i8>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.10 DataView.prototype.getInt16 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getInt16 ( byteOffset [ , littleEndian ] )`
///
/// The `getInt16()` method gets a signed 16-bit integer (short) at the specified byte offset
/// from the start of the `DataView`.
@ -540,7 +642,7 @@ impl DataView {
Self::get_view_value::<i16>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.11 DataView.prototype.getInt32 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getInt32 ( byteOffset [ , littleEndian ] )`
///
/// The `getInt32()` method gets a signed 32-bit integer (long) at the specified byte offset
/// from the start of the `DataView`.
@ -563,7 +665,7 @@ impl DataView {
Self::get_view_value::<i32>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.12 DataView.prototype.getUint8 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getUint8 ( byteOffset [ , littleEndian ] )`
///
/// The `getUint8()` method gets an unsigned 8-bit integer (unsigned byte) at the specified
/// byte offset from the start of the `DataView`.
@ -586,7 +688,7 @@ impl DataView {
Self::get_view_value::<u8>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.13 DataView.prototype.getUint16 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getUint16 ( byteOffset [ , littleEndian ] )`
///
/// The `getUint16()` method gets an unsigned 16-bit integer (unsigned short) at the specified
/// byte offset from the start of the `DataView`.
@ -609,7 +711,7 @@ impl DataView {
Self::get_view_value::<u16>(this, byte_offset, is_little_endian, context)
}
/// `25.3.4.14 DataView.prototype.getUint32 ( byteOffset [ , littleEndian ] )`
/// `DataView.prototype.getUint32 ( byteOffset [ , littleEndian ] )`
///
/// The `getUint32()` method gets an unsigned 32-bit integer (unsigned long) at the specified
/// byte offset from the start of the `DataView`.
@ -632,7 +734,7 @@ impl DataView {
Self::get_view_value::<u32>(this, byte_offset, is_little_endian, context)
}
/// `25.3.1.1 SetViewValue ( view, requestIndex, isLittleEndian, type )`
/// `SetViewValue ( view, requestIndex, isLittleEndian, type )`
///
/// The abstract operation `SetViewValue` takes arguments view, requestIndex, `isLittleEndian`,
/// type, and value. It is used by functions on `DataView` instances to store values into the
@ -655,44 +757,55 @@ impl DataView {
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| JsNativeError::typ().with_message("`this` is not a DataView"))?;
// 3. Let getIndex be ? ToIndex(requestIndex).
let get_index = request_index.to_index(context)?;
// 4. If ! IsBigIntElementType(type) is true, let numberValue be ? ToBigInt(value).
// 4. If IsBigIntElementType(type) is true, let numberValue be ? ToBigInt(value).
// 5. Otherwise, let numberValue be ? ToNumber(value).
let value = T::from_js_value(value, context)?;
// 6. Set isLittleEndian to ! ToBoolean(isLittleEndian).
// 6. Set isLittleEndian to ToBoolean(isLittleEndian).
let is_little_endian = is_little_endian.to_boolean();
// 7. Let buffer be view.[[ViewedArrayBuffer]].
// 8. Let viewRecord be MakeDataViewWithBufferWitnessRecord(view, unordered).
// 9. NOTE: Bounds checking is not a synchronizing operation when view's backing buffer is a growable SharedArrayBuffer.
// 10. If IsViewOutOfBounds(viewRecord) is true, throw a TypeError exception.
let mut buffer = view.viewed_array_buffer.as_buffer_mut();
// 8. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
let Some(mut data) = buffer.data_mut() else {
let Some(mut data) = buffer
.bytes(Ordering::Relaxed)
.filter(|buf| !view.is_out_of_bounds(buf.len()))
else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer is detached")
.with_message("view out of bounds for its inner buffer")
.into());
};
// 9. Let viewOffset be view.[[ByteOffset]].
// 11. Let viewSize be GetViewByteLength(viewRecord).
let view_size = view.byte_length(data.len());
// 7. Let viewOffset be view.[[ByteOffset]].
let view_offset = view.byte_offset;
// 10. Let viewSize be view.[[ByteLength]].
let view_size = view.byte_length;
// 12. Let elementSize be the Element Size value specified in Table 71 for Element Type type.
let elem_size = mem::size_of::<T>();
// 11. Let elementSize be the Element Size value specified in Table 72 for Element Type type.
// 12. If getIndex + elementSize > viewSize, throw a RangeError exception.
if get_index + mem::size_of::<T>() as u64 > view_size {
// 13. If getIndex + elementSize > viewSize, throw a RangeError exception.
if get_index + elem_size as u64 > view_size {
return Err(JsNativeError::range()
.with_message("Offset is outside the bounds of DataView")
.into());
}
// 13. Let bufferIndex be getIndex + viewOffset.
// 14. Let bufferIndex be getIndex + viewOffset.
let buffer_index = (get_index + view_offset) as usize;
// 14. Return SetValueInBuffer(buffer, bufferIndex, type, numberValue, false, Unordered, isLittleEndian).
let mut target = data.subslice_mut(buffer_index..);
debug_assert!(target.len() >= mem::size_of::<T>());
// 15. Perform SetValueInBuffer(view.[[ViewedArrayBuffer]], bufferIndex, type, numberValue, false, unordered, isLittleEndian).
// SAFETY: All previous checks ensure the element fits in the buffer.
unsafe {
let value = if is_little_endian {
@ -702,16 +815,17 @@ impl DataView {
};
memcpy(
SliceRef::Slice(bytes_of(&value)),
data.subslice_mut(buffer_index..),
BytesConstPtr::Bytes(bytes_of(&value).as_ptr()),
target.as_ptr(),
mem::size_of::<T>(),
);
}
// 16. Return undefined.
Ok(JsValue::undefined())
}
/// `25.3.4.15 DataView.prototype.setBigInt64 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setBigInt64 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setBigInt64()` method stores a signed 64-bit integer (long long) value at the
/// specified byte offset from the start of the `DataView`.
@ -735,7 +849,7 @@ impl DataView {
Self::set_view_value::<i64>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.16 DataView.prototype.setBigUint64 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setBigUint64 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setBigUint64()` method stores an unsigned 64-bit integer (unsigned long long) value at
/// the specified byte offset from the start of the `DataView`.
@ -759,7 +873,7 @@ impl DataView {
Self::set_view_value::<u64>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.17 DataView.prototype.setFloat32 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setFloat32 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setFloat32()` method stores a signed 32-bit float (float) value at the specified byte
/// offset from the start of the `DataView`.
@ -783,7 +897,7 @@ impl DataView {
Self::set_view_value::<f32>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.18 DataView.prototype.setFloat64 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setFloat64 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setFloat64()` method stores a signed 64-bit float (double) value at the specified byte
/// offset from the start of the `DataView`.
@ -807,7 +921,7 @@ impl DataView {
Self::set_view_value::<f64>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.19 DataView.prototype.setInt8 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setInt8 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setInt8()` method stores a signed 8-bit integer (byte) value at the specified byte
/// offset from the start of the `DataView`.
@ -831,7 +945,7 @@ impl DataView {
Self::set_view_value::<i8>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.20 DataView.prototype.setInt16 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setInt16 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setInt16()` method stores a signed 16-bit integer (short) value at the specified byte
/// offset from the start of the `DataView`.
@ -855,7 +969,7 @@ impl DataView {
Self::set_view_value::<i16>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.21 DataView.prototype.setInt32 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setInt32 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setInt32()` method stores a signed 32-bit integer (long) value at the specified byte
/// offset from the start of the `DataView`.
@ -879,7 +993,7 @@ impl DataView {
Self::set_view_value::<i32>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.22 DataView.prototype.setUint8 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setUint8 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setUint8()` method stores an unsigned 8-bit integer (byte) value at the specified byte
/// offset from the start of the `DataView`.
@ -903,7 +1017,7 @@ impl DataView {
Self::set_view_value::<u8>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.23 DataView.prototype.setUint16 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setUint16 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setUint16()` method stores an unsigned 16-bit integer (unsigned short) value at the
/// specified byte offset from the start of the `DataView`.
@ -927,7 +1041,7 @@ impl DataView {
Self::set_view_value::<u16>(this, byte_offset, is_little_endian, value, context)
}
/// `25.3.4.24 DataView.prototype.setUint32 ( byteOffset, value [ , littleEndian ] )`
/// `DataView.prototype.setUint32 ( byteOffset, value [ , littleEndian ] )`
///
/// The `setUint32()` method stores an unsigned 32-bit integer (unsigned long) value at the
/// specified byte offset from the start of the `DataView`.

1962
core/engine/src/builtins/typed_array/builtin.rs

File diff suppressed because it is too large Load Diff

4
core/engine/src/builtins/typed_array/mod.rs

@ -511,11 +511,11 @@ pub(crate) enum TypedArrayElement {
}
impl TypedArrayElement {
/// Converts the element into its extended bytes representation as a `u64`.
/// Converts the element into its extended bytes representation as an `u64`.
///
/// This is guaranteed to never fail, since all numeric types supported by JS are less than
/// 8 bytes long.
pub(crate) fn to_bytes(self) -> u64 {
pub(crate) fn to_bits(self) -> u64 {
#[allow(clippy::cast_lossless)]
match self {
TypedArrayElement::Int8(num) => num as u64,

303
core/engine/src/builtins/typed_array/object.rs

@ -1,6 +1,6 @@
//! This module implements the `TypedArray` exotic object.
use std::sync::atomic;
use std::sync::atomic::{self, Ordering};
use crate::{
builtins::{array_buffer::BufferObject, Number},
@ -13,7 +13,7 @@ use crate::{
JsData, JsObject,
},
property::{PropertyDescriptor, PropertyKey},
Context, JsResult, JsString, JsValue,
Context, JsNativeError, JsResult, JsString, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_macros::utf16;
@ -32,8 +32,8 @@ pub struct TypedArray {
viewed_array_buffer: BufferObject,
kind: TypedArrayKind,
byte_offset: u64,
byte_length: u64,
array_length: u64,
byte_length: Option<u64>,
array_length: Option<u64>,
}
impl JsData for TypedArray {
@ -58,8 +58,8 @@ impl TypedArray {
viewed_array_buffer: BufferObject,
kind: TypedArrayKind,
byte_offset: u64,
byte_length: u64,
array_length: u64,
byte_length: Option<u64>,
array_length: Option<u64>,
) -> Self {
Self {
viewed_array_buffer,
@ -70,16 +70,42 @@ impl TypedArray {
}
}
/// Abstract operation `IsDetachedBuffer ( arrayBuffer )`.
///
/// Check if `[[ArrayBufferData]]` is null.
///
/// More information:
/// - [ECMAScript reference][spec]
/// Returns `true` if the typed array has an automatic array length.
pub(crate) fn is_auto_length(&self) -> bool {
self.array_length.is_none()
}
/// Abstract operation [`IsTypedArrayOutOfBounds ( taRecord )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-isdetachedbuffer
pub(crate) fn is_detached(&self) -> bool {
self.viewed_array_buffer.as_buffer().is_detached()
/// [spec]: https://tc39.es/ecma262/sec-istypedarrayoutofbounds
pub(crate) fn is_out_of_bounds(&self, buf_byte_len: usize) -> bool {
// Checks when allocating the buffer ensure the length fits inside an `u64`.
let buf_byte_len = buf_byte_len as u64;
// 1. Let O be taRecord.[[Object]].
// 2. Let bufferByteLength be taRecord.[[CachedBufferByteLength]].
// 3. Assert: IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is true if and only if bufferByteLength is detached.
// 4. If bufferByteLength is detached, return true.
// Handled by the caller
// 5. Let byteOffsetStart be O.[[ByteOffset]].
let byte_start = self.byte_offset;
// 6. If O.[[ArrayLength]] is auto, then
// a. Let byteOffsetEnd be bufferByteLength.
let byte_end = self.array_length.map_or(buf_byte_len, |arr_len| {
// 7. Else,
// a. Let elementSize be TypedArrayElementSize(O).
let element_size = self.kind.element_size();
// b. Let byteOffsetEnd be byteOffsetStart + O.[[ArrayLength]] × elementSize.
byte_start + arr_len * element_size
});
// 8. If byteOffsetStart > bufferByteLength or byteOffsetEnd > bufferByteLength, return true.
// 9. NOTE: 0-length TypedArrays are not considered out-of-bounds.
// 10. Return false.
byte_start > buf_byte_len || byte_end > buf_byte_len
}
/// Get the `TypedArray` object's byte offset.
@ -99,16 +125,133 @@ impl TypedArray {
&self.viewed_array_buffer
}
/// [`TypedArrayByteLength ( taRecord )`][spec].
///
/// Get the `TypedArray` object's byte length.
///
/// [spec]: https://tc39.es/ecma262/#sec-typedarraybytelength
#[must_use]
pub const fn byte_length(&self) -> u64 {
self.byte_length
pub fn byte_length(&self, buf_byte_len: usize) -> u64 {
// 1. If IsTypedArrayOutOfBounds(taRecord) is true, return 0.
if self.is_out_of_bounds(buf_byte_len) {
return 0;
}
// 2. Let length be TypedArrayLength(taRecord).
let length = self.array_length(buf_byte_len);
// 3. If length = 0, return 0.
if length == 0 {
return 0;
}
// 4. Let O be taRecord.[[Object]].
// 5. If O.[[ByteLength]] is not auto, return O.[[ByteLength]].
if let Some(byte_length) = self.byte_length {
return byte_length;
}
// 6. Let elementSize be TypedArrayElementSize(O).
let elem_size = self.kind.element_size();
// 7. Return length × elementSize.
// Should not overflow thanks to the checks at creation time.
length * elem_size
}
/// [`TypedArrayLength ( taRecord )`][spec].
///
/// Get the `TypedArray` object's array length.
///
/// [spec]: https://tc39.es/ecma262/#sec-typedarraylength
#[must_use]
pub const fn array_length(&self) -> u64 {
self.array_length
pub fn array_length(&self, buf_byte_len: usize) -> u64 {
// 1. Assert: IsTypedArrayOutOfBounds(taRecord) is false.
debug_assert!(!self.is_out_of_bounds(buf_byte_len));
let buf_byte_len = buf_byte_len as u64;
// 2. Let O be taRecord.[[Object]].
// 3. If O.[[ArrayLength]] is not auto, return O.[[ArrayLength]].
if let Some(array_length) = self.array_length {
return array_length;
}
// 4. Assert: IsFixedLengthArrayBuffer(O.[[ViewedArrayBuffer]]) is false.
// 5. Let byteOffset be O.[[ByteOffset]].
let byte_offset = self.byte_offset;
// 6. Let elementSize be TypedArrayElementSize(O).
let elem_size = self.kind.element_size();
// 7. Let byteLength be taRecord.[[CachedBufferByteLength]].
// 8. Assert: byteLength is not detached.
// 9. Return floor((byteLength - byteOffset) / elementSize).
(buf_byte_len - byte_offset) / elem_size
}
/// Abstract operation [`ValidateTypedArray ( O, order )`][spec].
///
/// [spec]: https://tc39.es/ecma262/sec-validatetypedarray
pub(crate) fn validate(this: &JsValue, order: Ordering) -> JsResult<(JsObject<Self>, usize)> {
// 1. Perform ? RequireInternalSlot(O, [[TypedArrayName]]).
let obj = this
.as_object()
.and_then(|o| o.clone().downcast::<Self>().ok())
.ok_or_else(|| {
JsNativeError::typ().with_message("`this` is not a typed array object")
})?;
let len = {
let array = obj.borrow();
let buffer = array.data.viewed_array_buffer().as_buffer();
// 2. Assert: O has a [[ViewedArrayBuffer]] internal slot.
// 3. Let taRecord be MakeTypedArrayWithBufferWitnessRecord(O, order).
// 4. If IsTypedArrayOutOfBounds(taRecord) is true, throw a TypeError exception.
let Some(buf) = buffer
.bytes(order)
.filter(|buf| !array.data.is_out_of_bounds(buf.len()))
else {
return Err(JsNativeError::typ()
.with_message("typed array is outside the bounds of its inner buffer")
.into());
};
buf.len()
};
// 5. Return taRecord.
Ok((obj, len))
}
/// Validates `index` to be in bounds for the inner buffer of this `TypedArray`.
///
/// Note: if this is only used for bounds checking, it is recommended to use
/// the `Ordering::Relaxed` ordering to get the buffer slice.
pub(crate) fn validate_index(&self, index: f64, buf_len: usize) -> Option<u64> {
// 2. If IsIntegralNumber(index) is false, return false.
if index.is_nan() || index.is_infinite() || index.fract() != 0.0 {
return None;
}
// 3. If index is -0𝔽, return false.
if index == 0.0 && index.is_sign_negative() {
return None;
}
// 6. If IsTypedArrayOutOfBounds(taRecord) is true, return false.
if self.is_out_of_bounds(buf_len) {
return None;
}
// 7. Let length be TypedArrayLength(taRecord).
let length = self.array_length(buf_len);
// 8. If ℝ(index) < 0 or ℝ(index) ≥ length, return false.
if index < 0.0 || index >= length as f64 {
return None;
}
// 9. Return true.
Some(index as u64)
}
}
@ -393,24 +536,32 @@ pub(crate) fn typed_array_exotic_own_property_keys(
.downcast_ref::<TypedArray>()
.expect("TypedArray exotic method should only be callable from TypedArray objects");
// 1. Let keys be a new empty List.
let mut keys = if inner.is_detached() {
vec![]
} else {
// 2. If IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is false, then
// a. For each integer i starting with 0 such that i < O.[[ArrayLength]], in ascending order, do
// i. Add ! ToString(𝔽(i)) as the last element of keys.
(0..inner.array_length()).map(PropertyKey::from).collect()
// 1. Let taRecord be MakeTypedArrayWithBufferWitnessRecord(O, seq-cst).
// 2. Let keys be a new empty List.
// 3. If IsTypedArrayOutOfBounds(taRecord) is false, then
let mut keys = match inner
.viewed_array_buffer
.as_buffer()
.bytes(Ordering::SeqCst)
{
Some(buf) if !inner.is_out_of_bounds(buf.len()) => {
// a. Let length be TypedArrayLength(taRecord).
let length = inner.array_length(buf.len());
// b. For each integer i such that 0 ≤ i < length, in ascending order, do
// i. Append ! ToString(𝔽(i)) to keys.
(0..length).map(PropertyKey::from).collect()
}
_ => Vec::new(),
};
// 3. For each own property key P of O such that Type(P) is String and P is not an array index, in ascending chronological order of property creation, do
// a. Add P as the last element of keys.
//
// 4. For each own property key P of O such that Type(P) is Symbol, in ascending chronological order of property creation, do
// a. Add P as the last element of keys.
// 4. For each own property key P of O such that P is a String and P is not an integer index, in ascending chronological order of property creation, do
// a. Append P to keys.
// 5. For each own property key P of O such that P is a Symbol, in ascending chronological order of property creation, do
// a. Append P to keys.
keys.extend(obj.properties.shape.keys());
// 5. Return keys.
// 6. Return keys.
Ok(keys)
}
@ -421,40 +572,38 @@ pub(crate) fn typed_array_exotic_own_property_keys(
///
/// [spec]: https://tc39.es/ecma262/sec-typedarraygetelement
fn typed_array_get_element(obj: &JsObject, index: f64) -> Option<JsValue> {
// 1. If ! IsValidIntegerIndex(O, index) is false, return undefined.
if !is_valid_integer_index(obj, index) {
return None;
}
let inner = obj
.downcast_ref::<TypedArray>()
.expect("Must be an TypedArray object");
let buffer = inner.viewed_array_buffer();
let buffer = buffer.as_buffer();
let buffer = buffer
.data()
.expect("already checked that it's not detached");
// 1. If IsValidIntegerIndex(O, index) is false, return undefined.
let Some(buffer) = buffer.bytes(Ordering::Relaxed) else {
return None;
};
let Some(index) = inner.validate_index(index, buffer.len()) else {
return None;
};
// 2. Let offset be O.[[ByteOffset]].
let offset = inner.byte_offset();
// 3. Let arrayTypeName be the String value of O.[[TypedArrayName]].
// 6. Let elementType be the Element Type value in Table 73 for arrayTypeName.
let elem_type = inner.kind();
// 4. Let elementSize be the Element Size value specified in Table 73 for arrayTypeName.
let size = elem_type.element_size();
// 3. Let elementSize be TypedArrayElementSize(O).
let size = inner.kind.element_size();
// 5. Let indexedPosition be (ℝ(index) × elementSize) + offset.
let indexed_position = ((index as u64 * size) + offset) as usize;
// 4. Let byteIndexInBuffer be (ℝ(index) × elementSize) + offset.
let byte_index = ((index * size) + offset) as usize;
// 7. Return GetValueFromBuffer(O.[[ViewedArrayBuffer]], indexedPosition, elementType, true, Unordered).
// 5. Let elementType be TypedArrayElementType(O).
let elem_type = inner.kind();
// 6. Return GetValueFromBuffer(O.[[ViewedArrayBuffer]], byteIndexInBuffer, elementType, true, unordered).
// SAFETY: The TypedArray object guarantees that the buffer is aligned.
// The call to `is_valid_integer_index` guarantees that the index is in-bounds.
let value = unsafe {
buffer
.subslice(indexed_position..)
.subslice(byte_index..)
.get_value(elem_type, atomic::Ordering::Relaxed)
};
@ -473,49 +622,47 @@ pub(crate) fn typed_array_set_element(
value: &JsValue,
context: &mut InternalMethodContext<'_>,
) -> JsResult<()> {
let obj_borrow = obj.borrow();
let inner = obj_borrow
.downcast_ref::<TypedArray>()
.expect("TypedArray exotic method should only be callable from TypedArray objects");
let obj = obj
.clone()
.downcast::<TypedArray>()
.expect("function can only be called for typed array objects");
// b. Let arrayTypeName be the String value of O.[[TypedArrayName]].
// e. Let elementType be the Element Type value in Table 73 for arrayTypeName.
let elem_type = obj.borrow().data.kind();
// 1. If O.[[ContentType]] is BigInt, let numValue be ? ToBigInt(value).
// 2. Otherwise, let numValue be ? ToNumber(value).
let value = inner.kind().get_element(value, context)?;
let value = elem_type.get_element(value, context)?;
if !is_valid_integer_index(obj, index) {
// 3. If IsValidIntegerIndex(O, index) is true, then
let array = obj.borrow();
let mut buffer = array.data.viewed_array_buffer().as_buffer_mut();
let Some(mut buffer) = buffer.bytes(Ordering::Relaxed) else {
return Ok(());
}
};
let Some(index) = array.data.validate_index(index, buffer.len()) else {
return Ok(());
};
// 3. If ! IsValidIntegerIndex(O, index) is true, then
// a. Let offset be O.[[ByteOffset]].
let offset = inner.byte_offset();
// b. Let arrayTypeName be the String value of O.[[TypedArrayName]].
// e. Let elementType be the Element Type value in Table 73 for arrayTypeName.
let elem_type = inner.kind();
let offset = array.data.byte_offset();
// c. Let elementSize be the Element Size value specified in Table 73 for arrayTypeName.
// b. Let elementSize be TypedArrayElementSize(O).
let size = elem_type.element_size();
// d. Let indexedPosition be (ℝ(index) × elementSize) + offset.
let indexed_position = ((index as u64 * size) + offset) as usize;
let buffer = inner.viewed_array_buffer();
let mut buffer = buffer.as_buffer_mut();
let mut buffer = buffer
.data_mut()
.expect("already checked that it's not detached");
// f. Perform SetValueInBuffer(O.[[ViewedArrayBuffer]], indexedPosition, elementType, numValue, true, Unordered).
// c. Let byteIndexInBuffer be (ℝ(index) × elementSize) + offset.
let byte_index = ((index * size) + offset) as usize;
// e. Perform SetValueInBuffer(O.[[ViewedArrayBuffer]], byteIndexInBuffer, elementType, numValue, true, unordered).
// SAFETY: The TypedArray object guarantees that the buffer is aligned.
// The call to `is_valid_integer_index` guarantees that the index is in-bounds.
// The call to `validate_index` guarantees that the index is in-bounds.
unsafe {
buffer
.subslice_mut(indexed_position..)
.subslice_mut(byte_index..)
.set_value(value, atomic::Ordering::Relaxed);
}
// 4. Return NormalCompletion(undefined).
// 4. Return unused.
Ok(())
}

2
core/engine/src/context/hooks.rs

@ -210,7 +210,7 @@ pub trait HostHooks {
/// exhaust the virtual memory address space and to reduce interoperability risk.
///
///
/// [specification]: https://tc39.es/ecma262/multipage/structured-data.html#sec-resizable-arraybuffer-guidelines
/// [specification]: https://tc39.es/ecma262/#sec-resizable-arraybuffer-guidelines
fn max_buffer_size(&self, _context: &mut Context) -> u64 {
1_610_612_736 // 1.5 GiB
}

5
core/engine/src/object/builtins/jsarraybuffer.rs

@ -61,6 +61,7 @@ impl JsArrayBuffer {
.constructor()
.into(),
byte_length as u64,
None,
context,
)?;
@ -236,7 +237,7 @@ impl JsArrayBuffer {
#[inline]
#[must_use]
pub fn data(&self) -> Option<GcRef<'_, [u8]>> {
GcRef::try_map(self.inner.borrow(), |o| o.data.data())
GcRef::try_map(self.inner.borrow(), |o| o.data.bytes())
}
/// Get a mutable reference to the [`JsArrayBuffer`]'s data.
@ -269,7 +270,7 @@ impl JsArrayBuffer {
#[inline]
#[must_use]
pub fn data_mut(&self) -> Option<GcRefMut<'_, Object<ArrayBuffer>, [u8]>> {
GcRefMut::try_map(self.inner.borrow_mut(), |o| o.data.data_mut())
GcRefMut::try_map(self.inner.borrow_mut(), |o| o.data.bytes_mut())
}
}

94
core/engine/src/object/builtins/jsdataview.rs

@ -1,10 +1,7 @@
//! A Rust API wrapper for Boa's `DataView` Builtin ECMAScript Object
use crate::{
builtins::{array_buffer::BufferObject, DataView},
context::intrinsics::StandardConstructors,
object::{
internal_methods::get_prototype_from_constructor, JsArrayBuffer, JsObject, JsObjectType,
},
object::{JsArrayBuffer, JsObject, JsObjectType},
value::TryFromJs,
Context, JsNativeError, JsResult, JsValue,
};
@ -55,66 +52,101 @@ impl From<JsObject<DataView>> for JsDataView {
impl JsDataView {
/// Create a new `JsDataView` object from an existing `JsArrayBuffer`.
pub fn from_js_array_buffer(
array_buffer: JsArrayBuffer,
buffer: JsArrayBuffer,
offset: Option<u64>,
byte_length: Option<u64>,
byte_len: Option<u64>,
context: &mut Context,
) -> JsResult<Self> {
let (byte_offset, byte_length) = {
let buffer = array_buffer.borrow();
let provided_offset = offset.unwrap_or(0_u64);
let offset = offset.unwrap_or_default();
let (buf_byte_len, is_fixed_len) = {
let buffer = buffer.borrow();
let buffer = &buffer.data;
// Check if buffer is detached.
if buffer.data.is_detached() {
// 4. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
let Some(slice) = buffer.bytes() else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer is detached")
.into());
};
let array_buffer_length = buffer.data.len() as u64;
// 5. Let bufferByteLength be ArrayBufferByteLength(buffer, seq-cst).
let buf_len = slice.len() as u64;
if provided_offset > array_buffer_length {
// 6. If offset > bufferByteLength, throw a RangeError exception.
if offset > buf_len {
return Err(JsNativeError::range()
.with_message("Provided offset is outside the bounds of the buffer")
.with_message("Start offset is outside the bounds of the buffer")
.into());
}
let view_byte_length = if let Some(provided_length) = byte_length {
// Check that the provided length and offset does not exceed the bounds of the ArrayBuffer
if provided_offset + provided_length > array_buffer_length {
// 7. Let bufferIsFixedLength be IsFixedLengthArrayBuffer(buffer).
(buf_len, buffer.is_fixed_len())
};
// 8. If byteLength is undefined, then
let view_byte_len = if let Some(byte_len) = byte_len {
// 9. Else,
// a. Let viewByteLength be ? ToIndex(byteLength).
// b. If offset + viewByteLength > bufferByteLength, throw a RangeError exception.
if offset + byte_len > buf_byte_len {
return Err(JsNativeError::range()
.with_message("Invalid data view length")
.into());
}
provided_length
Some(byte_len)
} else {
array_buffer_length - provided_offset
// a. If bufferIsFixedLength is true, then
// i. Let viewByteLength be bufferByteLength - offset.
// b. Else,
// i. Let viewByteLength be auto.
is_fixed_len.then_some(buf_byte_len - offset)
};
(provided_offset, view_byte_length)
// 10. Let O be ? OrdinaryCreateFromConstructor(NewTarget, "%DataView.prototype%",
// « [[DataView]], [[ViewedArrayBuffer]], [[ByteLength]], [[ByteOffset]] »).
let prototype = context.intrinsics().constructors().data_view().prototype();
// 11. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
// 12. Set bufferByteLength to ArrayBufferByteLength(buffer, seq-cst).
let Some(buf_byte_len) = buffer.borrow().data.bytes().map(|s| s.len() as u64) else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer is detached")
.into());
};
let constructor = context
.intrinsics()
.constructors()
.data_view()
.constructor()
.into();
// 13. If offset > bufferByteLength, throw a RangeError exception.
if offset > buf_byte_len {
return Err(JsNativeError::range()
.with_message("DataView offset outside of buffer array bounds")
.into());
}
let prototype =
get_prototype_from_constructor(&constructor, StandardConstructors::data_view, context)?;
// 14. If byteLength is not undefined, then
if let Some(view_byte_len) = view_byte_len.filter(|_| byte_len.is_some()) {
// a. If offset + viewByteLength > bufferByteLength, throw a RangeError exception.
if offset + view_byte_len > buf_byte_len {
return Err(JsNativeError::range()
.with_message("DataView offset outside of buffer array bounds")
.into());
}
}
let obj = JsObject::new(
context.root_shape(),
prototype,
DataView {
viewed_array_buffer: BufferObject::Buffer(array_buffer.into()),
byte_length,
byte_offset,
// 15. Set O.[[ViewedArrayBuffer]] to buffer.
viewed_array_buffer: BufferObject::Buffer(buffer.into()),
// 16. Set O.[[ByteLength]] to viewByteLength.
byte_length: view_byte_len,
// 17. Set O.[[ByteOffset]] to offset.
byte_offset: offset,
},
);
// 18. Return O.
Ok(Self { inner: obj })
}

5
core/engine/src/object/builtins/jssharedarraybuffer.rs

@ -7,7 +7,7 @@ use crate::{
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use std::ops::Deref;
use std::{ops::Deref, sync::atomic::Ordering};
/// `JsSharedArrayBuffer` provides a wrapper for Boa's implementation of the ECMAScript `ArrayBuffer` object
#[derive(Debug, Clone, Trace, Finalize)]
@ -42,6 +42,7 @@ impl JsSharedArrayBuffer {
.constructor()
.into(),
byte_length as u64,
None,
context,
)?;
@ -83,7 +84,7 @@ impl JsSharedArrayBuffer {
#[inline]
#[must_use]
pub fn byte_length(&self) -> usize {
self.borrow().data.len()
self.borrow().data.len(Ordering::SeqCst)
}
/// Gets the raw buffer of this `JsSharedArrayBuffer`.

1
test262_config.toml

@ -9,7 +9,6 @@ features = [
"FinalizationRegistry",
"IsHTMLDDA",
"resizable-arraybuffer",
"symbols-as-weakmap-keys",
"intl-normative-optional",
"Intl.DisplayNames",

Loading…
Cancel
Save