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    /// Whether an attached buffer can be detached using this key.
297    ///
298    /// Hosts serializing transfer lists use this before moving any allocation,
299    /// so a rejected entry cannot partially detach earlier entries.
300    #[must_use]
301    pub fn can_detach(&self, key: &JsValue) -> bool {
302        self.data.is_some() && JsValue::same_value(&self.detach_key, key)
303    }
304
305    /// Detaches the inner data of this `ArrayBuffer`, returning the original buffer if still
306    /// present.
307    ///
308    /// # Errors
309    ///
310    /// Throws an error if the provided detach key is invalid.
311    pub fn detach(&mut self, key: &JsValue) -> JsResult<Option<AlignedVec<u8>>> {
312        if !JsValue::same_value(&self.detach_key, key) {
313            return Err(JsNativeError::typ()
314                .with_message("Cannot detach array buffer with different key")
315                .into());
316        }
317
318        Ok(self.data.take())
319    }
320
321    /// `IsDetachedBuffer ( arrayBuffer )`
322    ///
323    /// More information:
324    ///  - [ECMAScript reference][spec]
325    ///
326    /// [spec]: https://tc39.es/ecma262/#sec-isdetachedbuffer
327    pub(crate) const fn is_detached(&self) -> bool {
328        // 1. If arrayBuffer.[[ArrayBufferData]] is null, return true.
329        // 2. Return false.
330        self.data.is_none()
331    }
332
333    pub(crate) fn is_fixed_len(&self) -> bool {
334        self.max_byte_len.is_none()
335    }
336}
337
338impl IntrinsicObject for ArrayBuffer {
339    fn init(realm: &Realm) {
340        let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE;
341
342        let get_species = BuiltInBuilder::callable(realm, Self::get_species)
343            .name(js_string!("get [Symbol.species]"))
344            .build();
345
346        let get_byte_length = BuiltInBuilder::callable(realm, Self::get_byte_length)
347            .name(js_string!("get byteLength"))
348            .build();
349
350        let get_resizable = BuiltInBuilder::callable(realm, Self::get_resizable)
351            .name(js_string!("get resizable"))
352            .build();
353
354        let get_max_byte_length = BuiltInBuilder::callable(realm, Self::get_max_byte_length)
355            .name(js_string!("get maxByteLength"))
356            .build();
357
358        // ES2024 resizable/transferable buffers are standard; `detached` and `transfer` ship unflagged.
359        let get_detached = BuiltInBuilder::callable(realm, Self::get_detached)
360            .name(js_string!("get detached"))
361            .build();
362
363        let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
364            .static_accessor(
365                JsSymbol::species(),
366                Some(get_species),
367                None,
368                Attribute::CONFIGURABLE,
369            )
370            .static_method(Self::is_view, js_string!("isView"), 1)
371            .accessor(
372                js_string!("byteLength"),
373                Some(get_byte_length),
374                None,
375                flag_attributes,
376            )
377            .accessor(
378                js_string!("resizable"),
379                Some(get_resizable),
380                None,
381                flag_attributes,
382            )
383            .accessor(
384                js_string!("maxByteLength"),
385                Some(get_max_byte_length),
386                None,
387                flag_attributes,
388            )
389            .method(Self::js_resize, js_string!("resize"), 1)
390            .method(Self::slice, js_string!("slice"), 2)
391            .property(
392                JsSymbol::to_string_tag(),
393                Self::NAME,
394                Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
395            );
396
397        let builder = builder
398            .accessor(
399                js_string!("detached"),
400                Some(get_detached),
401                None,
402                flag_attributes,
403            )
404            .method(Self::transfer::<false>, js_string!("transfer"), 0)
405            .method(
406                Self::transfer::<true>,
407                js_string!("transferToFixedLength"),
408                0,
409            );
410
411        builder.build();
412    }
413
414    fn get(intrinsics: &Intrinsics) -> JsObject {
415        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
416    }
417}
418
419impl BuiltInObject for ArrayBuffer {
420    const NAME: JsString = StaticJsStrings::ARRAY_BUFFER;
421}
422
423impl BuiltInConstructor for ArrayBuffer {
424    const PROTOTYPE_STORAGE_SLOTS: usize = 13;
425    const CONSTRUCTOR_STORAGE_SLOTS: usize = 3;
426    const CONSTRUCTOR_ARGUMENTS: usize = 1;
427
428    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
429        StandardConstructors::array_buffer;
430
431    /// `ArrayBuffer ( length )`
432    ///
433    /// More information:
434    ///  - [ECMAScript reference][spec]
435    ///
436    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer-length
437    fn constructor(
438        new_target: &JsValue,
439        args: &[JsValue],
440        context: &mut Context,
441    ) -> JsResult<JsValue> {
442        // 1. If NewTarget is undefined, throw a TypeError exception.
443        if new_target.is_undefined() {
444            return Err(JsNativeError::typ()
445                .with_message("ArrayBuffer.constructor called with undefined new target")
446                .into());
447        }
448
449        // 2. Let byteLength be ? ToIndex(length).
450        let byte_len = args.get_or_undefined(0).to_index(context)?;
451
452        // 3. Let requestedMaxByteLength be ? GetArrayBufferMaxByteLengthOption(options).
453        let max_byte_len = get_max_byte_len(args.get_or_undefined(1), context)?;
454
455        // 4. Return ? AllocateArrayBuffer(NewTarget, byteLength, requestedMaxByteLength).
456        Ok(Self::allocate(new_target, byte_len, max_byte_len, context)?
457            .upcast()
458            .into())
459    }
460}
461
462impl ArrayBuffer {
463    /// `ArrayBuffer.isView ( arg )`
464    ///
465    /// More information:
466    ///  - [ECMAScript reference][spec]
467    ///
468    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.isview
469    #[allow(clippy::unnecessary_wraps)]
470    fn is_view(_: &JsValue, args: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
471        // 1. If Type(arg) is not Object, return false.
472        // 2. If arg has a [[ViewedArrayBuffer]] internal slot, return true.
473        // 3. Return false.
474        Ok(args
475            .get_or_undefined(0)
476            .as_object()
477            .is_some_and(|obj| obj.is::<TypedArray>() || obj.is::<DataView>())
478            .into())
479    }
480
481    /// `get ArrayBuffer [ @@species ]`
482    ///
483    /// More information:
484    ///  - [ECMAScript reference][spec]
485    ///
486    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer-@@species
487    #[allow(clippy::unnecessary_wraps)]
488    fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
489        // 1. Return the this value.
490        Ok(this.clone())
491    }
492
493    /// `get ArrayBuffer.prototype.byteLength`
494    ///
495    /// More information:
496    ///  - [ECMAScript reference][spec]
497    ///
498    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.bytelength
499    pub(crate) fn get_byte_length(
500        this: &JsValue,
501        _args: &[JsValue],
502        _: &mut Context,
503    ) -> JsResult<JsValue> {
504        // 1. Let O be the this value.
505        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
506        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
507        let object = this.as_object();
508        let buf = object
509            .as_ref()
510            .and_then(JsObject::downcast_ref::<Self>)
511            .ok_or_else(|| {
512                JsNativeError::typ()
513                    .with_message("get ArrayBuffer.prototype.byteLength called with invalid `this`")
514            })?;
515
516        // 4. If IsDetachedBuffer(O) is true, return +0𝔽.
517        // 5. Let length be O.[[ArrayBufferByteLength]].
518        // 6. Return 𝔽(length).
519        Ok(buf.len().into())
520    }
521
522    /// [`get ArrayBuffer.prototype.maxByteLength`][spec].
523    ///
524    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.maxbytelength
525    pub(crate) fn get_max_byte_length(
526        this: &JsValue,
527        _args: &[JsValue],
528        _context: &mut Context,
529    ) -> JsResult<JsValue> {
530        // 1. Let O be the this value.
531        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
532        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
533        let object = this.as_object();
534        let buf = object
535            .as_ref()
536            .and_then(JsObject::downcast_ref::<Self>)
537            .ok_or_else(|| {
538                JsNativeError::typ().with_message(
539                    "get ArrayBuffer.prototype.maxByteLength called with invalid `this`",
540                )
541            })?;
542
543        // 4. If IsDetachedBuffer(O) is true, return +0𝔽.
544        let Some(data) = buf.bytes() else {
545            return Ok(JsValue::from(0));
546        };
547
548        // 5. If IsFixedLengthArrayBuffer(O) is true, then
549        //     a. Let length be O.[[ArrayBufferByteLength]].
550        // 6. Else,
551        //     a. Let length be O.[[ArrayBufferMaxByteLength]].
552        // 7. Return 𝔽(length).
553        Ok(buf.max_byte_len.unwrap_or(data.len() as u64).into())
554    }
555
556    /// [`get ArrayBuffer.prototype.resizable`][spec].
557    ///
558    /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.resizable
559    pub(crate) fn get_resizable(
560        this: &JsValue,
561        _args: &[JsValue],
562        _context: &mut Context,
563    ) -> JsResult<JsValue> {
564        // 1. Let O be the this value.
565        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
566        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
567        let object = this.as_object();
568        let buf = object
569            .as_ref()
570            .and_then(JsObject::downcast_ref::<Self>)
571            .ok_or_else(|| {
572                JsNativeError::typ()
573                    .with_message("get ArrayBuffer.prototype.resizable called with invalid `this`")
574            })?;
575
576        // 4. If IsFixedLengthArrayBuffer(O) is false, return true; otherwise return false.
577        Ok(JsValue::from(!buf.is_fixed_len()))
578    }
579
580    /// [`get ArrayBuffer.prototype.detached`][spec].
581    ///
582    /// [spec]: https://tc39.es/proposal-arraybuffer-transfer/#sec-get-arraybuffer.prototype.detached
583    fn get_detached(
584        this: &JsValue,
585        _args: &[JsValue],
586        _context: &mut Context,
587    ) -> JsResult<JsValue> {
588        // 1. Let O be the this value.
589        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
590        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
591        let object = this.as_object();
592        let buf = object
593            .as_ref()
594            .and_then(JsObject::downcast_ref::<Self>)
595            .ok_or_else(|| {
596                JsNativeError::typ()
597                    .with_message("get ArrayBuffer.prototype.detached called with invalid `this`")
598            })?;
599
600        // 4. Return IsDetachedBuffer(O).
601        Ok(buf.is_detached().into())
602    }
603
604    /// [`ArrayBuffer.prototype.resize ( newLength )`][spec].
605    ///
606    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.prototype.resize
607    pub(crate) fn js_resize(
608        this: &JsValue,
609        args: &[JsValue],
610        context: &mut Context,
611    ) -> JsResult<JsValue> {
612        // 1. Let O be the this value.
613        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]).
614        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
615        let buf = this
616            .as_object()
617            .and_then(|o| o.clone().downcast::<Self>().ok())
618            .ok_or_else(|| {
619                JsNativeError::typ()
620                    .with_message("ArrayBuffer.prototype.resize called with invalid `this`")
621            })?;
622
623        // 4. Let newByteLength be ? ToIndex(newLength).
624        let new_byte_length = args.get_or_undefined(0).to_index(context)?;
625
626        // These steps are performed in the `Self::resize` method.
627        // 5. If IsDetachedBuffer(O) is true, throw a TypeError exception.
628        // 6. If newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception.
629
630        // TODO: 7. Let hostHandled be ? HostResizeArrayBuffer(O, newByteLength).
631        // 8. If hostHandled is handled, return undefined.
632        // Used in engines to handle Wasm buffers in a special way, but we don't
633        // have a Wasm interpreter in place yet.
634
635        // 9. Let oldBlock be O.[[ArrayBufferData]].
636        // 10. Let newBlock be ? CreateByteDataBlock(newByteLength).
637        // 11. Let copyLength be min(newByteLength, O.[[ArrayBufferByteLength]]).
638        // 12. Perform CopyDataBlockBytes(newBlock, 0, oldBlock, 0, copyLength).
639        // 13. NOTE: Neither creation of the new Data Block nor copying from the old Data Block are observable.
640        //     Implementations may implement this method as in-place growth or shrinkage.
641        // 14. Set O.[[ArrayBufferData]] to newBlock.
642        // 15. Set O.[[ArrayBufferByteLength]] to newByteLength.
643        buf.borrow_mut().data_mut().resize(new_byte_length)?;
644
645        // 16. Return undefined.
646        Ok(JsValue::undefined())
647    }
648
649    /// `ArrayBuffer.prototype.slice ( start, end )`
650    ///
651    /// More information:
652    ///  - [ECMAScript reference][spec]
653    ///
654    /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.prototype.slice
655    fn slice(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
656        // 1. Let O be the this value.
657        // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]).
658        // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception.
659        let buf = this
660            .as_object()
661            .and_then(|o| o.clone().downcast::<Self>().ok())
662            .ok_or_else(|| {
663                JsNativeError::typ()
664                    .with_message("ArrayBuffer.slice called with invalid `this` value")
665            })?;
666
667        let len = {
668            let buf = buf.borrow();
669            // 4. If IsDetachedBuffer(O) is true, throw a TypeError exception.
670            if buf.data().is_detached() {
671                return Err(JsNativeError::typ()
672                    .with_message("ArrayBuffer.slice called with detached buffer")
673                    .into());
674            }
675            // 5. Let len be O.[[ArrayBufferByteLength]].
676            buf.data().len() as u64
677        };
678
679        // 6. Let relativeStart be ? ToIntegerOrInfinity(start).
680        // 7. If relativeStart = -∞, let first be 0.
681        // 8. Else if relativeStart < 0, let first be max(len + relativeStart, 0).
682        // 9. Else, let first be min(relativeStart, len).
683        let first = Array::get_relative_start(context, args.get_or_undefined(0), len)?;
684
685        // 10. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
686        // 11. If relativeEnd = -∞, let final be 0.
687        // 12. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
688        // 13. Else, let final be min(relativeEnd, len).
689        let final_ = Array::get_relative_end(context, args.get_or_undefined(1), len)?;
690
691        // 14. Let newLen be max(final - first, 0).
692        let new_len = final_.saturating_sub(first);
693
694        // 15. Let ctor be ? SpeciesConstructor(O, %ArrayBuffer%).
695        let ctor = buf
696            .clone()
697            .upcast()
698            .species_constructor(StandardConstructors::array_buffer, context)?;
699
700        // 16. Let new be ? Construct(ctor, « 𝔽(newLen) »).
701        let new = ctor.construct(&[new_len.into()], Some(&ctor), context)?;
702
703        // 17. Perform ? RequireInternalSlot(new, [[ArrayBufferData]]).
704        // 18. If IsSharedArrayBuffer(new) is true, throw a TypeError exception.
705        let Ok(new) = new.downcast::<Self>() else {
706            return Err(JsNativeError::typ()
707                .with_message("ArrayBuffer constructor returned invalid object")
708                .into());
709        };
710
711        // 20. If SameValue(new, O) is true, throw a TypeError exception.
712        if JsObject::equals(&buf, &new) {
713            return Err(JsNativeError::typ()
714                .with_message("new ArrayBuffer is the same as this ArrayBuffer")
715                .into());
716        }
717
718        {
719            // 19. If IsDetachedBuffer(new) is true, throw a TypeError exception.
720            // 25. Let toBuf be new.[[ArrayBufferData]].
721            let mut new = new.borrow_mut();
722            let Some(to_buf) = new.data_mut().bytes_mut() else {
723                return Err(JsNativeError::typ()
724                    .with_message("ArrayBuffer constructor returned detached ArrayBuffer")
725                    .into());
726            };
727
728            // 21. If new.[[ArrayBufferByteLength]] < newLen, throw a TypeError exception.
729            if (to_buf.len() as u64) < new_len {
730                return Err(JsNativeError::typ()
731                    .with_message("new ArrayBuffer length too small")
732                    .into());
733            }
734
735            // 22. NOTE: Side-effects of the above steps may have detached O.
736            // 23. If IsDetachedBuffer(O) is true, throw a TypeError exception.
737            // 24. Let fromBuf be O.[[ArrayBufferData]].
738            let buf = buf.borrow();
739            let Some(from_buf) = buf.data().bytes() else {
740                return Err(JsNativeError::typ()
741                    .with_message("ArrayBuffer detached while ArrayBuffer.slice was running")
742                    .into());
743            };
744
745            // 26. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen).
746            let first = first as usize;
747            let new_len = new_len as usize;
748            to_buf[..new_len].copy_from_slice(&from_buf[first..first + new_len]);
749        }
750
751        // 27. Return new.
752        Ok(new.upcast().into())
753    }
754
755    /// [`ArrayBuffer.prototype.transfer ( [ newLength ] )`][transfer] and
756    /// [`ArrayBuffer.prototype.transferToFixedLength ( [ newLength ] )`][transferFL]
757    ///
758    /// [transfer]: https://tc39.es/proposal-arraybuffer-transfer/#sec-arraybuffer.prototype.transfer
759    /// [transferFL]: https://tc39.es/proposal-arraybuffer-transfer/#sec-arraybuffer.prototype.transfertofixedlength
760    fn transfer<const TO_FIXED_LENGTH: bool>(
761        this: &JsValue,
762        args: &[JsValue],
763        context: &mut Context,
764    ) -> JsResult<JsValue> {
765        // 1. Let O be the this value.
766        // 2. Return ? ArrayBufferCopyAndDetach(O, newLength, preserve-resizability).
767
768        // Abstract operation `ArrayBufferCopyAndDetach ( arrayBuffer, newLength, preserveResizability )`
769        // https://tc39.es/proposal-arraybuffer-transfer/#sec-arraybuffercopyanddetach
770
771        let new_length = args.get_or_undefined(0);
772
773        // 1. Perform ? RequireInternalSlot(arrayBuffer, [[ArrayBufferData]]).
774        // 2. If IsSharedArrayBuffer(arrayBuffer) is true, throw a TypeError exception.
775        let buf = this
776            .as_object()
777            .and_then(|o| o.clone().downcast::<Self>().ok())
778            .ok_or_else(|| {
779                JsNativeError::typ().with_message(if TO_FIXED_LENGTH {
780                    "ArrayBuffer.prototype.transferToFixedLength called with invalid `this`"
781                } else {
782                    "ArrayBuffer.prototype.transfer called with invalid `this`"
783                })
784            })?;
785
786        // 3. If newLength is undefined, then
787        let new_len = if new_length.is_undefined() {
788            // a. Let newByteLength be arrayBuffer.[[ArrayBufferByteLength]].
789            buf.borrow().data().len() as u64
790        } else {
791            // 4. Else,
792            //     a. Let newByteLength be ? ToIndex(newLength).
793            new_length.to_index(context)?
794        };
795
796        // 5. If IsDetachedBuffer(arrayBuffer) is true, throw a TypeError exception.
797        let Some(mut bytes) = buf.borrow_mut().data_mut().data.take() else {
798            return Err(JsNativeError::typ()
799                .with_message("cannot transfer a detached buffer")
800                .into());
801        };
802
803        // 6. If preserveResizability is preserve-resizability and IsResizableArrayBuffer(arrayBuffer)
804        //    is true, then
805        //     a. Let newMaxByteLength be arrayBuffer.[[ArrayBufferMaxByteLength]].
806        // 7. Else,
807        //     a. Let newMaxByteLength be empty.
808        let new_max_len = buf
809            .borrow()
810            .data()
811            .max_byte_len
812            .filter(|_| !TO_FIXED_LENGTH);
813
814        // 8. If arrayBuffer.[[ArrayBufferDetachKey]] is not undefined, throw a TypeError exception.
815        if !buf.borrow().data().detach_key.is_undefined() {
816            buf.borrow_mut().data_mut().data = Some(bytes);
817            return Err(JsNativeError::typ()
818                .with_message("cannot transfer a buffer with a detach key")
819                .into());
820        }
821
822        // Effectively, the next steps only create a new object for the same vec, so we can skip all
823        // those steps and just make a single check + trigger the realloc.
824
825        // 9. Let newBuffer be ? AllocateArrayBuffer(%ArrayBuffer%, newByteLength, newMaxByteLength).
826        // 10. Let copyLength be min(newByteLength, arrayBuffer.[[ArrayBufferByteLength]]).
827        // 11. Let fromBlock be arrayBuffer.[[ArrayBufferData]].
828        // 12. Let toBlock be newBuffer.[[ArrayBufferData]].
829        // 13. Perform CopyDataBlockBytes(toBlock, 0, fromBlock, 0, copyLength).
830        // 14. NOTE: Neither creation of the new Data Block nor copying from the old Data Block are
831        //     observable. Implementations may implement this method as a zero-copy move or a realloc.
832        // 15. Perform ! DetachArrayBuffer(arrayBuffer).
833        // 16. Return newBuffer.
834        if let Some(new_max_len) = new_max_len {
835            if new_len > new_max_len {
836                buf.borrow_mut().data_mut().data = Some(bytes);
837                return Err(JsNativeError::range()
838                    .with_message("`length` cannot be bigger than `maxByteLength`")
839                    .into());
840            }
841            // Should only truncate without reallocating.
842            bytes.resize(new_len as usize, 0);
843        } else {
844            bytes.resize(new_len as usize, 0);
845
846            // Realloc the vec to fit onto the new exact length.
847            bytes.shrink_to_fit();
848        }
849
850        let prototype = context
851            .intrinsics()
852            .constructors()
853            .array_buffer()
854            .prototype();
855
856        Ok(JsObject::from_proto_and_data_with_shared_shape(
857            context.root_shape(),
858            prototype,
859            ArrayBuffer {
860                data: Some(bytes),
861                max_byte_len: new_max_len,
862                detach_key: JsValue::undefined(),
863            },
864        )
865        .into())
866    }
867
868    /// `AllocateArrayBuffer ( constructor, byteLength )`
869    ///
870    /// More information:
871    ///  - [ECMAScript reference][spec]
872    ///
873    /// [spec]: https://tc39.es/ecma262/#sec-allocatearraybuffer
874    pub(crate) fn allocate(
875        constructor: &JsValue,
876        byte_len: u64,
877        max_byte_len: Option<u64>,
878        context: &mut Context,
879    ) -> JsResult<JsObject<ArrayBuffer>> {
880        // 1. Let slots be « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] ».
881        // 2. If maxByteLength is present and maxByteLength is not empty, let allocatingResizableBuffer be true; otherwise let allocatingResizableBuffer be false.
882        // 3. If allocatingResizableBuffer is true, then
883        //     a. If byteLength > maxByteLength, throw a RangeError exception.
884        //     b. Append [[ArrayBufferMaxByteLength]] to slots.
885        if let Some(max_byte_len) = max_byte_len
886            && byte_len > max_byte_len
887        {
888            return Err(JsNativeError::range()
889                .with_message("`length` cannot be bigger than `maxByteLength`")
890                .into());
891        }
892
893        // 4. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", slots).
894        let prototype = get_prototype_from_constructor(
895            constructor,
896            StandardConstructors::array_buffer,
897            context,
898        )?;
899
900        // 5. Let block be ? CreateByteDataBlock(byteLength).
901        // Preemptively allocate for `max_byte_len` if possible.
902        //     a. If it is not possible to create a Data Block block consisting of maxByteLength bytes, throw a RangeError exception.
903        //     b. NOTE: Resizable ArrayBuffers are designed to be implementable with in-place growth. Implementations may
904        //        throw if, for example, virtual memory cannot be reserved up front.
905        let block = create_byte_data_block(byte_len, max_byte_len, context)?;
906
907        let obj = JsObject::new(
908            context.root_shape(),
909            prototype,
910            Self {
911                // 6. Set obj.[[ArrayBufferData]] to block.
912                // 7. Set obj.[[ArrayBufferByteLength]] to byteLength.
913                data: Some(block),
914                // 8. If allocatingResizableBuffer is true, then
915                //    c. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength.
916                max_byte_len,
917                detach_key: JsValue::undefined(),
918            },
919        );
920
921        // 9. Return obj.
922        Ok(obj)
923    }
924}
925
926/// Abstract operation [`GetArrayBufferMaxByteLengthOption ( options )`][spec]
927///
928/// [spec]: https://tc39.es/ecma262/#sec-getarraybuffermaxbytelengthoption
929fn get_max_byte_len(options: &JsValue, context: &mut Context) -> JsResult<Option<u64>> {
930    // 1. If options is not an Object, return empty.
931    let Some(options) = options.as_object() else {
932        return Ok(None);
933    };
934
935    // 2. Let maxByteLength be ? Get(options, "maxByteLength").
936    let max_byte_len = options.get(js_string!("maxByteLength"), context)?;
937
938    // 3. If maxByteLength is undefined, return empty.
939    if max_byte_len.is_undefined() {
940        return Ok(None);
941    }
942
943    // 4. Return ? ToIndex(maxByteLength).
944    max_byte_len.to_index(context).map(Some)
945}
946
947/// `CreateByteDataBlock ( size )` abstract operation.
948///
949/// The abstract operation `CreateByteDataBlock` takes argument `size` (a non-negative
950/// integer). For more information, check the [spec][spec].
951///
952/// [spec]: https://tc39.es/ecma262/#sec-createbytedatablock
953pub(crate) fn create_byte_data_block(
954    size: u64,
955    max_buffer_size: Option<u64>,
956    context: &mut Context,
957) -> JsResult<AlignedVec<u8>> {
958    let alloc_size = max_buffer_size.unwrap_or(size);
959
960    assert!(size <= alloc_size);
961
962    if alloc_size > context.host_hooks().max_buffer_size(context) {
963        return Err(JsNativeError::range()
964            .with_message("cannot allocate a buffer that exceeds the maximum buffer size")
965            .into());
966    }
967
968    // 1. Let db be a new Data Block value consisting of size bytes. If it is impossible to
969    //    create such a Data Block, throw a RangeError exception.
970    let alloc_size = alloc_size.try_into().map_err(|e| {
971        JsNativeError::range().with_message(format!("couldn't allocate the data block: {e}"))
972    })?;
973
974    let mut data_block = AlignedVec::<u8>::new(64);
975    data_block.try_reserve_exact(alloc_size).map_err(|e| {
976        let message = match e {
977            aligned_vec::TryReserveError::CapacityOverflow => {
978                format!("capacity overflow for size {size} while allocating data block")
979            }
980            aligned_vec::TryReserveError::AllocError { layout } => {
981                format!("invalid layout {layout:?} while allocating data block")
982            }
983        };
984        JsNativeError::range().with_message(message)
985    })?;
986
987    // since size <= alloc_size, then `size` must also fit inside a `usize`.
988    let size = size as usize;
989
990    // 2. Set all of the bytes of db to 0.
991    data_block.resize(size, 0);
992
993    // 3. Return db.
994    Ok(data_block)
995}