diff --git a/core/runtime/src/store/from.rs b/core/runtime/src/store/from.rs index 203746fd042..9dc33a1e962 100644 --- a/core/runtime/src/store/from.rs +++ b/core/runtime/src/store/from.rs @@ -59,7 +59,7 @@ fn try_from_js_object( /// Transfer an object into a store instead of cloning it. See [mdn]. /// /// Only [transferable objects][to] can be transferred. Anything else will return an -/// error. Since any object t +/// error. /// /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects /// [to]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects#supported_objects @@ -151,6 +151,28 @@ fn clone_typed_array( Ok(dolly) } +fn clone_dataview( + original: &JsObject, + dataview: &JsDataView, + transfer: &FxHashSet, + seen: &mut SeenMap, + context: &mut Context, +) -> JsResult { + let byte_length = dataview.byte_length(context)?; + let byte_offset = dataview.byte_offset(context)?; + + let buffer_value = dataview.buffer(context)?; + let buffer = try_from_js_value(&buffer_value, transfer, seen, context)?; + + let dolly = JsValueStore::new(ValueStoreInner::DataView { + buffer, + byte_length, + byte_offset, + }); + seen.insert(original, dolly.clone()); + Ok(dolly) +} + fn clone_date( original: &JsObject, date: &JsDate, @@ -262,8 +284,8 @@ fn try_from_js_object_clone( return Err(js_error!(TypeError: "Errors are not supported yet.")); } else if let Ok(ref regexp) = JsRegExp::from_object(object.clone()) { return clone_regexp(object, regexp, seen, context); - } else if let Ok(_dataview) = JsDataView::from_object(object.clone()) { - return Err(js_error!(TypeError: "Data views are not supported yet.")); + } else if let Ok(dataview) = JsDataView::from_object(object.clone()) { + return clone_dataview(object, &dataview, transfer, seen, context); } else if object.is_callable() { // Functions are invalid. return Err(unsupported_type()); diff --git a/core/runtime/src/store/mod.rs b/core/runtime/src/store/mod.rs index 68b8b699e8e..dd3c0d5687f 100644 --- a/core/runtime/src/store/mod.rs +++ b/core/runtime/src/store/mod.rs @@ -117,7 +117,6 @@ enum ValueStoreInner { SharedArrayBuffer(SharedArrayBuffer), /// Dataview. - #[expect(unused)] DataView { buffer: JsValueStore, byte_length: u64, @@ -217,3 +216,6 @@ impl JsValueStore { Ok(v) } } + +#[cfg(test)] +mod tests; diff --git a/core/runtime/src/store/tests.rs b/core/runtime/src/store/tests.rs new file mode 100644 index 00000000000..94f9ba5ca46 --- /dev/null +++ b/core/runtime/src/store/tests.rs @@ -0,0 +1,88 @@ +use super::JsValueStore; +use boa_engine::object::builtins::{JsArrayBuffer, JsDataView}; +use boa_engine::value::TryIntoJs; +use boa_engine::Context; + +#[test] +fn dataview_clone_non_zero_offset() { + let mut context = Context::default(); + + // Create an ArrayBuffer of 32 bytes + let array_buffer = JsArrayBuffer::new(32, &mut context).unwrap(); + + // Create DataView covering 16 bytes starting at offset 8 + let data_view = JsDataView::from_js_array_buffer( + array_buffer.clone(), + Some(8), + Some(16), + &mut context, + ) + .unwrap(); + + // Store + let store = JsValueStore::try_from_js(&data_view.into(), &mut context, vec![]).unwrap(); + + // Restore + let restored = store.try_into_js(&mut context).unwrap(); + + // Assertions + let restored_dv = JsDataView::from_object(restored.as_object().unwrap().clone()).unwrap(); + + assert_eq!(restored_dv.byte_offset(&mut context).unwrap(), 8); + assert_eq!(restored_dv.byte_length(&mut context).unwrap(), 16); + + let restored_buffer_val = restored_dv.buffer(&mut context).unwrap(); + let restored_buffer = + JsArrayBuffer::from_object(restored_buffer_val.as_object().unwrap().clone()).unwrap(); + + // The underlying buffer must be correctly cloned and sized to the original buffer size (32) + let buffer_len = restored_buffer.borrow().data().bytes().unwrap().len(); + assert_eq!(buffer_len, 32); + + // Also, it should NOT be the exact same buffer instance + assert_ne!( + std::ptr::from_ref(array_buffer.as_object().as_ref()).addr(), + std::ptr::from_ref(restored_buffer.as_object().as_ref()).addr() + ); +} + +#[test] +fn dataview_transfer() { + let mut context = Context::default(); + + // Create an ArrayBuffer of 32 bytes + let array_buffer = JsArrayBuffer::new(32, &mut context).unwrap(); + + // Create DataView + let data_view = JsDataView::from_js_array_buffer( + array_buffer.clone(), + Some(4), + Some(8), + &mut context, + ) + .unwrap(); + + // Store, but transfer the ArrayBuffer + let store = JsValueStore::try_from_js( + &data_view.into(), + &mut context, + vec![array_buffer.clone().into()], + ) + .unwrap(); + + // Original buffer should be detached + assert!(array_buffer.borrow().data().bytes().is_none()); + + // Restore + let restored = store.try_into_js(&mut context).unwrap(); + + let restored_dv = JsDataView::from_object(restored.as_object().unwrap().clone()).unwrap(); + assert_eq!(restored_dv.byte_offset(&mut context).unwrap(), 4); + assert_eq!(restored_dv.byte_length(&mut context).unwrap(), 8); + + let restored_buffer_val = restored_dv.buffer(&mut context).unwrap(); + let restored_buffer = + JsArrayBuffer::from_object(restored_buffer_val.as_object().unwrap().clone()).unwrap(); + let buffer_len = restored_buffer.borrow().data().bytes().unwrap().len(); + assert_eq!(buffer_len, 32); +} diff --git a/core/runtime/tests/clone/dataview.js b/core/runtime/tests/clone/dataview.js new file mode 100644 index 00000000000..59e819b371e --- /dev/null +++ b/core/runtime/tests/clone/dataview.js @@ -0,0 +1,54 @@ +// https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone + +{ + // Test basic DataView cloning + const buffer = new ArrayBuffer(16); + const dataview = new DataView(buffer); + dataview.setInt8(0, 42); + + const clone = structuredClone(dataview); + + assertNEq(clone, dataview); + assertNEq(clone.buffer, dataview.buffer); + assertEq(clone.byteLength, 16); + assertEq(clone.byteOffset, 0); + assertEq(clone.getInt8(0), 42); + + // modifying clone doesn't affect original + clone.setInt8(0, 10); + assertEq(dataview.getInt8(0), 42); +} + +{ + // Test DataView with byteOffset and byteLength + const buffer = new ArrayBuffer(32); + const dataview = new DataView(buffer, 8, 16); + dataview.setInt16(0, 1234); + + const clone = structuredClone(dataview); + + assertNEq(clone, dataview); + assertNEq(clone.buffer, dataview.buffer); + assertEq(clone.byteLength, 16); + assertEq(clone.byteOffset, 8); + assertEq(clone.getInt16(0), 1234); +} + +{ + // Test transferring the underlying buffer of a DataView + const buffer = new ArrayBuffer(16); + const dataview = new DataView(buffer, 4, 8); + dataview.setInt32(0, 98765); + + const object1 = { + dataview, + }; + + const object2 = structuredClone(object1, { transfer: [buffer] }); + + assert(object2.dataview !== dataview); + assertEq(object1.dataview.byteLength, 0); // Original dataview is detached + assertEq(object2.dataview.byteLength, 8); + assertEq(object2.dataview.byteOffset, 4); + assertEq(object2.dataview.getInt32(0), 98765); +}