feat(clone): serialize native DOMRect platform values

This commit is contained in:
ldm0
2026-10-04 04:07:26 +08:00
committed by Donough Liu
parent 98edb0848b
commit 5b631561a3
6 changed files with 176 additions and 8 deletions
+3 -1
View File
@@ -247,7 +247,9 @@ pub(crate) use self::css_stylesheet_runtime::{
sync_css_style_sheet_media_list_from_owner,
sync_css_style_sheet_shadow_root_adopted_owner_tracking,
};
pub(crate) use self::dom_rect::build_dom_rect_object;
pub(crate) use self::dom_rect::{
build_dom_rect_clone_object, build_dom_rect_object, dom_rect_clone_data,
};
pub(crate) use self::events::{
EVENT_DISPATCHING_SLOT, EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT,
EVENT_STOP_PROPAGATION_SLOT, clear_event_composed_path, event_attribute, event_backing,
@@ -561,6 +561,37 @@ fn dom_rect_receiver_branded<'s>(
web_api_interfaces::DOMRectReadOnly::is_instance(scope, receiver)
}
pub(crate) 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(crate) 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,56 @@
use super::*;
#[test]
fn structured_clone_preserves_native_rectangle_kind_values_and_graph_identity() {
let mut vm = new_storage_test_vm("https://rect-clone.test/");
assert_eq!(vm.eval(r#"
(() => {
const check = (ok, message) => { if (!ok) throw Error(message); };
if (!document.documentElement) document.appendChild(document.createElement('html'));
if (!document.body) document.documentElement.appendChild(document.createElement('body'));
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const other = iframe.contentWindow;
for (const realm of [window, other]) {
for (const name of ['DOMRect', 'DOMRectReadOnly']) {
const C = realm[name];
const prototype = C.prototype;
const values = [-0, NaN, Infinity, -Infinity];
const rect = new C(...values);
Object.defineProperty(rect, 'x', { enumerable: true, get() { throw Error('public x must not be read'); } });
Object.defineProperty(rect, 'toJSON', { enumerable: true, get() { throw Error('toJSON must not be read'); } });
const subclass = new (class extends C {})(1, 2, -3, -4);
realm[name] = function PoisonedConstructor() { throw Error('public constructor must not be called'); };
try {
const graph = realm.structuredClone({ rect, again: rect, list: [rect], subclass });
const clone = graph.rect;
check(clone !== rect && graph.again === clone && graph.list[0] === clone, name + ' cloned graph identity');
check(Object.getPrototypeOf(clone) === prototype, name + ' realm-local native prototype');
check(Object.keys(clone).length === 0, name + ' ignores expandos');
for (const [i, key] of ['x', 'y', 'width', 'height'].entries())
check(Object.is(clone[key], values[i]), name + ' preserves ' + key);
check(Object.getPrototypeOf(graph.subclass) === prototype, name + ' strips author subclass prototype');
check(graph.subclass.x === 1 && graph.subclass.y === 2 && graph.subclass.width === -3 && graph.subclass.height === -4, name + ' subclass native state');
if (name === 'DOMRect') {
clone.x = 10;
check(clone.x === 10, name + ' mutable clone');
} else {
check(Reflect.set(clone, 'x', 10) === false && Object.is(clone.x, -0), name + ' readonly clone');
}
} finally { realm[name] = C; }
}
}
const source = new other.DOMRect(5, 6, -7, -8);
const cross = structuredClone(source);
check(Object.getPrototypeOf(cross) === DOMRect.prototype && cross.x === 5 && cross.width === -7, 'cross-realm reconstruction');
let traps = 0;
let error;
try { structuredClone(new Proxy(source, { ownKeys() { traps++; return []; } })); } catch (caught) { error = caught; }
check(error instanceof DOMException && error.name === 'DataCloneError' && traps === 0, 'author Proxy rejection');
history.replaceState({ rect: new DOMRectReadOnly(-0, 2, -3, 4) }, '', '#rectangle');
const stored = history.state.rect;
check(Object.getPrototypeOf(stored) === DOMRectReadOnly.prototype && Object.is(stored.x, -0) && stored.width === -3, 'history storage reconstruction');
return 'passed';
})()
"#).unwrap(), "passed");
}
@@ -2193,3 +2193,4 @@ mod observer_element_arguments;
mod media_device_interfaces;
mod dom_rect_factory_descriptors;
mod dom_rect_structured_clone;
+44 -7
View File
@@ -11,13 +11,14 @@ use crate::{
CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, FileSystemFileSnapshotClonePayload,
FileSystemHandleClonePayload, 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,
build_writable_stream_clone_shell, crypto_key_clone_payload_from_object,
crypto_key_object_from_clone_payload, detach_message_port_owner_for_transfer,
detach_transferred_message_port, dom_exception_clone_fields,
ensure_message_port_wrapper_for_id, file_system_file_snapshot_clone_payload_from_object,
attach_file_system_file_snapshot_clone_payload, build_dom_rect_clone_object,
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, build_writable_stream_clone_shell,
crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload,
detach_message_port_owner_for_transfer, detach_transferred_message_port,
dom_exception_clone_fields, dom_rect_clone_data, ensure_message_port_wrapper_for_id,
file_system_file_snapshot_clone_payload_from_object,
file_system_handle_clone_payload_from_object, image_data_clone_payload_from_object,
initialize_readable_stream_clone_shell, initialize_transform_stream_clone_shell,
initialize_writable_stream_clone_shell, message_port_id_from_object,
@@ -44,6 +45,9 @@ 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_RECT_READONLY: u32 = 2;
const GEOMETRY_KIND_DOM_RECT: u32 = 3;
#[derive(Clone, Debug, Default)]
pub(crate) struct V8StructuredClonePayload {
@@ -368,6 +372,20 @@ impl v8::ValueSerializerImpl for WireSerializer {
return Some(true);
}
}
Some("DOMRect" | "DOMRectReadOnly") => {
if let Some((mutable, values)) = dom_rect_clone_data(scope, object) {
serializer.write_uint32(HOST_OBJECT_TAG_GEOMETRY);
serializer.write_uint32(if mutable {
GEOMETRY_KIND_DOM_RECT
} else {
GEOMETRY_KIND_DOM_RECT_READONLY
});
for value in values {
serializer.write_double(value);
}
return Some(true);
}
}
Some("ImageData") => {
if let Some(payload) = image_data_clone_payload_from_object(scope, object) {
write_image_data_payload(serializer, payload);
@@ -583,6 +601,25 @@ impl v8::ValueDeserializerImpl for WireDeserializer {
}
ensure_message_port_wrapper_for_id(scope, port_id)
}
HOST_OBJECT_TAG_GEOMETRY => {
let kind = read_u32(deserializer)?;
let mutable = match kind {
GEOMETRY_KIND_DOM_RECT_READONLY => false,
GEOMETRY_KIND_DOM_RECT => true,
_ => {
throw_data_clone_exception(scope, "Unsupported geometry clone kind.");
return None;
}
};
let mut values = [0.0; 4];
for value in &mut values {
if !deserializer.read_double(value) {
throw_data_clone_exception(scope, "Invalid DOMRect clone payload.");
return None;
}
}
Some(build_dom_rect_clone_object(scope, mutable, values))
}
HOST_OBJECT_TAG_IMAGE_DATA => read_image_data_payload(scope, deserializer),
HOST_OBJECT_TAG_CRYPTO_KEY => {
read_crypto_key_payload(scope, deserializer).or_else(|| {
@@ -3028,3 +3028,44 @@ async fn worker_does_not_expose_window_media_device_interfaces() {
r#"{"MediaDeviceInfo":false,"InputDeviceInfo":false}"#
);
}
#[tokio::test]
async fn worker_rectangle_clones_use_native_prototypes_and_message_channel_delivery() {
ensure_v8();
let mut handle = spawn_worker(
r#"
(async () => {
const check = (ok, label) => { if (!ok) throw Error(label); };
for (const name of ['DOMRect', 'DOMRectReadOnly']) {
const C = self[name];
const prototype = C.prototype;
const rect = new C(-0, NaN, Infinity, -Infinity);
Object.defineProperty(rect, 'x', {get() { throw Error('must read native state'); }});
self[name] = () => { throw Error('must not call public constructor'); };
try {
const clone = structuredClone({rect, again: rect});
check(clone.rect === clone.again, name + ' graph alias');
check(Object.getPrototypeOf(clone.rect) === prototype, name + ' intrinsic prototype');
check(Object.is(clone.rect.x, -0) && Number.isNaN(clone.rect.y) &&
clone.rect.width === Infinity && clone.rect.height === -Infinity, name + ' values');
const channel = new MessageChannel();
const received = new Promise(resolve => { channel.port2.onmessage = event => resolve(event.data); });
channel.port1.postMessage({rect, again: rect});
const message = await received;
check(message.rect === message.again && message.rect !== rect, name + ' message graph');
check(Object.getPrototypeOf(message.rect) === prototype, name + ' message receiver prototype');
check(Object.is(message.rect.x, -0) && Number.isNaN(message.rect.y) &&
message.rect.width === Infinity && message.rect.height === -Infinity, name + ' message values');
channel.port1.close();
channel.port2.close();
} finally { self[name] = C; }
}
postMessage('passed');
close();
})().catch(error => { postMessage(String(error)); close(); });
"#
.into(),
"https://worker-rectangle.test/clone.js".into(),
);
assert_eq!(recv_post_json(&mut handle).await, r#""passed""#);
}