Skip to main content

boa_engine/builtins/array_buffer/
mod.rs

1//! Boa's implementation of ECMAScript's global `ArrayBuffer` and `SharedArrayBuffer` objects
2//!
3//! More information:
4//!  - [ECMAScript reference][spec]
5//!  - [MDN documentation][mdn]
6//!
7//! [spec]: https://tc39.es/ecma262/#sec-arraybuffer-objects
8//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
9
10#![deny(unsafe_op_in_unsafe_fn)]
11#![deny(clippy::undocumented_unsafe_blocks)]
12
13pub(crate) mod shared;
14pub(crate) mod utils;
15
16#[cfg(test)]
17mod tests;
18
19use std::ops::{Deref, DerefMut};
20
21use aligned_vec::{ABox, AVec, ConstAlign};
22pub use shared::SharedArrayBuffer;
23use std::sync::atomic::Ordering;
24
25use crate::{
26    Context, JsArgs, JsData, JsResult, JsString, JsValue,
27    builtins::BuiltInObject,
28    context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
29    error::JsNativeError,
30    js_string,
31    object::{JsObject, internal_methods::get_prototype_from_constructor},
32    property::Attribute,
33    realm::Realm,
34    string::StaticJsStrings,
35    symbol::JsSymbol,
36};
37use boa_gc::{Finalize, GcRef, GcRefMut, Trace};
38
39use self::utils::{SliceRef, SliceRefMut};
40
41use super::{
42    Array, BuiltInBuilder, BuiltInConstructor, DataView, IntrinsicObject, typed_array::TypedArray,
43};
44
45/// `Vec`, but aligned to a 64-bit memory address.
46pub type AlignedVec<T> = AVec<T, ConstAlign<64>>;
47pub(crate) type AlignedBox<T> = ABox<T, ConstAlign<64>>;
48
49#[derive(Debug, Clone, Copy)]
50pub(crate) enum BufferRef<B, S> {
51    Buffer(B),
52    SharedBuffer(S),
53}
54
55impl<B, S> BufferRef<B, S>
56where
57    B: Deref<Target = ArrayBuffer>,
58    S: Deref<Target = SharedArrayBuffer>,
59{
60    /// Gets the inner data of the buffer.
61    pub(crate) fn bytes(&self, ordering: Ordering) -> Option<SliceRef<'_>> {
62        match self {
63            Self::Buffer(buf) => buf.deref().bytes().map(SliceRef::Slice),
64            Self::SharedBuffer(buf) => Some(SliceRef::AtomicSlice(buf.deref().bytes(ordering))),
65        }
66    }
67
68    /// Gets the inner data of the buffer without accessing the current atomic length.
69    ///
70    /// Returns `None` if the buffer is detached or if the provided `len` is bigger than
71    /// the allocated buffer.
72    #[track_caller]
73    pub(crate) fn bytes_with_len(&self, len: usize) -> Option<SliceRef<'_>> {
74        match self {
75            Self::Buffer(buf) => buf.deref().bytes_with_len(len).map(SliceRef::Slice),
76            Self::SharedBuffer(buf) => Some(SliceRef::AtomicSlice(buf.deref().bytes_with_len(len))),
77        }
78    }
79
80    pub(crate) fn is_fixed_len(&self) -> bool {
81        match self {
82            Self::Buffer(buf) => buf.is_fixed_len(),
83            Self::SharedBuffer(buf) => buf.is_fixed_len(),
84        }
85    }
86}
87
88#[derive(Debug)]
89pub(crate) enum BufferRefMut<B, S> {
90    Buffer(B),
91    SharedBuffer(S),
92}
93
94impl<B, S> BufferRefMut<B, S>
95where
96    B: DerefMut<Target = ArrayBuffer>,
97    S: DerefMut<Target = SharedArrayBuffer>,
98{
99    pub(crate) fn bytes(&mut self, ordering: Ordering) -> Option<SliceRefMut<'_>> {
100        match self {
101            Self::Buffer(buf) => buf.deref_mut().bytes_mut().map(SliceRefMut::Slice),
102            Self::SharedBuffer(buf) => {
103                Some(SliceRefMut::AtomicSlice(buf.deref_mut().bytes(ordering)))
104            }
105        }
106    }
107
108    /// Gets the mutable inner data of the buffer without accessing the current atomic length.
109    ///
110    /// Returns `None` if the buffer is detached or if the provided `len` is bigger than
111    /// the allocated buffer.
112    pub(crate) fn bytes_with_len(&mut self, len: usize) -> Option<SliceRefMut<'_>> {
113        match self {
114            Self::Buffer(buf) => buf
115                .deref_mut()
116                .bytes_with_len_mut(len)
117                .map(SliceRefMut::Slice),
118            Self::SharedBuffer(buf) => Some(SliceRefMut::AtomicSlice(
119                buf.deref_mut().bytes_with_len(len),
120            )),
121        }
122    }
123}
124
125/// A `JsObject` containing a bytes buffer as its inner data.
126#[derive(Debug, Clone, Trace, Finalize)]
127#[boa_gc(unsafe_no_drop)]
128pub(crate) enum BufferObject {
129    Buffer(JsObject<ArrayBuffer>),
130    SharedBuffer(JsObject<SharedArrayBuffer>),
131}
132
133impl From<BufferObject> for JsObject {
134    fn from(value: BufferObject) -> Self {
135        match value {
136            BufferObject::Buffer(buf) => buf.upcast(),
137            BufferObject::SharedBuffer(buf) => buf.upcast(),
138        }
139    }
140}
141
142impl From<BufferObject> for JsValue {
143    fn from(value: BufferObject) -> Self {
144        JsValue::from(JsObject::from(value))
145    }
146}
147
148impl BufferObject {
149    /// Gets the buffer data of the object.
150    #[inline]
151    #[must_use]
152    pub(crate) fn as_buffer(
153        &self,
154    ) -> BufferRef<GcRef<'_, ArrayBuffer>, GcRef<'_, SharedArrayBuffer>> {
155        match self {
156            Self::Buffer(buf) => BufferRef::Buffer(GcRef::map(buf.borrow(), |o| o.data())),
157            Self::SharedBuffer(buf) => {
158                BufferRef::SharedBuffer(GcRef::map(buf.borrow(), |o| o.data()))
159            }
160        }
161    }
162
163    /// Gets the mutable buffer data of the object
164    #[inline]
165    #[track_caller]
166    pub(crate) fn as_buffer_mut(
167        &self,
168    ) -> BufferRefMut<GcRefMut<'_, ArrayBuffer>, GcRefMut<'_, SharedArrayBuffer>> {
169        match self {
170            Self::Buffer(buf) => {
171                BufferRefMut::Buffer(GcRefMut::map(buf.borrow_mut(), |o| o.data_mut()))
172            }
173            Self::SharedBuffer(buf) => {
174                BufferRefMut::SharedBuffer(GcRefMut::map(buf.borrow_mut(), |o| o.data_mut()))
175            }
176        }
177    }
178
179    /// Returns `true` if the buffer objects point to the same buffer.
180    #[inline]
181    #[track_caller]
182    pub(crate) fn equals(lhs: &Self, rhs: &Self) -> bool {
183        match (lhs, rhs) {
184            (BufferObject::Buffer(lhs), BufferObject::Buffer(rhs)) => JsObject::equals(lhs, rhs),
185            (BufferObject::SharedBuffer(lhs), BufferObject::SharedBuffer(rhs)) => {
186                if JsObject::equals(lhs, rhs) {
187                    return true;
188                }
189
190                let lhs = lhs.borrow();
191                let rhs = rhs.borrow();
192
193                std::ptr::eq(lhs.data().as_ptr(), rhs.data().as_ptr())
194            }
195            _ => false,
196        }
197    }
198}
199
200/// The internal representation of an `ArrayBuffer` object.
201#[derive(Debug, Clone, Trace, Finalize, JsData)]
202pub struct ArrayBuffer {
203    /// The `[[ArrayBufferData]]` internal slot.
204    #[unsafe_ignore_trace]
205    data: Option<AlignedVec<u8>>,
206
207    /// The `[[ArrayBufferMaxByteLength]]` internal slot.
208    max_byte_len: Option<u64>,
209
210    /// The `[[ArrayBufferDetachKey]]` internal slot.
211    detach_key: JsValue,
212}
213
214impl ArrayBuffer {
215    pub(crate) fn from_data(data: AlignedVec<u8>, detach_key: JsValue) -> Self {
216        Self {
217            data: Some(data),
218            max_byte_len: None,
219            detach_key,
220        }
221    }
222
223    pub(crate) fn len(&self) -> usize {
224        self.data.as_ref().map_or(0, AlignedVec::len)
225    }
226
227    pub(crate) fn bytes(&self) -> Option<&[u8]> {
228        self.data.as_deref()
229    }
230
231    pub(crate) fn bytes_mut(&mut self) -> Option<&mut [u8]> {
232        self.data.as_deref_mut()
233    }
234
235    pub(crate) fn vec_mut(&mut self) -> Option<&mut AlignedVec<u8>> {
236        self.data.as_mut()
237    }
238
239    /// Sets the maximum byte length of the buffer, returning the previous value if present.
240    pub(crate) fn set_max_byte_length(&mut self, max_byte_len: u64) -> Option<u64> {
241        self.max_byte_len.replace(max_byte_len)
242    }
243
244    /// Gets the inner bytes of the buffer without accessing the current atomic length.
245    #[track_caller]
246    pub(crate) fn bytes_with_len(&self, len: usize) -> Option<&[u8]> {
247        if let Some(s) = self.data.as_deref() {
248            Some(&s[..len])
249        } else {
250            None
251        }
252    }
253
254    /// Gets the mutable inner bytes of the buffer without accessing the current atomic length.
255    #[track_caller]
256    pub(crate) fn bytes_with_len_mut(&mut self, len: usize) -> Option<&mut [u8]> {
257        if let Some(s) = self.data.as_deref_mut() {
258            Some(&mut s[..len])
259        } else {
260            None
261        }
262    }
263
264    /// Gets the underlying vector for this buffer.
265    #[must_use]
266    pub fn data(&self) -> Option<&[u8]> {
267        self.data.as_deref()
268    }
269
270    /// Resizes the buffer to the new size, clamped to the maximum byte length if present.
271    pub fn resize(&mut self, new_byte_length: u64) -> JsResult<()> {
272        let Some(max_byte_len) = self.max_byte_len else {
273            return Err(JsNativeError::typ()
274                .with_message("ArrayBuffer.resize: cannot resize a fixed-length buffer")
275                .into());
276        };
277
278        let Some(buf) = self.vec_mut() else {
279            return Err(JsNativeError::typ()
280                .with_message("ArrayBuffer.resize: cannot resize a detached buffer")
281                .into());
282        };
283
284        if new_byte_length > max_byte_len {
285            return Err(JsNativeError::range()
286                .with_message(
287                    "ArrayBuffer.resize: new byte length exceeds buffer's maximum byte length",
288                )
289                .into());
290        }
291
292        buf.resize(new_byte_length as usize, 0);
293        Ok(())
294    }
295
296    /// Detaches the inner data of this `ArrayBuffer`, returning the original buffer if still
297    /// present.
298    ///
299    /// # Errors
300    ///
301    /// Throws an error if the provided detach key is invalid.
302    pub fn detach(&mut self, key: &JsValue) -> JsResult<Option<AlignedVec<u8>>> {
303        if !JsValue::same_value(&self.detach_key, key) {
304            return Err(JsNativeError::typ()
305                .with_message("Cannot detach array buffer with different key")
306                .into());
307        }
308
309        Ok(self.data.take())
310    }
311
312    /// `IsDetachedBuffer ( arrayBuffer )`
313    ///
314    /// More information:
315    ///  - [ECMAScript reference][spec]
316    ///
317    /// [spec]: https://tc39.es/ecma262/#sec-isdetachedbuffer
318    pub(crate) const fn is_detached(&self) -> bool {
319        // 1. If arrayBuffer.[[ArrayBufferData]] is null, return true.
320        // 2. Return false.
321        self.data.is_none()
322    }
323
324    pub(crate) fn is_fixed_len(&self) -> bool {
325        self.max_byte_len.is_none()
326    }
327}
328
329impl IntrinsicObject for ArrayBuffer {
330    fn init(realm: &Realm) {
331        let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE;
332
333        let get_species = BuiltInBuilder::callable(realm, Self::get_species)
334            .name(js_string!("get [Symbol.species]"))
335            .build();
336
337        let get_byte_length = BuiltInBuilder::callable(realm, Self::get_byte_length)
338            .name(js_string!("get byteLength"))
339            .build();
340
341        let get_resizable = BuiltInBuilder::callable(realm, Self::get_resizable)
342            .name(js_string!("get resizable"))
343            .build();
344
345        let get_max_byte_length = BuiltInBuilder::callable(realm, Self::get_max_byte_length)
346            .name(js_string!("get maxByteLength"))
347            .build();
348
349        #[cfg(feature = "experimental")]
350        let get_detached = BuiltInBuilder::callable(realm, Self::get_detached)
351            .name(js_string!("get detached"))
352            .build();
353
354        let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
355            .static_accessor(
356                JsSymbol::species(),
357                Some(get_species),
358                None,
359                Attribute::CONFIGURABLE,
360            )
361            .static_method(Self::is_view, js_string!("isView"), 1)
362            .accessor(
363                js_string!("byteLength"),
364                Some(get_byte_length),
365                None,
366                flag_attributes,
367            )
368            .accessor(
369                js_string!("resizable"),
370                Some(get_resizable),
371                None,
372                flag_attributes,
373            )
374            .accessor(
375                js_string!("maxByteLength"),
376                Some(get_max_byte_length),
377                None,
378                flag_attributes,
379            )
380            .method(Self::js_resize, js_string!("resize"), 1)
381            .method(Self::slice, js_string!("slice"), 2)
382            .property(
383                JsSymbol::to_string_tag(),
384                Self::NAME,
385                Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
386            );
387
388        #[cfg(feature = "experimental")]
389        let builder = builder
390            .accessor(
391                js_string!("detached"),
392                Some(get_detached),
393                None,
394                flag_attributes,
395            )
396            .method(Self::transfer::<false>, js_string!("transfer"), 0)
397            .method(
398                Self::transfer::<true>,
399                js_string!("transferToFixedLength"),
400                0,
401            );
402
403        builder.build();
404    }
405
406    fn get(intrinsics: &Intrinsics) -> JsObject {
407        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
408    }
409}
410
411impl BuiltInObject for ArrayBuffer {
412    const NAME: JsString = StaticJsStrings::ARRAY_BUFFER;
413}
414
415impl BuiltInConstructor for ArrayBuffer {
416    const PROTOTYPE_STORAGE_SLOTS: usize = 13;
417    const CONSTRUCTOR_STORAGE_SLOTS: usize = 3;
418    const CONSTRUCTOR_ARGUMENTS: usize = 1;
419
420    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
421        StandardConstructors::array_buffer;
422
423    /// `ArrayBuffer ( length )`
424    ///
425    /// More information:
426    ///  - [ECMAScript reference][spec]
427    ///
428    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer-length
429    fn constructor(
430        new_target: &JsValue,
431        args: &[JsValue],
432        context: &mut Context,
433    ) -> JsResult<JsValue> {
434        // 1. If NewTarget is undefined, throw a TypeError exception.
435        if new_target.is_undefined() {
436            return Err(JsNativeError::typ()
437                .with_message("ArrayBuffer.constructor called with undefined new target")
438                .into());
439        }
440
441        // 2. Let byteLength be ? ToIndex(length).
442        let byte_len = args.get_or_undefined(0).to_index(context)?;
443
444        // 3. Let requestedMaxByteLength be ? GetArrayBufferMaxByteLengthOption(options).
445        let max_byte_len = get_max_byte_len(args.get_or_undefined(1), context)?;
446
447        // 4. Return ? AllocateArrayBuffer(NewTarget, byteLength, requestedMaxByteLength).
448        Ok(Self::allocate(new_target, byte_len, max_byte_len, context)?
449            .upcast()
450            .into())
451    }
452}
453
454impl ArrayBuffer {
455    /// `ArrayBuffer.isView ( arg )`
456    ///
457    /// More information:
458    ///  - [ECMAScript reference][spec]
459    ///
460    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.isview
461    #[allow(clippy::unnecessary_wraps)]
462    fn is_view(_: &JsValue, args: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
463        // 1. If Type(arg) is not Object, return false.
464        // 2. If arg has a [[ViewedArrayBuffer]] internal slot, return true.
465        // 3. Return false.
466        Ok(args
467            .get_or_undefined(0)
468            .as_object()
469            .is_some_and(|obj| obj.is::<TypedArray>() || obj.is::<DataView>())
470            .into())
471    }
472
473    /// `get ArrayBuffer [ @@species ]`
474    ///
475    /// More information:
476    ///  - [ECMAScript reference][spec]
477    ///
478    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer-@@species
479    #[allow(clippy::unnecessary_wraps)]
480    fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
481        // 1. Return the this value.
482        Ok(this.clone())
483    }
484
485    /// `get ArrayBuffer.prototype.byteLength`
486    ///
487    /// More information:
488    ///  - [ECMAScript reference][spec]
489    ///
490    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.bytelength
491    pub(crate) fn get_byte_length(
492        this: &JsValue,
493        _args: &[JsValue],
494        _: &mut Context,
495    ) -> JsResult<JsValue> {
496        // 1. Let O be the this value.
497        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
498        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
499        let object = this.as_object();
500        let buf = object
501            .as_ref()
502            .and_then(JsObject::downcast_ref::<Self>)
503            .ok_or_else(|| {
504                JsNativeError::typ()
505                    .with_message("get ArrayBuffer.prototype.byteLength called with invalid `this`")
506            })?;
507
508        // 4. If IsDetachedBuffer(O) is true, return +0𝔽.
509        // 5. Let length be O.[[ArrayBufferByteLength]].
510        // 6. Return 𝔽(length).
511        Ok(buf.len().into())
512    }
513
514    /// [`get ArrayBuffer.prototype.maxByteLength`][spec].
515    ///
516    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.maxbytelength
517    pub(crate) fn get_max_byte_length(
518        this: &JsValue,
519        _args: &[JsValue],
520        _context: &mut Context,
521    ) -> JsResult<JsValue> {
522        // 1. Let O be the this value.
523        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
524        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
525        let object = this.as_object();
526        let buf = object
527            .as_ref()
528            .and_then(JsObject::downcast_ref::<Self>)
529            .ok_or_else(|| {
530                JsNativeError::typ().with_message(
531                    "get ArrayBuffer.prototype.maxByteLength called with invalid `this`",
532                )
533            })?;
534
535        // 4. If IsDetachedBuffer(O) is true, return +0𝔽.
536        let Some(data) = buf.bytes() else {
537            return Ok(JsValue::from(0));
538        };
539
540        // 5. If IsFixedLengthArrayBuffer(O) is true, then
541        //     a. Let length be O.[[ArrayBufferByteLength]].
542        // 6. Else,
543        //     a. Let length be O.[[ArrayBufferMaxByteLength]].
544        // 7. Return 𝔽(length).
545        Ok(buf.max_byte_len.unwrap_or(data.len() as u64).into())
546    }
547
548    /// [`get ArrayBuffer.prototype.resizable`][spec].
549    ///
550    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.resizable
551    pub(crate) fn get_resizable(
552        this: &JsValue,
553        _args: &[JsValue],
554        _context: &mut Context,
555    ) -> JsResult<JsValue> {
556        // 1. Let O be the this value.
557        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
558        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
559        let object = this.as_object();
560        let buf = object
561            .as_ref()
562            .and_then(JsObject::downcast_ref::<Self>)
563            .ok_or_else(|| {
564                JsNativeError::typ()
565                    .with_message("get ArrayBuffer.prototype.resizable called with invalid `this`")
566            })?;
567
568        // 4. If IsFixedLengthArrayBuffer(O) is false, return true; otherwise return false.
569        Ok(JsValue::from(!buf.is_fixed_len()))
570    }
571
572    /// [`get ArrayBuffer.prototype.detached`][spec].
573    ///
574    /// [spec]: https://tc39.es/proposal-arraybuffer-transfer/#sec-get-arraybuffer.prototype.detached
575    #[cfg(feature = "experimental")]
576    fn get_detached(
577        this: &JsValue,
578        _args: &[JsValue],
579        _context: &mut Context,
580    ) -> JsResult<JsValue> {
581        // 1. Let O be the this value.
582        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
583        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
584        let object = this.as_object();
585        let buf = object
586            .as_ref()
587            .and_then(JsObject::downcast_ref::<Self>)
588            .ok_or_else(|| {
589                JsNativeError::typ()
590                    .with_message("get ArrayBuffer.prototype.detached called with invalid `this`")
591            })?;
592
593        // 4. Return IsDetachedBuffer(O).
594        Ok(buf.is_detached().into())
595    }
596
597    /// [`ArrayBuffer.prototype.resize ( newLength )`][spec].
598    ///
599    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.prototype.resize
600    pub(crate) fn js_resize(
601        this: &JsValue,
602        args: &[JsValue],
603        context: &mut Context,
604    ) -> JsResult<JsValue> {
605        // 1. Let O be the this value.
606        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]).
607        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
608        let buf = this
609            .as_object()
610            .and_then(|o| o.clone().downcast::<Self>().ok())
611            .ok_or_else(|| {
612                JsNativeError::typ()
613                    .with_message("ArrayBuffer.prototype.resize called with invalid `this`")
614            })?;
615
616        // 4. Let newByteLength be ? ToIndex(newLength).
617        let new_byte_length = args.get_or_undefined(0).to_index(context)?;
618
619        // These steps are performed in the `Self::resize` method.
620        // 5. If IsDetachedBuffer(O) is true, throw a TypeError exception.
621        // 6. If newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception.
622
623        // TODO: 7. Let hostHandled be ? HostResizeArrayBuffer(O, newByteLength).
624        // 8. If hostHandled is handled, return undefined.
625        // Used in engines to handle Wasm buffers in a special way, but we don't
626        // have a Wasm interpreter in place yet.
627
628        // 9. Let oldBlock be O.[[ArrayBufferData]].
629        // 10. Let newBlock be ? CreateByteDataBlock(newByteLength).
630        // 11. Let copyLength be min(newByteLength, O.[[ArrayBufferByteLength]]).
631        // 12. Perform CopyDataBlockBytes(newBlock, 0, oldBlock, 0, copyLength).
632        // 13. NOTE: Neither creation of the new Data Block nor copying from the old Data Block are observable.
633        //     Implementations may implement this method as in-place growth or shrinkage.
634        // 14. Set O.[[ArrayBufferData]] to newBlock.
635        // 15. Set O.[[ArrayBufferByteLength]] to newByteLength.
636        buf.borrow_mut().data_mut().resize(new_byte_length)?;
637
638        // 16. Return undefined.
639        Ok(JsValue::undefined())
640    }
641
642    /// `ArrayBuffer.prototype.slice ( start, end )`
643    ///
644    /// More information:
645    ///  - [ECMAScript reference][spec]
646    ///
647    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.prototype.slice
648    fn slice(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
649        // 1. Let O be the this value.
650        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
651        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
652        let buf = this
653            .as_object()
654            .and_then(|o| o.clone().downcast::<Self>().ok())
655            .ok_or_else(|| {
656                JsNativeError::typ()
657                    .with_message("ArrayBuffer.slice called with invalid `this` value")
658            })?;
659
660        let len = {
661            let buf = buf.borrow();
662            // 4. If IsDetachedBuffer(O) is true, throw a TypeError exception.
663            if buf.data().is_detached() {
664                return Err(JsNativeError::typ()
665                    .with_message("ArrayBuffer.slice called with detached buffer")
666                    .into());
667            }
668            // 5. Let len be O.[[ArrayBufferByteLength]].
669            buf.data().len() as u64
670        };
671
672        // 6. Let relativeStart be ? ToIntegerOrInfinity(start).
673        // 7. If relativeStart = -∞, let first be 0.
674        // 8. Else if relativeStart < 0, let first be max(len + relativeStart, 0).
675        // 9. Else, let first be min(relativeStart, len).
676        let first = Array::get_relative_start(context, args.get_or_undefined(0), len)?;
677
678        // 10. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
679        // 11. If relativeEnd = -∞, let final be 0.
680        // 12. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
681        // 13. Else, let final be min(relativeEnd, len).
682        let final_ = Array::get_relative_end(context, args.get_or_undefined(1), len)?;
683
684        // 14. Let newLen be max(final - first, 0).
685        let new_len = final_.saturating_sub(first);
686
687        // 15. Let ctor be ? SpeciesConstructor(O, %ArrayBuffer%).
688        let ctor = buf
689            .clone()
690            .upcast()
691            .species_constructor(StandardConstructors::array_buffer, context)?;
692
693        // 16. Let new be ? Construct(ctor, « 𝔽(newLen) »).
694        let new = ctor.construct(&[new_len.into()], Some(&ctor), context)?;
695
696        // 17. Perform ? RequireInternalSlot(new, [[ArrayBufferData]]).
697        // 18. If IsSharedArrayBuffer(new) is true, throw a TypeError exception.
698        let Ok(new) = new.downcast::<Self>() else {
699            return Err(JsNativeError::typ()
700                .with_message("ArrayBuffer constructor returned invalid object")
701                .into());
702        };
703
704        // 20. If SameValue(new, O) is true, throw a TypeError exception.
705        if JsObject::equals(&buf, &new) {
706            return Err(JsNativeError::typ()
707                .with_message("new ArrayBuffer is the same as this ArrayBuffer")
708                .into());
709        }
710
711        {
712            // 19. If IsDetachedBuffer(new) is true, throw a TypeError exception.
713            // 25. Let toBuf be new.[[ArrayBufferData]].
714            let mut new = new.borrow_mut();
715            let Some(to_buf) = new.data_mut().bytes_mut() else {
716                return Err(JsNativeError::typ()
717                    .with_message("ArrayBuffer constructor returned detached ArrayBuffer")
718                    .into());
719            };
720
721            // 21. If new.[[ArrayBufferByteLength]] < newLen, throw a TypeError exception.
722            if (to_buf.len() as u64) < new_len {
723                return Err(JsNativeError::typ()
724                    .with_message("new ArrayBuffer length too small")
725                    .into());
726            }
727
728            // 22. NOTE: Side-effects of the above steps may have detached O.
729            // 23. If IsDetachedBuffer(O) is true, throw a TypeError exception.
730            // 24. Let fromBuf be O.[[ArrayBufferData]].
731            let buf = buf.borrow();
732            let Some(from_buf) = buf.data().bytes() else {
733                return Err(JsNativeError::typ()
734                    .with_message("ArrayBuffer detached while ArrayBuffer.slice was running")
735                    .into());
736            };
737
738            // 26. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen).
739            let first = first as usize;
740            let new_len = new_len as usize;
741            to_buf[..new_len].copy_from_slice(&from_buf[first..first + new_len]);
742        }
743
744        // 27. Return new.
745        Ok(new.upcast().into())
746    }
747
748    /// [`ArrayBuffer.prototype.transfer ( [ newLength ] )`][transfer] and
749    /// [`ArrayBuffer.prototype.transferToFixedLength ( [ newLength ] )`][transferFL]
750    ///
751    /// [transfer]: https://tc39.es/proposal-arraybuffer-transfer/#sec-arraybuffer.prototype.transfer
752    /// [transferFL]: https://tc39.es/proposal-arraybuffer-transfer/#sec-arraybuffer.prototype.transfertofixedlength
753    #[cfg(feature = "experimental")]
754    fn transfer<const TO_FIXED_LENGTH: bool>(
755        this: &JsValue,
756        args: &[JsValue],
757        context: &mut Context,
758    ) -> JsResult<JsValue> {
759        // 1. Let O be the this value.
760        // 2. Return ? ArrayBufferCopyAndDetach(O, newLength, preserve-resizability).
761
762        // Abstract operation `ArrayBufferCopyAndDetach ( arrayBuffer, newLength, preserveResizability )`
763        // https://tc39.es/proposal-arraybuffer-transfer/#sec-arraybuffercopyanddetach
764
765        let new_length = args.get_or_undefined(0);
766
767        // 1. Perform ? RequireInternalSlot(arrayBuffer, [[ArrayBufferData]]).
768        // 2. If IsSharedArrayBuffer(arrayBuffer) is true, throw a TypeError exception.
769        let buf = this
770            .as_object()
771            .and_then(|o| o.clone().downcast::<Self>().ok())
772            .ok_or_else(|| {
773                JsNativeError::typ().with_message(if TO_FIXED_LENGTH {
774                    "ArrayBuffer.prototype.transferToFixedLength called with invalid `this`"
775                } else {
776                    "ArrayBuffer.prototype.transfer called with invalid `this`"
777                })
778            })?;
779
780        // 3. If newLength is undefined, then
781        let new_len = if new_length.is_undefined() {
782            // a. Let newByteLength be arrayBuffer.[[ArrayBufferByteLength]].
783            buf.borrow().data().len() as u64
784        } else {
785            // 4. Else,
786            //     a. Let newByteLength be ? ToIndex(newLength).
787            new_length.to_index(context)?
788        };
789
790        // 5. If IsDetachedBuffer(arrayBuffer) is true, throw a TypeError exception.
791        let Some(mut bytes) = buf.borrow_mut().data_mut().data.take() else {
792            return Err(JsNativeError::typ()
793                .with_message("cannot transfer a detached buffer")
794                .into());
795        };
796
797        // 6. If preserveResizability is preserve-resizability and IsResizableArrayBuffer(arrayBuffer)
798        //    is true, then
799        //     a. Let newMaxByteLength be arrayBuffer.[[ArrayBufferMaxByteLength]].
800        // 7. Else,
801        //     a. Let newMaxByteLength be empty.
802        let new_max_len = buf
803            .borrow()
804            .data()
805            .max_byte_len
806            .filter(|_| !TO_FIXED_LENGTH);
807
808        // 8. If arrayBuffer.[[ArrayBufferDetachKey]] is not undefined, throw a TypeError exception.
809        if !buf.borrow().data().detach_key.is_undefined() {
810            buf.borrow_mut().data_mut().data = Some(bytes);
811            return Err(JsNativeError::typ()
812                .with_message("cannot transfer a buffer with a detach key")
813                .into());
814        }
815
816        // Effectively, the next steps only create a new object for the same vec, so we can skip all
817        // those steps and just make a single check + trigger the realloc.
818
819        // 9. Let newBuffer be ? AllocateArrayBuffer(%ArrayBuffer%, newByteLength, newMaxByteLength).
820        // 10. Let copyLength be min(newByteLength, arrayBuffer.[[ArrayBufferByteLength]]).
821        // 11. Let fromBlock be arrayBuffer.[[ArrayBufferData]].
822        // 12. Let toBlock be newBuffer.[[ArrayBufferData]].
823        // 13. Perform CopyDataBlockBytes(toBlock, 0, fromBlock, 0, copyLength).
824        // 14. NOTE: Neither creation of the new Data Block nor copying from the old Data Block are
825        //     observable. Implementations may implement this method as a zero-copy move or a realloc.
826        // 15. Perform ! DetachArrayBuffer(arrayBuffer).
827        // 16. Return newBuffer.
828        if let Some(new_max_len) = new_max_len {
829            if new_len > new_max_len {
830                buf.borrow_mut().data_mut().data = Some(bytes);
831                return Err(JsNativeError::range()
832                    .with_message("`length` cannot be bigger than `maxByteLength`")
833                    .into());
834            }
835            // Should only truncate without reallocating.
836            bytes.resize(new_len as usize, 0);
837        } else {
838            bytes.resize(new_len as usize, 0);
839
840            // Realloc the vec to fit onto the new exact length.
841            bytes.shrink_to_fit();
842        }
843
844        let prototype = context
845            .intrinsics()
846            .constructors()
847            .array_buffer()
848            .prototype();
849
850        Ok(JsObject::from_proto_and_data_with_shared_shape(
851            context.root_shape(),
852            prototype,
853            ArrayBuffer {
854                data: Some(bytes),
855                max_byte_len: new_max_len,
856                detach_key: JsValue::undefined(),
857            },
858        )
859        .into())
860    }
861
862    /// `AllocateArrayBuffer ( constructor, byteLength )`
863    ///
864    /// More information:
865    ///  - [ECMAScript reference][spec]
866    ///
867    /// [spec]: https://tc39.es/ecma262/#sec-allocatearraybuffer
868    pub(crate) fn allocate(
869        constructor: &JsValue,
870        byte_len: u64,
871        max_byte_len: Option<u64>,
872        context: &mut Context,
873    ) -> JsResult<JsObject<ArrayBuffer>> {
874        // 1. Let slots be « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] ».
875        // 2. If maxByteLength is present and maxByteLength is not empty, let allocatingResizableBuffer be true; otherwise let allocatingResizableBuffer be false.
876        // 3. If allocatingResizableBuffer is true, then
877        //     a. If byteLength > maxByteLength, throw a RangeError exception.
878        //     b. Append [[ArrayBufferMaxByteLength]] to slots.
879        if let Some(max_byte_len) = max_byte_len
880            && byte_len > max_byte_len
881        {
882            return Err(JsNativeError::range()
883                .with_message("`length` cannot be bigger than `maxByteLength`")
884                .into());
885        }
886
887        // 4. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", slots).
888        let prototype = get_prototype_from_constructor(
889            constructor,
890            StandardConstructors::array_buffer,
891            context,
892        )?;
893
894        // 5. Let block be ? CreateByteDataBlock(byteLength).
895        // Preemptively allocate for `max_byte_len` if possible.
896        //     a. If it is not possible to create a Data Block block consisting of maxByteLength bytes, throw a RangeError exception.
897        //     b. NOTE: Resizable ArrayBuffers are designed to be implementable with in-place growth. Implementations may
898        //        throw if, for example, virtual memory cannot be reserved up front.
899        let block = create_byte_data_block(byte_len, max_byte_len, context)?;
900
901        let obj = JsObject::new(
902            context.root_shape(),
903            prototype,
904            Self {
905                // 6. Set obj.[[ArrayBufferData]] to block.
906                // 7. Set obj.[[ArrayBufferByteLength]] to byteLength.
907                data: Some(block),
908                // 8. If allocatingResizableBuffer is true, then
909                //    c. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength.
910                max_byte_len,
911                detach_key: JsValue::undefined(),
912            },
913        );
914
915        // 9. Return obj.
916        Ok(obj)
917    }
918}
919
920/// Abstract operation [`GetArrayBufferMaxByteLengthOption ( options )`][spec]
921///
922/// [spec]: https://tc39.es/ecma262/#sec-getarraybuffermaxbytelengthoption
923fn get_max_byte_len(options: &JsValue, context: &mut Context) -> JsResult<Option<u64>> {
924    // 1. If options is not an Object, return empty.
925    let Some(options) = options.as_object() else {
926        return Ok(None);
927    };
928
929    // 2. Let maxByteLength be ? Get(options, "maxByteLength").
930    let max_byte_len = options.get(js_string!("maxByteLength"), context)?;
931
932    // 3. If maxByteLength is undefined, return empty.
933    if max_byte_len.is_undefined() {
934        return Ok(None);
935    }
936
937    // 4. Return ? ToIndex(maxByteLength).
938    max_byte_len.to_index(context).map(Some)
939}
940
941/// `CreateByteDataBlock ( size )` abstract operation.
942///
943/// The abstract operation `CreateByteDataBlock` takes argument `size` (a non-negative
944/// integer). For more information, check the [spec][spec].
945///
946/// [spec]: https://tc39.es/ecma262/#sec-createbytedatablock
947pub(crate) fn create_byte_data_block(
948    size: u64,
949    max_buffer_size: Option<u64>,
950    context: &mut Context,
951) -> JsResult<AlignedVec<u8>> {
952    let alloc_size = max_buffer_size.unwrap_or(size);
953
954    assert!(size <= alloc_size);
955
956    if alloc_size > context.host_hooks().max_buffer_size(context) {
957        return Err(JsNativeError::range()
958            .with_message("cannot allocate a buffer that exceeds the maximum buffer size")
959            .into());
960    }
961
962    // 1. Let db be a new Data Block value consisting of size bytes. If it is impossible to
963    //    create such a Data Block, throw a RangeError exception.
964    let alloc_size = alloc_size.try_into().map_err(|e| {
965        JsNativeError::range().with_message(format!("couldn't allocate the data block: {e}"))
966    })?;
967
968    let mut data_block = AlignedVec::<u8>::new(64);
969    data_block.try_reserve_exact(alloc_size).map_err(|e| {
970        let message = match e {
971            aligned_vec::TryReserveError::CapacityOverflow => {
972                format!("capacity overflow for size {size} while allocating data block")
973            }
974            aligned_vec::TryReserveError::AllocError { layout } => {
975                format!("invalid layout {layout:?} while allocating data block")
976            }
977        };
978        JsNativeError::range().with_message(message)
979    })?;
980
981    // since size <= alloc_size, then `size` must also fit inside a `usize`.
982    let size = size as usize;
983
984    // 2. Set all of the bytes of db to 0.
985    data_block.resize(size, 0);
986
987    // 3. Return db.
988    Ok(data_block)
989}