fix(indexeddb): clone ImageData and geometry objects

This commit is contained in:
ldm0
2026-09-09 06:52:20 +08:00
parent c67603d28d
commit 52af848eae
3 changed files with 183 additions and 12 deletions
@@ -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<bool> {
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) {
@@ -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/");
+6 -6
View File
@@ -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<v8::Local<'s, v8::Object>> {
@@ -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<GeometryClonePayload> {
let kind = read_u32(deserializer)?;