fix(geometry): support structured cloning

This commit is contained in:
ldm0
2026-10-02 03:27:49 +08:00
parent 9808b1a6ce
commit e8dbea7f80
12 changed files with 528 additions and 6 deletions
@@ -2467,7 +2467,6 @@ css/cssom/getComputedStyle-layout-dependent-replaced-into-ib-split.html
css/cssom/idlharness.html
css/cssom/style-sheet-interfaces-001.html
css/geometry/DOMMatrix2DInit-validate-fixup.html
css/geometry/structured-serialization.html
css/mediaqueries/at-custom-media-cssom.html
css/mediaqueries/at-custom-media-parsing.html
css/mediaqueries/display-mode.html
@@ -3367,6 +3367,7 @@ css/geometry/DOMRectList.html
css/geometry/WebKitCSSMatrix.html
css/geometry/historical.html
css/geometry/spec-examples.html
css/geometry/structured-serialization.html
css/mediaqueries/aspect-ratio-serialization.html
css/mediaqueries/duplicate-media-stylesheet-crash.html
css/mediaqueries/dynamic-range.html
@@ -24,6 +24,7 @@ mod events;
pub(crate) mod exposed_interfaces;
mod file_api;
mod form_data_runtime;
mod geometry_clone;
mod geometry_runtime;
mod history_mutation;
mod history_runtime;
@@ -273,6 +274,10 @@ pub(crate) use self::file_api::{
flush_one_pending_file_reader, is_file_list_object, selected_file_from_object,
};
pub(crate) use self::form_data_runtime::form_data_request_body;
pub(crate) use self::geometry_clone::{
GeometryClonePayload, build_geometry_object_from_clone_payload,
geometry_clone_payload_from_object,
};
use self::geometry_runtime::{build_dom_point_object, optional_dom_point_init_arg};
pub(crate) use self::image_data::{
ImageDataClonePayload, build_image_data_object_from_clone_payload,
@@ -24,8 +24,8 @@ use super::super::{
},
form_data_runtime::build_form_data_constructor_template,
geometry_runtime::{
dom_matrix_constructor_callback, dom_point_constructor_callback,
dom_point_readonly_constructor_callback,
dom_matrix_constructor_callback, dom_matrix_readonly_constructor_callback,
dom_point_constructor_callback, dom_point_readonly_constructor_callback,
},
idle_detection::idle_detector_constructor_callback,
image_data::image_data_constructor_callback,
@@ -536,6 +536,14 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
.length(0)
.build(scope)
}
ConstructorKind::DomMatrixReadOnly => {
v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!(
web_api_interfaces::DOMMatrixReadOnly,
dom_matrix_readonly_constructor_callback
))
.length(0)
.build(scope)
}
ConstructorKind::DomMatrix => {
v8::FunctionTemplate::builder(dom_matrix_constructor_callback)
.length(0)
@@ -322,6 +322,27 @@ fn dom_quad_points<'s>(
])
}
pub(super) fn dom_quad_clone_data<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<[[f64; 4]; 4]> {
let points = dom_quad_points(scope, object)?;
Some(points.map(|point| {
let point = geometry_runtime::dom_point_init_from_object(scope, point);
[point.x, point.y, point.z, point.w]
}))
}
pub(super) fn build_dom_quad_clone_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
points: [[f64; 4]; 4],
) -> v8::Local<'s, v8::Object> {
build_dom_quad(
scope,
points.map(|[x, y, z, w]| geometry_runtime::DomPointInit { x, y, z, w }),
)
}
fn dom_quad_receiver_branded<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
@@ -561,6 +561,37 @@ fn dom_rect_receiver_branded<'s>(
web_api_interfaces::DOMRectReadOnly::is_instance(scope, receiver)
}
pub(super) fn dom_rect_clone_data<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<(bool, [f64; 4])> {
if !dom_rect_receiver_branded(scope, object) {
return None;
}
let mutable = web_api_interfaces::DOMRect::is_instance(scope, object);
Some((
mutable,
[
dom_rect_slot(object, scope, DOM_RECT_X_SLOT),
dom_rect_slot(object, scope, DOM_RECT_Y_SLOT),
dom_rect_slot(object, scope, DOM_RECT_WIDTH_SLOT),
dom_rect_slot(object, scope, DOM_RECT_HEIGHT_SLOT),
],
))
}
pub(super) fn build_dom_rect_clone_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
mutable: bool,
[x, y, width, height]: [f64; 4],
) -> v8::Local<'s, v8::Object> {
if mutable {
build_dom_rect_object(scope, x, y, width, height)
} else {
build_dom_rect_readonly_object(scope, x, y, width, height)
}
}
const DOM_RECT_WRITABLE_ATTRIBUTE_SLOTS: &[&str] = &[
DOM_RECT_X_SLOT,
DOM_RECT_Y_SLOT,
@@ -0,0 +1,65 @@
use super::{dom_quad, dom_rect, geometry_runtime};
#[derive(Clone, Copy, Debug)]
pub(crate) enum GeometryClonePayload {
Point {
mutable: bool,
values: [f64; 4],
},
Rect {
mutable: bool,
values: [f64; 4],
},
Quad {
points: [[f64; 4]; 4],
},
Matrix {
mutable: bool,
is_2d: bool,
values: [f64; 16],
},
}
pub(crate) fn geometry_clone_payload_from_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<GeometryClonePayload> {
if let Some((mutable, values)) = geometry_runtime::dom_point_clone_data(scope, object) {
return Some(GeometryClonePayload::Point { mutable, values });
}
if let Some((mutable, values)) = dom_rect::dom_rect_clone_data(scope, object) {
return Some(GeometryClonePayload::Rect { mutable, values });
}
if let Some(points) = dom_quad::dom_quad_clone_data(scope, object) {
return Some(GeometryClonePayload::Quad { points });
}
geometry_runtime::dom_matrix_clone_data(scope, object).map(|(mutable, is_2d, values)| {
GeometryClonePayload::Matrix {
mutable,
is_2d,
values,
}
})
}
pub(crate) fn build_geometry_object_from_clone_payload<'s>(
scope: &mut v8::PinScope<'s, '_>,
payload: GeometryClonePayload,
) -> v8::Local<'s, v8::Object> {
match payload {
GeometryClonePayload::Point { mutable, values } => {
geometry_runtime::build_dom_point_clone_object(scope, mutable, values)
}
GeometryClonePayload::Rect { mutable, values } => {
dom_rect::build_dom_rect_clone_object(scope, mutable, values)
}
GeometryClonePayload::Quad { points } => {
dom_quad::build_dom_quad_clone_object(scope, points)
}
GeometryClonePayload::Matrix {
mutable,
is_2d,
values,
} => geometry_runtime::build_dom_matrix_clone_object(scope, mutable, is_2d, values),
}
}
@@ -657,6 +657,28 @@ pub(super) fn dom_matrix_constructor_callback<'s>(
rv.set(args.this().into());
}
pub(super) fn dom_matrix_readonly_constructor_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !args.is_construct_call() {
throw_type_error(
scope,
"Failed to construct 'DOMMatrixReadOnly': Please use the 'new' operator.",
);
return;
}
initialize_dom_matrix_readonly_identity_object(scope, args.this());
if args.length() > 0
&& !args.get(0).is_undefined()
&& !apply_dom_matrix_init(scope, args.this(), args.get(0))
{
return;
}
rv.set(args.this().into());
}
pub(in crate::context_bootstrap) fn initialize_dom_point_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -1175,6 +1197,37 @@ fn dom_point_receiver_branded<'s>(
web_api_interfaces::DOMPointReadOnly::is_instance(scope, receiver)
}
pub(super) fn dom_point_clone_data<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<(bool, [f64; 4])> {
if !dom_point_receiver_branded(scope, object) {
return None;
}
let mutable = web_api_interfaces::DOMPoint::is_instance(scope, object);
Some((
mutable,
[
dom_point_slot(scope, object, DOM_POINT_X_SLOT, 0.0),
dom_point_slot(scope, object, DOM_POINT_Y_SLOT, 0.0),
dom_point_slot(scope, object, DOM_POINT_Z_SLOT, 0.0),
dom_point_slot(scope, object, DOM_POINT_W_SLOT, 1.0),
],
))
}
pub(super) fn build_dom_point_clone_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
mutable: bool,
[x, y, z, w]: [f64; 4],
) -> v8::Local<'s, v8::Object> {
if mutable {
build_dom_point_object(scope, x, y, z, w)
} else {
build_dom_point_readonly_object(scope, x, y, z, w)
}
}
fn dom_matrix_require_readonly_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
@@ -1197,6 +1250,107 @@ fn dom_matrix_require_mutable_receiver<'s>(
false
}
pub(super) fn dom_matrix_clone_data<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<(bool, bool, [f64; DOM_MATRIX_COMPONENT_COUNT])> {
if !web_api_interfaces::DOMMatrixReadOnly::is_instance(scope, object) {
return None;
}
let mutable = web_api_interfaces::DOMMatrix::is_instance(scope, object);
let is_2d = dom_matrix_is_2d(scope, object);
let mut components = dom_matrix_components(scope, object);
if is_2d {
components = DomMatrixComponents {
m11: components.m11,
m12: components.m12,
m21: components.m21,
m22: components.m22,
m41: components.m41,
m42: components.m42,
..DomMatrixComponents::identity()
};
}
Some((
mutable,
is_2d,
[
components.m11,
components.m12,
components.m13,
components.m14,
components.m21,
components.m22,
components.m23,
components.m24,
components.m31,
components.m32,
components.m33,
components.m34,
components.m41,
components.m42,
components.m43,
components.m44,
],
))
}
pub(super) fn build_dom_matrix_clone_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
mutable: bool,
is_2d: bool,
values: [f64; DOM_MATRIX_COMPONENT_COUNT],
) -> v8::Local<'s, v8::Object> {
let [
m11,
m12,
m13,
m14,
m21,
m22,
m23,
m24,
m31,
m32,
m33,
m34,
m41,
m42,
m43,
m44,
] = values;
let value = DomMatrixValue {
components: DomMatrixComponents {
m11,
m12,
m13,
m14,
m21,
m22,
m23,
m24,
m31,
m32,
m33,
m34,
m41,
m42,
m43,
m44,
},
is_2d,
};
if mutable {
DomMatrixObjectDeclaration::from_value(value)
.bind(scope)
.expect("DOMMatrix clone declaration should bind")
} else {
DomMatrixReadOnlyObjectDeclaration::from_value(value)
.bind(scope)
.expect("DOMMatrixReadOnly clone declaration should bind")
}
}
fn dom_matrix_to_json_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -2331,6 +2485,15 @@ fn initialize_dom_matrix_identity_object<'s>(
.expect("DOMMatrix declaration should initialize object");
}
fn initialize_dom_matrix_readonly_identity_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) {
DomMatrixReadOnlyObjectDeclaration::identity()
.initialize(scope, object)
.expect("DOMMatrixReadOnly declaration should initialize object");
}
fn dom_matrix_is_2d<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -1105,7 +1105,7 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[
},
ConstructorSpec {
interface: web_api_interfaces::DOMMatrixReadOnly::DESCRIPTOR,
kind: ConstructorKind::DomMatrix,
kind: ConstructorKind::DomMatrixReadOnly,
},
ConstructorSpec {
interface: web_api_interfaces::DOMMatrix::DESCRIPTOR,
@@ -116,6 +116,7 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
DomPointReadOnly,
DomPoint,
DomQuad,
DomMatrixReadOnly,
DomMatrix,
ResizeObserver,
PerformanceObserver,
@@ -323,6 +323,90 @@ fn structured_clone_preserves_dom_exception_fields_and_brand() {
);
}
#[test]
fn structured_clone_preserves_geometry_interfaces_and_internal_values() {
let mut vm = new_storage_test_vm("https://geometry-structured-clone.test/");
let result = vm
.eval(
r#"
(() => {
const same = (actual, expected) => Object.is(actual, expected);
const check = (name, source, attrs, expected, nested = false) => {
const originals = nested ? attrs.map(attr => source[attr]) : [];
source.expando = "not serialized";
for (const attr of attrs) {
Object.defineProperty(source, attr, {
get() { throw new Error(`unexpected ${name}.${attr} getter`); }
});
}
const clone = structuredClone(source);
const valuesMatch = attrs.every((attr, index) => {
if (!nested) return same(clone[attr], expected[index]);
return clone[attr] !== originals[index]
&& expected[index].every((value, component) =>
same(clone[attr][["x", "y", "z", "w"][component]], value));
});
return Object.prototype.toString.call(clone) === `[object ${name}]`
&& Object.getPrototypeOf(clone) === self[name].prototype
&& !("expando" in clone)
&& valuesMatch;
};
const pointAttrs = ["x", "y", "z", "w"];
const rectAttrs = ["x", "y", "width", "height"];
const quadAttrs = ["p1", "p2", "p3", "p4"];
const pointValues = [1, -0, Infinity, NaN];
const quadValues = [
[1, 2, 3, 4],
[-0, -0, -0, -0],
[Infinity, Infinity, Infinity, Infinity],
[NaN, NaN, NaN, NaN]
];
const matrixAttrs = [
"a", "b", "c", "d", "e", "f",
"m11", "m12", "m13", "m14",
"m21", "m22", "m23", "m24",
"m31", "m32", "m33", "m34",
"m41", "m42", "m43", "m44", "is2D"
];
const normalized2DAttrs = new Set([
"m13", "m14", "m23", "m24",
"m31", "m32", "m34", "m43"
]);
const matrixCheck = (name, values) => {
const source = new self[name](values);
if (values.length === 6) {
for (const attr of normalized2DAttrs) source[attr] = -0;
}
const expected = matrixAttrs.map(attr =>
normalized2DAttrs.has(attr) && values.length === 6 ? 0 : source[attr]);
return check(name, source, matrixAttrs, expected);
};
return [
check("DOMPointReadOnly", new DOMPointReadOnly(...pointValues),
pointAttrs, pointValues),
check("DOMPoint", new DOMPoint(...pointValues), pointAttrs, pointValues),
check("DOMRectReadOnly", new DOMRectReadOnly(...pointValues),
rectAttrs, pointValues),
check("DOMRect", new DOMRect(...pointValues), rectAttrs, pointValues),
check("DOMQuad", new DOMQuad(...quadValues.map(
([x, y, z, w]) => ({x, y, z, w}))), quadAttrs, quadValues, true),
matrixCheck("DOMMatrixReadOnly", [1, -0, Infinity, NaN, 5, 6]),
matrixCheck("DOMMatrix", [
11, -0, Infinity, NaN, 21, 22, 23, 24,
31, 32, 33, 34, 41, 42, 43, 44
])
].join("|");
})()
"#,
)
.expect("Geometry structuredClone probe should evaluate");
assert_eq!(result, "true|true|true|true|true|true|true");
}
#[test]
fn structured_clone_transfers_array_buffer_and_preserves_view_aliases() {
let mut vm = new_storage_test_vm("https://array-buffer-transfer.test/");
+146 -2
View File
@@ -9,8 +9,8 @@ use std::{
use crate::{
context_bootstrap::{
CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, FileSystemFileSnapshotClonePayload,
FileSystemHandleClonePayload, ImageDataClonePayload, ReadableStreamClonePayload,
TransformStreamClonePayload, WritableStreamClonePayload,
FileSystemHandleClonePayload, GeometryClonePayload, ImageDataClonePayload,
ReadableStreamClonePayload, TransformStreamClonePayload, WritableStreamClonePayload,
attach_file_system_file_snapshot_clone_payload, build_file_object,
build_file_system_handle_from_clone_payload, build_image_data_object_from_clone_payload,
build_readable_stream_clone_shell, build_transform_stream_clone_shell,
@@ -25,6 +25,7 @@ use crate::{
prepare_transform_stream_transfer, prepare_writable_stream_transfer,
quota_exceeded_error_clone_fields, require_internal_stream_value,
selected_file_from_object,
build_geometry_object_from_clone_payload, geometry_clone_payload_from_object,
},
dom::native::SelectedFile,
types::MessagePortId,
@@ -44,6 +45,15 @@ 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;
const GEOMETRY_KIND_DOM_POINT_READONLY: u32 = 0;
const GEOMETRY_KIND_DOM_POINT: u32 = 1;
const GEOMETRY_KIND_DOM_RECT_READONLY: u32 = 2;
const GEOMETRY_KIND_DOM_RECT: u32 = 3;
const GEOMETRY_KIND_DOM_QUAD: u32 = 4;
const GEOMETRY_KIND_DOM_MATRIX_READONLY: u32 = 5;
const GEOMETRY_KIND_DOM_MATRIX: u32 = 6;
#[derive(Clone, Debug, Default)]
pub(crate) struct V8StructuredClonePayload {
@@ -368,6 +378,12 @@ impl v8::ValueSerializerImpl for WireSerializer {
return Some(true);
}
}
Some("DOMPoint" | "DOMPointReadOnly" | "DOMRect" | "DOMRectReadOnly" | "DOMQuad" | "DOMMatrix" | "DOMMatrixReadOnly") => {
if let Some(payload) = geometry_clone_payload_from_object(scope, object) {
write_geometry_clone_payload(serializer, payload);
return Some(true);
}
}
Some("ImageData") => {
if let Some(payload) = image_data_clone_payload_from_object(scope, object) {
write_image_data_payload(serializer, payload);
@@ -593,6 +609,15 @@ impl v8::ValueDeserializerImpl for WireDeserializer {
None
})
}
HOST_OBJECT_TAG_GEOMETRY => read_geometry_clone_payload(deserializer)
.map(|payload| build_geometry_object_from_clone_payload(scope, payload))
.or_else(|| {
throw_data_clone_exception(
scope,
"Failed to deserialize structured clone Geometry object.",
);
None
}),
HOST_OBJECT_TAG_READABLE_STREAM => {
let clone_id = read_u32(deserializer)?;
let Some(payload) = self.readable_streams.get(&clone_id).cloned() else {
@@ -824,6 +849,125 @@ pub(crate) fn read_image_data_payload<'s>(
)
}
fn write_geometry_clone_payload(
serializer: &dyn v8::ValueSerializerHelper,
payload: GeometryClonePayload,
) {
serializer.write_uint32(HOST_OBJECT_TAG_GEOMETRY);
match payload {
GeometryClonePayload::Point { mutable, values } => {
serializer.write_uint32(if mutable {
GEOMETRY_KIND_DOM_POINT
} else {
GEOMETRY_KIND_DOM_POINT_READONLY
});
write_doubles(serializer, &values);
}
GeometryClonePayload::Rect { mutable, values } => {
serializer.write_uint32(if mutable {
GEOMETRY_KIND_DOM_RECT
} else {
GEOMETRY_KIND_DOM_RECT_READONLY
});
write_doubles(serializer, &values);
}
GeometryClonePayload::Quad { points } => {
serializer.write_uint32(GEOMETRY_KIND_DOM_QUAD);
for point in points {
write_doubles(serializer, &point);
}
}
GeometryClonePayload::Matrix {
mutable,
is_2d,
values,
} => {
serializer.write_uint32(if mutable {
GEOMETRY_KIND_DOM_MATRIX
} else {
GEOMETRY_KIND_DOM_MATRIX_READONLY
});
serializer.write_uint32(u32::from(is_2d));
if is_2d {
write_doubles(
serializer,
&[
values[0], values[1], values[4], values[5], values[12], values[13],
],
);
} else {
write_doubles(serializer, &values);
}
}
}
}
fn read_geometry_clone_payload(
deserializer: &dyn v8::ValueDeserializerHelper,
) -> Option<GeometryClonePayload> {
let kind = read_u32(deserializer)?;
match kind {
GEOMETRY_KIND_DOM_POINT_READONLY | GEOMETRY_KIND_DOM_POINT => {
Some(GeometryClonePayload::Point {
mutable: kind == GEOMETRY_KIND_DOM_POINT,
values: read_doubles(deserializer)?,
})
}
GEOMETRY_KIND_DOM_RECT_READONLY | GEOMETRY_KIND_DOM_RECT => {
Some(GeometryClonePayload::Rect {
mutable: kind == GEOMETRY_KIND_DOM_RECT,
values: read_doubles(deserializer)?,
})
}
GEOMETRY_KIND_DOM_QUAD => {
let mut points = [[0.0; 4]; 4];
for point in &mut points {
*point = read_doubles(deserializer)?;
}
Some(GeometryClonePayload::Quad { points })
}
GEOMETRY_KIND_DOM_MATRIX_READONLY | GEOMETRY_KIND_DOM_MATRIX => {
let is_2d = match read_u32(deserializer)? {
0 => false,
1 => true,
_ => return None,
};
let values = if is_2d {
let [m11, m12, m21, m22, m41, m42] = read_doubles(deserializer)?;
[
m11, m12, 0.0, 0.0, m21, m22, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, m41, m42, 0.0, 1.0,
]
} else {
read_doubles(deserializer)?
};
Some(GeometryClonePayload::Matrix {
mutable: kind == GEOMETRY_KIND_DOM_MATRIX,
is_2d,
values,
})
}
_ => None,
}
}
fn write_doubles(serializer: &dyn v8::ValueSerializerHelper, values: &[f64]) {
for value in values {
serializer.write_double(*value);
}
}
fn read_doubles<const N: usize>(
deserializer: &dyn v8::ValueDeserializerHelper,
) -> Option<[f64; N]> {
let mut values = [0.0; N];
for value in &mut values {
if !deserializer.read_double(value) {
return None;
}
}
Some(values)
}
pub(crate) fn write_crypto_key_payload<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,