Skip to main content

rquickjs_core/value/
array_buffer.rs

1use crate::{
2    markers::ParallelSend, qjs, Ctx, Error, FromJs, IntoJs, JsLifetime, Object, Result, Value,
3};
4use alloc::boxed::Box;
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::{
8    ffi::c_void,
9    fmt,
10    mem::{self, size_of, ManuallyDrop, MaybeUninit},
11    ops::Deref,
12    ptr::NonNull,
13    result::Result as StdResult,
14    slice,
15};
16
17use super::typed_array::TypedArrayItem;
18
19/// JS_NewArrayBuffer uses max_len=0 for fixed-length buffers, and calls the
20/// realloc callback with size=0 to free. Same sentinel, different meanings.
21const FIXED_SIZE: qjs::size_t = 0;
22const FREE: qjs::size_t = 0;
23
24/// A contiguous byte region owned by `self` and usable as the backing store
25/// of an [`ArrayBuffer`].
26///
27/// # Safety
28///
29/// * `as_ptr()` must return a pointer that is valid for reads (and writes,
30///   when used via [`ArrayBuffer::from_source`] / [`ArrayBuffer::from_source_shared`])
31///   of `len()` bytes.
32/// * The returned pointer must remain valid until `self` is dropped, including
33///   across moves of `self`.
34pub unsafe trait ArrayBufferSource {
35    fn as_ptr(&self) -> *mut u8;
36    fn len(&self) -> usize;
37    fn is_empty(&self) -> bool {
38        self.len() == 0
39    }
40}
41
42macro_rules! impl_array_buffer_source {
43    ($($t:ty),* $(,)?) => {
44        $(
45            unsafe impl ArrayBufferSource for $t {
46                fn as_ptr(&self) -> *mut u8 {
47                    <[u8]>::as_ptr(self) as *mut u8
48                }
49                fn len(&self) -> usize {
50                    <[u8]>::len(self)
51                }
52            }
53        )*
54    };
55}
56
57impl_array_buffer_source!(Vec<u8>, alloc::boxed::Box<[u8]>, Arc<[u8]>, Arc<Vec<u8>>);
58
59#[cfg(feature = "bytes")]
60impl_array_buffer_source!(bytes::Bytes);
61
62#[derive(Debug, Clone, Copy, Eq, PartialEq)]
63pub enum AsSliceError {
64    BufferUsed,
65    InvalidAlignment,
66}
67
68impl fmt::Display for AsSliceError {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            AsSliceError::BufferUsed => write!(f, "Buffer was already used"),
72            AsSliceError::InvalidAlignment => {
73                write!(f, "Buffer had a different alignment than was requested")
74            }
75        }
76    }
77}
78
79/// Rust representation of a JavaScript object of class ArrayBuffer.
80///
81#[derive(Debug, PartialEq, Clone, Eq, Hash)]
82#[repr(transparent)]
83pub struct ArrayBuffer<'js>(pub(crate) Object<'js>);
84
85unsafe impl<'js> JsLifetime<'js> for ArrayBuffer<'js> {
86    type Changed<'to> = ArrayBuffer<'to>;
87}
88
89impl<'js> ArrayBuffer<'js> {
90    /// Create array buffer from vector data
91    pub fn new<T: Copy>(ctx: Ctx<'js>, src: impl Into<Vec<T>>) -> Result<Self> {
92        let mut src = ManuallyDrop::new(src.into());
93        let ptr = src.as_mut_ptr();
94        let capacity = src.capacity();
95        let size = src.len() * size_of::<T>();
96
97        // `opaque` stays fixed across `transfer()`, so mutable capacity lives behind it instead of in it.
98        let capacity_cell = Box::into_raw(Box::new(capacity));
99
100        extern "C" fn realloc_raw<T: Copy>(
101            _rt: *mut qjs::JSRuntime,
102            opaque: *mut c_void,
103            ptr: *mut c_void,
104            size: qjs::size_t,
105        ) -> *mut c_void {
106            let capacity_cell = opaque as *mut usize;
107            let capacity = unsafe { *capacity_cell };
108            // `ManuallyDrop` so a failed or unsupported resize leaves the allocation untouched.
109            let mut vec = ManuallyDrop::new(unsafe {
110                Vec::from_raw_parts(ptr as *mut T, capacity, capacity)
111            });
112
113            if size == FREE {
114                unsafe {
115                    ManuallyDrop::drop(&mut vec);
116                    drop(Box::from_raw(capacity_cell));
117                }
118                return core::ptr::null_mut();
119            }
120
121            let elem_size = size_of::<T>();
122            if elem_size == 0 {
123                return core::ptr::null_mut();
124            }
125            let new_len = (size as usize).div_ceil(elem_size);
126
127            if new_len > capacity {
128                if vec.try_reserve_exact(new_len - capacity).is_err() {
129                    return core::ptr::null_mut();
130                }
131            } else if new_len < capacity {
132                vec.truncate(new_len);
133                vec.shrink_to(new_len);
134            }
135
136            unsafe { *capacity_cell = vec.capacity() };
137            vec.as_mut_ptr() as *mut c_void
138        }
139
140        Ok(Self(Object(unsafe {
141            let val = qjs::JS_NewArrayBuffer(
142                ctx.as_ptr(),
143                ptr as _,
144                size as _,
145                FIXED_SIZE,
146                Some(realloc_raw::<T>),
147                capacity_cell as _,
148                false,
149            );
150            ctx.handle_exception(val).inspect_err(|_| {
151                // don't forget to free data when error occurred
152                Vec::from_raw_parts(ptr, capacity, capacity);
153                drop(Box::from_raw(capacity_cell));
154            })?;
155            Value::from_js_value(ctx, val)
156        })))
157    }
158
159    /// Create array buffer from slice
160    pub fn new_copy<T: Copy>(ctx: Ctx<'js>, src: impl AsRef<[T]>) -> Result<Self> {
161        let src = src.as_ref();
162        let ptr = src.as_ptr();
163        let size = core::mem::size_of_val(src);
164
165        Ok(Self(Object(unsafe {
166            let val = qjs::JS_NewArrayBufferCopy(ctx.as_ptr(), ptr as _, size as _);
167            ctx.handle_exception(val)?;
168            Value::from_js_value(ctx.clone(), val)
169        })))
170    }
171
172    /// Create an `ArrayBuffer` from a source that owns its backing bytes.
173    ///
174    /// The source is moved into the buffer; JS has exclusive mutable access
175    /// until the buffer is collected, at which point the source is dropped.
176    /// Using this with a shared-immutable source (`Arc<[u8]>`, `bytes::Bytes`,
177    /// …) is unsound; use [`from_source_immutable`](Self::from_source_immutable) for those.
178    pub fn from_source<S>(ctx: Ctx<'js>, src: S) -> Result<Self>
179    where
180        S: ArrayBufferSource + ParallelSend + 'static,
181    {
182        let ptr = src.as_ptr();
183        let len = src.len();
184        unsafe { Self::from_external(ctx, ptr, len, false, false, move || drop(src)) }
185    }
186
187    /// Create a `SharedArrayBuffer` from a source that owns its backing bytes.
188    ///
189    /// See [`from_source`](Self::from_source) for ownership semantics.
190    pub fn from_source_shared<S>(ctx: Ctx<'js>, src: S) -> Result<Self>
191    where
192        S: ArrayBufferSource + ParallelSend + 'static,
193    {
194        let ptr = src.as_ptr();
195        let len = src.len();
196        unsafe { Self::from_external(ctx, ptr, len, true, false, move || drop(src)) }
197    }
198
199    /// Create an `ArrayBuffer` that JS sees as immutable, backed by a
200    /// possibly shared-immutable source.
201    ///
202    /// JS writes throw, so it is sound for the caller to keep holding
203    /// shared-immutable references to the backing store (`Arc<[u8]>`,
204    /// `bytes::Bytes`, …).
205    pub fn from_source_immutable<S>(ctx: Ctx<'js>, src: S) -> Result<Self>
206    where
207        S: ArrayBufferSource + ParallelSend + 'static,
208    {
209        let ptr = src.as_ptr();
210        let len = src.len();
211        unsafe { Self::from_external(ctx, ptr, len, false, true, move || drop(src)) }
212    }
213
214    /// Internal helper backing the safe `from_source*` constructors.
215    ///
216    /// `drop_fn` is invoked exactly once: when the buffer is garbage-collected,
217    /// or synchronously if construction fails.
218    ///
219    /// # Safety
220    ///
221    /// `ptr` must point to `len` bytes of valid memory until `drop_fn` runs.
222    unsafe fn from_external<F>(
223        ctx: Ctx<'js>,
224        ptr: *mut u8,
225        len: usize,
226        is_shared: bool,
227        immutable: bool,
228        drop_fn: F,
229    ) -> Result<Self>
230    where
231        F: FnOnce() + ParallelSend + 'static,
232    {
233        extern "C" fn shim<F: FnOnce()>(
234            _rt: *mut qjs::JSRuntime,
235            opaque: *mut c_void,
236            _ptr: *mut c_void,
237            size: qjs::size_t,
238        ) -> *mut c_void {
239            if size != FREE {
240                return core::ptr::null_mut();
241            }
242            unsafe {
243                let boxed: Box<F> = Box::from_raw(opaque as *mut F);
244                (*boxed)();
245            }
246            core::ptr::null_mut()
247        }
248
249        let opaque = Box::into_raw(Box::new(drop_fn)) as *mut c_void;
250
251        Ok(Self(Object(unsafe {
252            let val = qjs::JS_NewArrayBuffer(
253                ctx.as_ptr(),
254                ptr,
255                len as _,
256                FIXED_SIZE,
257                Some(shim::<F>),
258                opaque,
259                is_shared,
260            );
261            if let Err(e) = ctx.handle_exception(val) {
262                shim::<F>(
263                    qjs::JS_GetRuntime(ctx.as_ptr()),
264                    opaque,
265                    ptr as *mut c_void,
266                    FREE,
267                );
268                return Err(e);
269            }
270            if immutable {
271                qjs::JS_SetImmutableArrayBuffer(val, true);
272            }
273            Value::from_js_value(ctx, val)
274        })))
275    }
276
277    /// Get the length of the array buffer in bytes.
278    pub fn len(&self) -> usize {
279        Self::get_raw(&self.0).expect("Not an ArrayBuffer").len()
280    }
281
282    /// Returns whether an array buffer is empty.
283    pub fn is_empty(&self) -> bool {
284        self.len() == 0
285    }
286
287    /// Returns the underlying bytes of the buffer,
288    ///
289    /// Returns `None` if the array is detached.
290    ///
291    /// # Safety
292    ///
293    /// The returned slice aliases memory owned by the JS engine. The caller must not run
294    /// any JavaScript for as long as the slice is alive, since JS can write into, detach,
295    /// or (for a resizable buffer) reallocate the backing store, invalidating the slice.
296    pub unsafe fn as_bytes(&self) -> Option<&[u8]> {
297        Some(self.as_raw()?.as_ref())
298    }
299
300    /// Returns a slice if the buffer underlying buffer is properly aligned for the type and the
301    /// buffer is not detached.
302    ///
303    /// # Safety
304    ///
305    /// The returned slice aliases memory owned by the JS engine. The caller must not run
306    /// any JavaScript for as long as the slice is alive, since JS can write into, detach,
307    /// or (for a resizable buffer) reallocate the backing store, invalidating the slice.
308    pub unsafe fn as_slice<T: TypedArrayItem>(&self) -> StdResult<&[T], AsSliceError> {
309        let raw = Self::get_raw(&self.0).ok_or(AsSliceError::BufferUsed)?;
310        if raw.cast::<u8>().align_offset(mem::align_of::<T>()) != 0 {
311            return Err(AsSliceError::InvalidAlignment);
312        }
313        let len = raw.len() / size_of::<T>();
314        Ok(slice::from_raw_parts(raw.as_ptr().cast(), len))
315    }
316
317    /// Detach array buffer
318    pub fn detach(&mut self) {
319        unsafe { qjs::JS_DetachArrayBuffer(self.0.ctx.as_ptr(), self.0.as_js_value()) }
320    }
321
322    /// Reference to value
323    #[inline]
324    pub fn as_value(&self) -> &Value<'js> {
325        self.0.as_value()
326    }
327
328    /// Convert into value
329    #[inline]
330    pub fn into_value(self) -> Value<'js> {
331        self.0.into_value()
332    }
333
334    /// Convert from value
335    pub fn from_value(value: Value<'js>) -> Option<Self> {
336        Self::from_object(Object::from_value(value).ok()?)
337    }
338
339    /// Reference as an object
340    #[inline]
341    pub fn as_object(&self) -> &Object<'js> {
342        &self.0
343    }
344
345    /// Convert into an object
346    #[inline]
347    pub fn into_object(self) -> Object<'js> {
348        self.0
349    }
350
351    /// Convert from an object
352    pub fn from_object(object: Object<'js>) -> Option<Self> {
353        if Self::get_raw(&object.0).is_some() {
354            Some(Self(object))
355        } else {
356            None
357        }
358    }
359
360    /// Returns a pointer to the underlying bytes of the buffer,
361    ///
362    /// The returned pointer is only guaranteed valid until the next time
363    /// JavaScript runs: JS can write through it, detach the buffer, or, for a
364    /// resizable buffer, reallocate the backing store and free this pointer.
365    /// Treat the pointer as invalidated after any call back into the engine.
366    ///
367    /// Returns None if the buffer was already detached.
368    pub fn as_raw(&self) -> Option<NonNull<[u8]>> {
369        Self::get_raw(self.as_value())
370    }
371
372    pub(crate) fn get_raw(val: &Value<'js>) -> Option<NonNull<[u8]>> {
373        let ctx = val.ctx();
374        let val = val.as_js_value();
375        let mut size = MaybeUninit::<qjs::size_t>::uninit();
376        let ptr = unsafe { qjs::JS_GetArrayBuffer(ctx.as_ptr(), size.as_mut_ptr(), val) };
377
378        if let Some(ptr) = NonNull::new(ptr) {
379            let len = unsafe { size.assume_init() }
380                .try_into()
381                .expect(qjs::SIZE_T_ERROR);
382            Some(NonNull::slice_from_raw_parts(ptr, len))
383        } else {
384            None
385        }
386    }
387}
388
389impl<'js> Deref for ArrayBuffer<'js> {
390    type Target = Object<'js>;
391
392    fn deref(&self) -> &Self::Target {
393        self.as_object()
394    }
395}
396
397impl<'js> AsRef<Object<'js>> for ArrayBuffer<'js> {
398    fn as_ref(&self) -> &Object<'js> {
399        self.as_object()
400    }
401}
402
403impl<'js> AsRef<Value<'js>> for ArrayBuffer<'js> {
404    fn as_ref(&self) -> &Value<'js> {
405        self.as_value()
406    }
407}
408
409impl<'js> FromJs<'js> for ArrayBuffer<'js> {
410    fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
411        let ty_name = value.type_name();
412        if let Some(v) = Self::from_value(value) {
413            Ok(v)
414        } else {
415            Err(Error::new_from_js(ty_name, "ArrayBuffer"))
416        }
417    }
418}
419
420impl<'js> IntoJs<'js> for ArrayBuffer<'js> {
421    fn into_js(self, _: &Ctx<'js>) -> Result<Value<'js>> {
422        Ok(self.into_value())
423    }
424}
425
426impl<'js> Object<'js> {
427    /// Returns whether the object is an instance of [`ArrayBuffer`].
428    pub fn is_array_buffer(&self) -> bool {
429        ArrayBuffer::get_raw(&self.0).is_some()
430    }
431
432    /// Interpret as [`ArrayBuffer`]
433    ///
434    /// # Safety
435    /// You should be sure that the object actually is the required type.
436    pub unsafe fn ref_array_buffer(&self) -> &ArrayBuffer {
437        mem::transmute(self)
438    }
439
440    /// Turn the object into an array buffer if the object is an instance of [`ArrayBuffer`].
441    pub fn as_array_buffer(&self) -> Option<&ArrayBuffer> {
442        self.is_array_buffer()
443            .then_some(unsafe { self.ref_array_buffer() })
444    }
445}
446
447#[cfg(test)]
448mod test {
449    use crate::*;
450    use alloc::sync::Arc;
451
452    #[test]
453    fn from_javascript_i8() {
454        test_with(|ctx| {
455            let val: ArrayBuffer = ctx
456                .eval(
457                    r#"
458                        new Int8Array([0, -5, 1, 11]).buffer
459                    "#,
460                )
461                .unwrap();
462            assert_eq!(val.len(), 4);
463            assert_eq!(
464                unsafe { val.as_slice() }.unwrap() as &[i8],
465                &[0i8, -5, 1, 11]
466            );
467        });
468    }
469
470    #[test]
471    fn into_javascript_i8() {
472        test_with(|ctx| {
473            let val = ArrayBuffer::new(ctx.clone(), [-1i8, 0, 22, 5]).unwrap();
474            ctx.globals().set("a", val).unwrap();
475            let res: i8 = ctx
476                .eval(
477                    r#"
478                        let v = new Int8Array(a);
479                        v.length != 4 ? 1 :
480                        v[0] != -1 ? 2 :
481                        v[1] != 0 ? 3 :
482                        v[2] != 22 ? 4 :
483                        v[3] != 5 ? 5 :
484                        0
485                    "#,
486                )
487                .unwrap();
488            assert_eq!(res, 0);
489        })
490    }
491
492    #[test]
493    fn from_javascript_f32() {
494        test_with(|ctx| {
495            let val: ArrayBuffer = ctx
496                .eval(
497                    r#"
498                        new Float32Array([0.5, -5.25, 123.125]).buffer
499                    "#,
500                )
501                .unwrap();
502            assert_eq!(val.len(), 12);
503            assert_eq!(
504                unsafe { val.as_slice() }.unwrap() as &[f32],
505                &[0.5f32, -5.25, 123.125]
506            );
507        });
508    }
509
510    #[test]
511    fn into_javascript_f32() {
512        test_with(|ctx| {
513            let val = ArrayBuffer::new(ctx.clone(), [-1.5f32, 0.0, 2.25]).unwrap();
514            ctx.globals().set("a", val).unwrap();
515            let res: i8 = ctx
516                .eval(
517                    r#"
518                        let v = new Float32Array(a);
519                        a.byteLength != 12 ? 1 :
520                        v.length != 3 ? 2 :
521                        v[0] != -1.5 ? 3 :
522                        v[1] != 0 ? 4 :
523                        v[2] != 2.25 ? 5 :
524                        0
525                    "#,
526                )
527                .unwrap();
528            assert_eq!(res, 0);
529        })
530    }
531
532    #[test]
533    fn as_bytes() {
534        test_with(|ctx| {
535            let val: ArrayBuffer = ctx
536                .eval(
537                    r#"
538                        new Uint32Array([0xCAFEDEAD,0xFEEDBEAD]).buffer
539                    "#,
540                )
541                .unwrap();
542            let mut res = [0; 8];
543            let bytes_0 = 0xCAFEDEADu32.to_ne_bytes();
544            res[..4].copy_from_slice(&bytes_0);
545            let bytes_1 = 0xFEEDBEADu32.to_ne_bytes();
546            res[4..].copy_from_slice(&bytes_1);
547
548            assert_eq!(unsafe { val.as_bytes() }.unwrap(), &res)
549        });
550    }
551
552    #[test]
553    fn from_source_external_buffer() {
554        use core::sync::atomic::{AtomicBool, Ordering};
555
556        static DROPPED: AtomicBool = AtomicBool::new(false);
557
558        struct Tracker(alloc::boxed::Box<[u8]>);
559        unsafe impl ArrayBufferSource for Tracker {
560            fn as_ptr(&self) -> *mut u8 {
561                self.0.as_ptr()
562            }
563            fn len(&self) -> usize {
564                self.0.len()
565            }
566        }
567        impl Drop for Tracker {
568            fn drop(&mut self) {
569                DROPPED.store(true, Ordering::SeqCst);
570            }
571        }
572
573        let rt = crate::Runtime::new().unwrap();
574        let c = crate::Context::full(&rt).unwrap();
575        c.with(|ctx| {
576            let src = Tracker(alloc::vec![1u8, 2, 3, 4].into_boxed_slice());
577            let ab = ArrayBuffer::from_source(ctx.clone(), src).unwrap();
578            assert_eq!(ab.len(), 4);
579            assert_eq!(unsafe { ab.as_bytes() }.unwrap(), &[1, 2, 3, 4]);
580        });
581        rt.run_gc();
582        assert!(DROPPED.load(Ordering::SeqCst));
583    }
584
585    #[test]
586    fn from_source_error_invokes_drop_fn() {
587        use core::sync::atomic::{AtomicBool, Ordering};
588
589        static DROPPED: AtomicBool = AtomicBool::new(false);
590
591        // A source that lies about its length to force construction failure,
592        // and signals via `Drop` that the source was released exactly once.
593        struct BadSource(#[allow(dead_code)] alloc::boxed::Box<[u8]>);
594        unsafe impl ArrayBufferSource for BadSource {
595            fn as_ptr(&self) -> *mut u8 {
596                self.0.as_ptr()
597            }
598            fn len(&self) -> usize {
599                i64::MAX as usize
600            }
601        }
602        impl Drop for BadSource {
603            fn drop(&mut self) {
604                DROPPED.store(true, Ordering::SeqCst);
605            }
606        }
607
608        let rt = crate::Runtime::new().unwrap();
609        let c = crate::Context::full(&rt).unwrap();
610        c.with(|ctx| {
611            let src = BadSource(alloc::vec![1u8, 2, 3, 4].into_boxed_slice());
612            let err = ArrayBuffer::from_source(ctx.clone(), src);
613            assert!(err.is_err());
614        });
615        assert!(DROPPED.load(Ordering::SeqCst));
616    }
617
618    #[test]
619    fn from_source_immutable_arc_slices() {
620        struct ArcSlice {
621            arc: Arc<Vec<u8>>,
622            offset: usize,
623            len: usize,
624        }
625        unsafe impl ArrayBufferSource for ArcSlice {
626            fn as_ptr(&self) -> *mut u8 {
627                unsafe { self.arc.as_ptr().add(self.offset) }
628            }
629            fn len(&self) -> usize {
630                self.len
631            }
632        }
633
634        let buf: Arc<Vec<u8>> = Arc::new((0u8..16).collect());
635        let weak = Arc::downgrade(&buf);
636
637        let rt = crate::Runtime::new().unwrap();
638        let c = crate::Context::full(&rt).unwrap();
639        c.with(|ctx| {
640            let mk = |offset: usize, len: usize| -> ArrayBuffer<'_> {
641                ArrayBuffer::from_source_immutable(
642                    ctx.clone(),
643                    ArcSlice {
644                        arc: buf.clone(),
645                        offset,
646                        len,
647                    },
648                )
649                .unwrap()
650            };
651            let full = mk(0, 16);
652            let head = mk(0, 4);
653            let tail = mk(12, 4);
654            let middle = mk(4, 8);
655
656            assert_eq!(
657                unsafe { full.as_bytes() }.unwrap(),
658                (0u8..16).collect::<Vec<_>>()
659            );
660            assert_eq!(unsafe { head.as_bytes() }.unwrap(), &[0, 1, 2, 3]);
661            assert_eq!(unsafe { tail.as_bytes() }.unwrap(), &[12, 13, 14, 15]);
662            assert_eq!(
663                unsafe { middle.as_bytes() }.unwrap(),
664                (4u8..12).collect::<Vec<_>>()
665            );
666            assert_eq!(Arc::strong_count(&buf), 5);
667
668            ctx.globals().set("buf", full).unwrap();
669
670            let after: u8 = ctx
671                .eval::<u8, _>(
672                    r#"
673                        const arr = new Uint8Array(buf);
674                        arr[0] = 99;
675                        arr[0];
676                    "#,
677                )
678                .unwrap();
679            assert_eq!(after, 0, "immutable ArrayBuffer must not accept writes");
680
681            let writer = ctx.eval::<(), _>(
682                r#"
683                    "use strict";
684                    new DataView(buf).setUint8(0, 99);
685                "#,
686            );
687            assert!(
688                writer.is_err(),
689                "DataView write on immutable buffer must throw"
690            );
691        });
692
693        drop(buf);
694        rt.run_gc();
695        drop(c);
696        drop(rt);
697        assert!(weak.upgrade().is_none());
698    }
699
700    #[test]
701    fn from_source_vec() {
702        let rt = crate::Runtime::new().unwrap();
703        let c = crate::Context::full(&rt).unwrap();
704        c.with(|ctx| {
705            let ab = ArrayBuffer::from_source(ctx.clone(), alloc::vec![1u8, 2, 3, 4]).unwrap();
706            assert_eq!(unsafe { ab.as_bytes() }.unwrap(), &[1, 2, 3, 4]);
707        });
708    }
709
710    #[test]
711    fn transfer_to_different_length_grows_vec_buffer() {
712        test_with(|ctx| {
713            let ab = ArrayBuffer::new(ctx.clone(), alloc::vec![1u8, 2, 3, 4]).unwrap();
714            ctx.globals().set("buf", ab).unwrap();
715
716            ctx.eval::<(), _>("globalThis.grown = buf.transfer(8);")
717                .unwrap();
718
719            // A detached buffer fails `FromJs`, so fetch it as a plain `Object`.
720            let original: Object = ctx.globals().get("buf").unwrap();
721            assert!(
722                ArrayBuffer::from_object(original).is_none(),
723                "source buffer must be detached by transfer"
724            );
725
726            let grown: ArrayBuffer = ctx.globals().get("grown").unwrap();
727            assert_eq!(
728                unsafe { grown.as_bytes() }.unwrap(),
729                &[1, 2, 3, 4, 0, 0, 0, 0]
730            );
731        });
732    }
733
734    #[test]
735    fn transfer_to_different_length_shrinks_vec_buffer() {
736        test_with(|ctx| {
737            let ab = ArrayBuffer::new(ctx.clone(), alloc::vec![1u8, 2, 3, 4]).unwrap();
738            ctx.globals().set("buf", ab).unwrap();
739
740            ctx.eval::<(), _>("globalThis.shrunk = buf.transfer(2);")
741                .unwrap();
742
743            let shrunk: ArrayBuffer = ctx.globals().get("shrunk").unwrap();
744            assert_eq!(unsafe { shrunk.as_bytes() }.unwrap(), &[1, 2]);
745        });
746    }
747
748    #[test]
749    fn transfer_chain_grows_then_shrinks_vec_buffer() {
750        test_with(|ctx| {
751            let ab = ArrayBuffer::new(ctx.clone(), alloc::vec![1u8, 2, 3, 4]).unwrap();
752            ctx.globals().set("buf", ab).unwrap();
753
754            // Grow far enough to force a real move, then shrink, then grow again.
755            ctx.eval::<(), _>(
756                r#"
757                    globalThis.mid = buf.transfer(64);
758                    globalThis.small = mid.transfer(3);
759                    globalThis.end = small.transfer(50);
760                "#,
761            )
762            .unwrap();
763
764            let end: ArrayBuffer = ctx.globals().get("end").unwrap();
765            let bytes = unsafe { end.as_bytes() }.unwrap();
766            assert_eq!(bytes.len(), 50);
767            assert_eq!(&bytes[..3], &[1, 2, 3]);
768            assert!(bytes[3..].iter().all(|&b| b == 0));
769        });
770    }
771
772    #[test]
773    fn transfer_to_different_length_preserves_external_buffer() {
774        use core::sync::atomic::{AtomicUsize, Ordering};
775
776        struct Tracker {
777            data: alloc::boxed::Box<[u8]>,
778            drops: Arc<AtomicUsize>,
779        }
780
781        unsafe impl ArrayBufferSource for Tracker {
782            fn as_ptr(&self) -> *mut u8 {
783                self.data.as_ptr()
784            }
785
786            fn len(&self) -> usize {
787                self.data.len()
788            }
789        }
790
791        impl Drop for Tracker {
792            fn drop(&mut self) {
793                self.drops.fetch_add(1, Ordering::SeqCst);
794            }
795        }
796
797        let drops = Arc::new(AtomicUsize::new(0));
798        let rt = crate::Runtime::new().unwrap();
799        let c = crate::Context::full(&rt).unwrap();
800        c.with(|ctx| {
801            let src = Tracker {
802                data: alloc::vec![1u8, 2, 3, 4].into_boxed_slice(),
803                drops: drops.clone(),
804            };
805            let ab = ArrayBuffer::from_source(ctx.clone(), src).unwrap();
806            ctx.globals().set("buf", ab).unwrap();
807
808            assert!(ctx.eval::<(), _>("buf.transfer(8)").is_err());
809            drop(ctx.catch());
810            assert_eq!(drops.load(Ordering::SeqCst), 0);
811
812            let ab: ArrayBuffer = ctx.globals().get("buf").unwrap();
813            assert_eq!(unsafe { ab.as_bytes() }.unwrap(), &[1, 2, 3, 4]);
814        });
815
816        drop(c);
817        drop(rt);
818        assert_eq!(drops.load(Ordering::SeqCst), 1);
819    }
820
821    #[test]
822    fn from_source_immutable_arc() {
823        let arc: Arc<[u8]> = Arc::from((0u8..8).collect::<Vec<_>>().into_boxed_slice());
824        let weak = Arc::downgrade(&arc);
825        assert_eq!(Arc::strong_count(&arc), 1);
826
827        let rt = crate::Runtime::new().unwrap();
828        let c = crate::Context::full(&rt).unwrap();
829
830        // Case 1: JS drops first while Rust keeps its Arc clone.
831        c.with(|ctx| {
832            let _ab = ArrayBuffer::from_source_immutable(ctx.clone(), arc.clone()).unwrap();
833            assert_eq!(Arc::strong_count(&arc), 2, "Arc cloned into drop closure");
834            // _ab drops at end of block; shim runs; closure drops its Arc clone.
835        });
836        assert_eq!(
837            Arc::strong_count(&arc),
838            1,
839            "drop closure must release its Arc clone when ArrayBuffer is freed"
840        );
841        assert!(
842            weak.upgrade().is_some(),
843            "allocation must stay alive while Rust still holds the Arc"
844        );
845
846        // Case 2: Rust drops first, JS keeps the buffer (stored in globals).
847        c.with(|ctx| {
848            let ab = ArrayBuffer::from_source_immutable(ctx.clone(), arc.clone()).unwrap();
849            ctx.globals().set("buf", ab).unwrap();
850            assert_eq!(Arc::strong_count(&arc), 2);
851        });
852        drop(arc);
853        assert!(
854            weak.upgrade().is_some(),
855            "allocation must stay alive: JS still holds the buffer via the Arc clone in the closure"
856        );
857        assert_eq!(
858            weak.strong_count(),
859            1,
860            "only the closure's Arc clone should remain"
861        );
862
863        // Drop the context: JS buffer is freed, shim runs, Arc clone released.
864        drop(c);
865        drop(rt);
866        assert!(
867            weak.upgrade().is_none(),
868            "allocation must be freed after both Rust and JS release their handles"
869        );
870    }
871
872    #[cfg(feature = "bytes")]
873    #[test]
874    fn from_source_immutable_bytes() {
875        let data: bytes::Bytes = (0u8..8).collect::<Vec<_>>().into();
876        let rt = crate::Runtime::new().unwrap();
877        let c = crate::Context::full(&rt).unwrap();
878        c.with(|ctx| {
879            let ab = ArrayBuffer::from_source_immutable(ctx.clone(), data.clone()).unwrap();
880            assert_eq!(
881                unsafe { ab.as_bytes() }.unwrap(),
882                (0u8..8).collect::<Vec<_>>()
883            );
884        });
885    }
886}