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}