From 52af848eae9fd801bd2ed795f250f4fabe84c8ab Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 1 Sep 2026 17:02:42 +0800 Subject: [PATCH] fix(indexeddb): clone ImageData and geometry objects --- .../indexed_db/core/clone.rs | 37 ++++- .../src/script_vm/tests/indexed_db.rs | 146 ++++++++++++++++++ moli-renderer-v8/src/structured_clone.rs | 12 +- 3 files changed, 183 insertions(+), 12 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs index 84d44637d7..3e59cfe0ce 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs @@ -2,16 +2,19 @@ use super::*; use crate::{ context_bootstrap::{ FileSystemHandleDurablePayload, build_file_list_object, - build_file_system_handle_from_durable_payload, file_list_files_from_object, - file_system_handle_clone_payload_from_object, - file_system_handle_durable_payload_from_object, is_file_list_object, + build_file_system_handle_from_durable_payload, build_geometry_object_from_clone_payload, + file_list_files_from_object, file_system_handle_clone_payload_from_object, + file_system_handle_durable_payload_from_object, geometry_clone_payload_from_object, + image_data_clone_payload_from_object, is_file_list_object, }, dom::native::SelectedFile, structured_clone::{ BlobClonePayload, HOST_OBJECT_TAG_BLOB, HOST_OBJECT_TAG_CRYPTO_KEY, - HOST_OBJECT_TAG_FILE_LIST, HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE, - blob_clone_payload_from_object, build_blob_object_from_clone_payload, - read_crypto_key_payload, write_crypto_key_payload, + HOST_OBJECT_TAG_FILE_LIST, HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE, HOST_OBJECT_TAG_GEOMETRY, + HOST_OBJECT_TAG_IMAGE_DATA, blob_clone_payload_from_object, + build_blob_object_from_clone_payload, read_crypto_key_payload, read_geometry_clone_payload, + read_image_data_payload, write_crypto_key_payload, write_geometry_clone_payload, + write_image_data_payload, }, }; use moli_indexeddb::{IndexedDbFileSystemHandleBucket, IndexedDbFileSystemHandleKind}; @@ -88,6 +91,8 @@ impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer { ) -> Option { Some( crate::context_bootstrap::is_crypto_key_object(scope, object) + || image_data_clone_payload_from_object(scope, object).is_some() + || geometry_clone_payload_from_object(scope, object).is_some() || crate::blob::is_blob_object(scope, object) || is_file_list_object(scope, object) || file_system_handle_clone_payload_from_object(scope, object).is_some() @@ -104,6 +109,14 @@ impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer { if write_crypto_key_payload(scope, object, serializer).is_some() { return Some(true); } + if let Some(payload) = image_data_clone_payload_from_object(scope, object) { + write_image_data_payload(serializer, payload); + return Some(true); + } + if let Some(payload) = geometry_clone_payload_from_object(scope, object) { + write_geometry_clone_payload(serializer, payload); + return Some(true); + } if is_file_list_object(scope, object) { let Some(files) = file_list_files_from_object(scope, object) else { let exception = dom_exception_value( @@ -245,6 +258,7 @@ impl v8::ValueDeserializerImpl for IndexedDbStructuredCloneDeserializer { return None; } match tag { + HOST_OBJECT_TAG_IMAGE_DATA => read_image_data_payload(scope, deserializer), HOST_OBJECT_TAG_CRYPTO_KEY => { read_crypto_key_payload(scope, deserializer).or_else(|| { let exception = dom_exception_value( @@ -256,6 +270,17 @@ impl v8::ValueDeserializerImpl for IndexedDbStructuredCloneDeserializer { None }) } + HOST_OBJECT_TAG_GEOMETRY => read_geometry_clone_payload(deserializer) + .map(|payload| build_geometry_object_from_clone_payload(scope, payload)) + .or_else(|| { + let exception = dom_exception_value( + scope, + "Failed to deserialize IndexedDB Geometry object.", + "DataCloneError", + ); + scope.throw_exception(exception); + None + }), HOST_OBJECT_TAG_BLOB => { let mut index = 0; if !deserializer.read_uint32(&mut index) { diff --git a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs index 739612a630..4cab6cbe12 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -1263,6 +1263,152 @@ fn indexed_db_roundtrips_file_list_with_file_graph_identity() { ); } +#[test] +fn indexed_db_roundtrips_image_data_and_geometry_objects() { + let mut vm = + new_storage_page_task_executor_test_vm("https://indexeddb-image-data-geometry.test/"); + + vm.eval( + r#" +(() => { + globalThis.__indexedDbGeometryResult = "pending"; + const dbName = `image-data-geometry-${Math.random()}`; + const open = indexedDB.open(dbName, 1); + open.onerror = () => { + globalThis.__indexedDbGeometryResult = `open-error:${open.error && open.error.name}`; + }; + open.onupgradeneeded = () => open.result.createObjectStore("values"); + open.onsuccess = () => { + const image = new ImageData( + new Uint8ClampedArray([1, 2, 3, 4, 5, 6, 7, 8]), + 2, + 1, + { colorSpace: "display-p3" } + ); + const pointReadOnly = new DOMPointReadOnly(1, -0, Infinity, NaN); + const point = new DOMPoint(2, 3, 4, 5); + const rectReadOnly = new DOMRectReadOnly(6, 7, 8, 9); + const rect = new DOMRect(10, 11, 12, 13); + const quad = new DOMQuad( + { x: 1, y: 2, z: 3, w: 4 }, + { x: 5, y: 6, z: 7, w: 8 }, + { x: 9, y: 10, z: 11, w: 12 }, + { x: 13, y: 14, z: 15, w: 16 } + ); + const matrixReadOnly = new DOMMatrixReadOnly([1, 2, 3, 4, 5, 6]); + const matrix = new DOMMatrix([ + 11, 12, 13, 14, + 21, 22, 23, 24, + 31, 32, 33, 34, + 41, 42, 43, 44 + ]); + for (const value of [ + image, + pointReadOnly, + point, + rectReadOnly, + rect, + quad, + matrixReadOnly, + matrix + ]) { + value.expando = "not serialized"; + } + + const write = open.result.transaction("values", "readwrite"); + write.objectStore("values").put({ + image, + imageAlias: image, + pointReadOnly, + point, + rectReadOnly, + rect, + quad, + matrixReadOnly, + matrix + }, "value"); + write.oncomplete = () => { + const read = open.result.transaction("values").objectStore("values").get("value"); + read.onerror = () => { + globalThis.__indexedDbGeometryResult = + `read-error:${read.error && read.error.name}`; + }; + read.onsuccess = () => { + const row = read.result; + const exactBrand = (value, name) => + Object.getPrototypeOf(value) === self[name].prototype; + const sameValues = (value, names, expected) => + names.every((name, index) => Object.is(value[name], expected[index])); + globalThis.__indexedDbGeometryResult = JSON.stringify({ + image: [ + exactBrand(row.image, "ImageData"), + row.image !== image, + row.image === row.imageAlias, + row.image.width, + row.image.height, + row.image.colorSpace, + Array.from(row.image.data).join(","), + row.image.data !== image.data + ], + pointReadOnly: + exactBrand(row.pointReadOnly, "DOMPointReadOnly") && + sameValues(row.pointReadOnly, ["x", "y", "z", "w"], [1, -0, Infinity, NaN]), + point: + exactBrand(row.point, "DOMPoint") && + sameValues(row.point, ["x", "y", "z", "w"], [2, 3, 4, 5]), + rectReadOnly: + exactBrand(row.rectReadOnly, "DOMRectReadOnly") && + sameValues(row.rectReadOnly, ["x", "y", "width", "height"], [6, 7, 8, 9]), + rect: + exactBrand(row.rect, "DOMRect") && + sameValues(row.rect, ["x", "y", "width", "height"], [10, 11, 12, 13]), + quad: + exactBrand(row.quad, "DOMQuad") && + [row.quad.p1, row.quad.p2, row.quad.p3, row.quad.p4] + .every((pointValue, index) => + sameValues( + pointValue, + ["x", "y", "z", "w"], + [1 + index * 4, 2 + index * 4, 3 + index * 4, 4 + index * 4] + )), + matrixReadOnly: + exactBrand(row.matrixReadOnly, "DOMMatrixReadOnly") && + sameValues(row.matrixReadOnly, ["a", "b", "c", "d", "e", "f", "is2D"], + [1, 2, 3, 4, 5, 6, true]), + matrix: + exactBrand(row.matrix, "DOMMatrix") && + sameValues(row.matrix, ["m11", "m12", "m13", "m14", "m41", "m42", "m43", "m44", "is2D"], + [11, 12, 13, 14, 41, 42, 43, 44, false]), + expandosExcluded: [ + row.image, + row.pointReadOnly, + row.point, + row.rectReadOnly, + row.rect, + row.quad, + row.matrixReadOnly, + row.matrix + ].every(value => value.expando === undefined) + }); + }; + }; + }; + return "scheduled"; +})() +"#, + ) + .expect("indexeddb ImageData/Geometry workflow should schedule"); + + let result = vm + .eval_after_selected_page_tasks("String(globalThis.__indexedDbGeometryResult)") + .expect("indexeddb ImageData/Geometry result should be readable"); + + assert_eq!( + result, + r#"{"image":[true,true,true,2,1,"display-p3","1,2,3,4,5,6,7,8",true],"pointReadOnly":true,"point":true,"rectReadOnly":true,"rect":true,"quad":true,"matrixReadOnly":true,"matrix":true,"expandosExcluded":true}"# + ); +} + #[test] fn indexed_db_roundtrips_opfs_handles_with_durable_external_objects() { let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-opfs-handle.test/"); diff --git a/moli-renderer-v8/src/structured_clone.rs b/moli-renderer-v8/src/structured_clone.rs index 76c80aa498..c1813eb601 100644 --- a/moli-renderer-v8/src/structured_clone.rs +++ b/moli-renderer-v8/src/structured_clone.rs @@ -39,7 +39,7 @@ pub(crate) use moli_structured_clone::{ use v8::{ValueDeserializerHelper, ValueSerializerHelper}; const HOST_OBJECT_TAG_MESSAGE_PORT: u32 = 1; -const HOST_OBJECT_TAG_IMAGE_DATA: u32 = 2; +pub(crate) const HOST_OBJECT_TAG_IMAGE_DATA: u32 = 2; pub(crate) const HOST_OBJECT_TAG_CRYPTO_KEY: u32 = 3; const HOST_OBJECT_TAG_READABLE_STREAM: u32 = 4; pub(crate) const HOST_OBJECT_TAG_BLOB: u32 = 5; @@ -48,7 +48,7 @@ pub(crate) const HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE: u32 = 7; const HOST_OBJECT_TAG_QUOTA_EXCEEDED_ERROR: u32 = 8; const HOST_OBJECT_TAG_WRITABLE_STREAM: u32 = 9; const HOST_OBJECT_TAG_TRANSFORM_STREAM: u32 = 10; -const HOST_OBJECT_TAG_GEOMETRY: u32 = 11; +pub(crate) const HOST_OBJECT_TAG_GEOMETRY: u32 = 11; pub(crate) const HOST_OBJECT_TAG_FILE_LIST: u32 = 12; const GEOMETRY_KIND_DOM_POINT_READONLY: u32 = 0; @@ -872,7 +872,7 @@ pub(crate) fn build_blob_object_from_clone_payload<'s>( } } -fn write_image_data_payload( +pub(crate) fn write_image_data_payload( serializer: &dyn v8::ValueSerializerHelper, payload: ImageDataClonePayload, ) { @@ -883,7 +883,7 @@ fn write_image_data_payload( write_raw_vec(serializer, &payload.bytes); } -fn read_image_data_payload<'s>( +pub(crate) fn read_image_data_payload<'s>( scope: &mut v8::PinScope<'s, '_>, deserializer: &dyn v8::ValueDeserializerHelper, ) -> Option> { @@ -911,7 +911,7 @@ fn read_image_data_payload<'s>( ) } -fn write_geometry_clone_payload( +pub(crate) fn write_geometry_clone_payload( serializer: &dyn v8::ValueSerializerHelper, payload: GeometryClonePayload, ) { @@ -964,7 +964,7 @@ fn write_geometry_clone_payload( } } -fn read_geometry_clone_payload( +pub(crate) fn read_geometry_clone_payload( deserializer: &dyn v8::ValueDeserializerHelper, ) -> Option { let kind = read_u32(deserializer)?;