refactor(webapi): complete identity migration and codec dispatch

This commit is contained in:
ldm0
2026-09-11 12:19:11 +08:00
parent fc2576808d
commit a332176903
38 changed files with 721 additions and 377 deletions
+1 -17
View File
@@ -6,7 +6,6 @@ use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
use std::sync::{Arc, OnceLock};
use super::{
native_bridge,
resource_owner::{ResourceOwnerId, current_resource_owner_id},
runtime::RendererStoragePartitionIdentity,
util::{get_private_value, set_private_value, throw_type_error, v8_string},
@@ -740,22 +739,7 @@ fn blob_platform_indexed_object_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<BlobPlatformIndexedObjectKind> {
if native_bridge::blob_parts_platform_collection_kind(scope, object).is_some() {
return Some(BlobPlatformIndexedObjectKind::Collection);
}
if let Ok((runtime_ptr, handle)) =
native_bridge::node_runtime_and_handle_from_object(scope, object)
&& unsafe { &*runtime_ptr }
.dom_host()
.is_html_element_named(handle, "select")
{
return Some(BlobPlatformIndexedObjectKind::HtmlSelectElement);
}
match object
.get_constructor_name()
.to_rust_string_lossy(scope)
.as_str()
{
match moli_webapi_declare::web_api_object_type(scope, object)?.name() {
"NamedNodeMap" => Some(BlobPlatformIndexedObjectKind::NamedNodeMap),
"FileList" => Some(BlobPlatformIndexedObjectKind::FileList),
"DOMStringList" => Some(BlobPlatformIndexedObjectKind::DomStringList),
@@ -421,7 +421,6 @@ pub(crate) use self::streams::{
build_readable_stream_clone_shell, build_transform_stream_clone_shell,
build_writable_stream_clone_shell, initialize_readable_stream_clone_shell,
initialize_transform_stream_clone_shell, initialize_writable_stream_clone_shell,
is_readable_stream_object, is_transform_stream_object, is_writable_stream_object,
new_readable_stream_from_array_buffer, new_readable_stream_from_source,
prepare_readable_stream_transfer, prepare_transform_stream_transfer,
prepare_writable_stream_transfer,
@@ -788,7 +788,12 @@ pub(crate) fn webgl_get_shader_precision_format_callback(
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", data_properties, enumerable)]
#[webapi(
interface = "WebGLShaderPrecisionFormat",
prototype = "Object",
data_properties,
enumerable
)]
struct WebGlShaderPrecisionFormat {
precision: i32,
#[webapi(data_property = "rangeMin")]
@@ -280,7 +280,7 @@ struct PointerEventTemplateMethodsDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "EventTarget", enumerable)]
#[webapi(name = "EventTarget", enumerable, receiver = "EventTarget")]
struct EventTargetTemplateMethodsDeclaration {
#[webapi(
method = "addEventListener",
@@ -222,15 +222,13 @@ pub(crate) fn selected_file_from_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<SelectedFile> {
let bytes = blob::blob_bytes_from_object(scope, object)?;
let mime_type = blob::blob_mime_type_from_object(scope, object).unwrap_or_default();
let name_value = object.get(scope, v8str(scope, "name").into())?;
if name_value.is_null_or_undefined() {
if !moli_webapi_declare::implements_interface(scope, object, "File") {
return None;
}
let name = name_value.to_string(scope)?.to_rust_string_lossy(scope);
let last_modified = object
.get(scope, v8str(scope, "lastModified").into())
let bytes = blob::blob_bytes_from_object(scope, object)?;
let mime_type = blob::blob_mime_type_from_object(scope, object).unwrap_or_default();
let name = file_name_from_object(scope, object)?;
let last_modified = get_private_value(scope, object, FILE_LAST_MODIFIED_SLOT)
.and_then(|value| value.number_value(scope))
.filter(|value| value.is_finite())
.unwrap_or_else(unix_epoch_millis);
@@ -3,7 +3,7 @@ use moli_webapi_declare::WebApiObject;
#[derive(WebApiObject)]
#[webapi(
interface = "Object",
interface = "ProgressEvent", prototype = "Object",
scope_lifetime = 'scope,
data_properties,
enumerable
@@ -48,52 +48,61 @@ impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer {
object: v8::Local<'s, v8::Object>,
serializer: &dyn v8::ValueSerializerHelper,
) -> Option<bool> {
if write_crypto_key_payload(scope, object, serializer).is_some() {
return Some(true);
}
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
let mut external_objects = self.external_objects.borrow_mut();
let Ok(index) = u32::try_from(external_objects.len()) else {
drop(external_objects);
let exception = dom_exception_value(
scope,
"Too many external objects in IndexedDB structured clone.",
"DataCloneError",
);
scope.throw_exception(exception);
return None;
};
external_objects.push(indexed_db_external_object_from_blob_payload(payload));
serializer.write_uint32(HOST_OBJECT_TAG_BLOB);
serializer.write_uint32(index);
return Some(true);
}
if file_system_handle_clone_payload_from_object(scope, object).is_some() {
let Some(payload) = file_system_handle_durable_payload_from_object(scope, object)
else {
let exception = dom_exception_value(
scope,
"FileSystemHandle is not authorized for this IndexedDB storage scope.",
"DataCloneError",
);
scope.throw_exception(exception);
return None;
};
let mut external_objects = self.external_objects.borrow_mut();
let Ok(index) = u32::try_from(external_objects.len()) else {
drop(external_objects);
let exception = dom_exception_value(
scope,
"Too many external objects in IndexedDB structured clone.",
"DataCloneError",
);
scope.throw_exception(exception);
return None;
};
external_objects.push(indexed_db_external_object_from_file_system_handle(payload));
serializer.write_uint32(HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE);
serializer.write_uint32(index);
return Some(true);
match moli_webapi_declare::web_api_object_type(scope, object).map(|kind| kind.name()) {
Some("CryptoKey") => {
if write_crypto_key_payload(scope, object, serializer).is_some() {
return Some(true);
}
}
Some("Blob" | "File") => {
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
let mut external_objects = self.external_objects.borrow_mut();
let Ok(index) = u32::try_from(external_objects.len()) else {
drop(external_objects);
let exception = dom_exception_value(
scope,
"Too many external objects in IndexedDB structured clone.",
"DataCloneError",
);
scope.throw_exception(exception);
return None;
};
external_objects.push(indexed_db_external_object_from_blob_payload(payload));
serializer.write_uint32(HOST_OBJECT_TAG_BLOB);
serializer.write_uint32(index);
return Some(true);
}
}
Some("FileSystemFileHandle" | "FileSystemDirectoryHandle")
if file_system_handle_clone_payload_from_object(scope, object).is_some() =>
{
let Some(payload) = file_system_handle_durable_payload_from_object(scope, object)
else {
let exception = dom_exception_value(
scope,
"FileSystemHandle is not authorized for this IndexedDB storage scope.",
"DataCloneError",
);
scope.throw_exception(exception);
return None;
};
let mut external_objects = self.external_objects.borrow_mut();
let Ok(index) = u32::try_from(external_objects.len()) else {
drop(external_objects);
let exception = dom_exception_value(
scope,
"Too many external objects in IndexedDB structured clone.",
"DataCloneError",
);
scope.throw_exception(exception);
return None;
};
external_objects.push(indexed_db_external_object_from_file_system_handle(payload));
serializer.write_uint32(HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE);
serializer.write_uint32(index);
return Some(true);
}
_ => {}
}
let exception = dom_exception_value(
scope,
@@ -3,7 +3,12 @@ use crate::util::set_private_value;
use moli_webapi_declare::WebApiObject;
#[derive(WebApiObject)]
#[webapi(interface = "EventTarget", prototype = "Object", enumerable)]
#[webapi(
interface = "EventTarget",
prototype = "Object",
enumerable,
receiver = "EventTarget"
)]
struct SimpleEventTargetMethodsDeclaration<'scope> {
#[webapi(method, length = 2, callback = simple_event_target_add_event_listener_callback)]
add_event_listener: (),
@@ -803,7 +803,8 @@ struct FileSystemWritableFileStreamPrototypeDeclaration {
}
#[derive(WebApiObject)]
#[webapi(prototype = "Object", interface = "FileSystemWritableSink")]
// An internal UnderlyingSink record, not a Web IDL interface instance.
#[webapi(interface = "Object")]
struct FileSystemWritableSinkObjectDeclaration {
#[webapi(slot = FILE_SYSTEM_WRITABLE_SINK_STATE_SLOT)]
state_json: String,
@@ -3168,9 +3169,8 @@ fn writable_sink_state<'s>(
scope: &mut v8::PinScope<'s, '_>,
sink: v8::Local<'s, v8::Object>,
) -> Option<FileSystemWritableSinkState> {
if !moli_webapi_declare::implements_interface(scope, sink, "FileSystemWritableSink") {
return None;
}
// The sink is an internal record; its private payload carries the writer
// capability and is not a platform interface brand.
let json = get_private_value(scope, sink, FILE_SYSTEM_WRITABLE_SINK_STATE_SLOT)?
.to_string(scope)?
.to_rust_string_lossy(scope);
@@ -26,7 +26,7 @@ struct ResizeObserverObjectDeclaration<'s> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "ResizeObserverEntry", prototype = "Object")]
struct ResizeObserverEntryDeclaration<'scope> {
#[webapi(data_property, enumerable)]
target: v8::Local<'scope, v8::Value>,
@@ -50,7 +50,7 @@ struct ResizeObserverObservedRecordDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "ResizeObserverSize", prototype = "Object")]
struct ResizeObserverSizeDeclaration {
#[webapi(data_property, enumerable)]
inline_size: f64,
@@ -1,9 +1,6 @@
use crate::{
context_bootstrap::{
current_child_browsing_context_handle_for_runtime_scope, current_worker_script_url,
},
context_bootstrap::{
is_readable_stream_object, is_transform_stream_object, is_writable_stream_object,
message_port_id_from_object,
},
structured_clone::{
@@ -448,40 +445,39 @@ fn parse_transfer_values<'s>(
array_buffers.push(buffer);
continue;
}
if let Ok(port) = v8::Local::<v8::Object>::try_from(candidate)
&& let Some(port_id) = message_port_id_from_object(scope, port)
{
if Some(port_id) == source_port_id {
operation.throw_data_clone_error(
scope,
"transfer list contains the source MessagePort.",
);
return None;
if let Ok(object) = v8::Local::<v8::Object>::try_from(candidate) {
match moli_webapi_declare::web_api_object_type(scope, object).map(|kind| kind.name()) {
Some("MessagePort") => {
if let Some(port_id) = message_port_id_from_object(scope, object) {
if Some(port_id) == source_port_id {
operation.throw_data_clone_error(
scope,
"transfer list contains the source MessagePort.",
);
return None;
}
seen.push(object.into());
message_ports.push(object);
continue;
}
}
Some("ReadableStream") => {
seen.push(object.into());
readable_streams.push(object);
continue;
}
Some("WritableStream") => {
seen.push(object.into());
writable_streams.push(object);
continue;
}
Some("TransformStream") => {
seen.push(object.into());
transform_streams.push(object);
continue;
}
_ => {}
}
seen.push(port.into());
message_ports.push(port);
continue;
}
if let Ok(stream) = v8::Local::<v8::Object>::try_from(candidate)
&& is_readable_stream_object(scope, stream)
{
seen.push(stream.into());
readable_streams.push(stream);
continue;
}
if let Ok(stream) = v8::Local::<v8::Object>::try_from(candidate)
&& is_writable_stream_object(scope, stream)
{
seen.push(stream.into());
writable_streams.push(stream);
continue;
}
if let Ok(stream) = v8::Local::<v8::Object>::try_from(candidate)
&& is_transform_stream_object(scope, stream)
{
seen.push(stream.into());
transform_streams.push(stream);
continue;
}
operation
.throw_data_clone_error(scope, "transfer list contains a non-transferable object.");
@@ -94,7 +94,7 @@ struct SharedWorkerHostEventInitDeclaration {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "Event", prototype = "Object")]
struct SharedWorkerHostEventFallbackDeclaration {
#[webapi(data_property, enumerable)]
r#type: String,
@@ -120,7 +120,7 @@ struct SharedWorkerHostErrorEventInitDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", scope_lifetime = 'scope)]
#[webapi(interface = "ErrorEvent", prototype = "Object", scope_lifetime = 'scope)]
struct SharedWorkerHostErrorEventFallbackDeclaration<'scope, 'text> {
#[webapi(data_property, enumerable)]
r#type: &'static str,
@@ -352,13 +352,6 @@ pub(crate) fn is_writable_stream_object<'s>(
moli_webapi_declare::implements_interface(scope, object, "WritableStream")
}
pub(crate) fn is_transform_stream_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> bool {
moli_webapi_declare::implements_interface(scope, object, "TransformStream")
}
pub(super) fn install_stream_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -72,7 +72,7 @@ pub(crate) fn is_readable_stream_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> bool {
has_readable_stream_brand(scope, object)
moli_webapi_declare::implements_interface(scope, object, "ReadableStream")
}
pub(crate) fn new_readable_stream_from_array_buffer<'s>(
@@ -277,7 +277,7 @@ fn readable_writable_pair_readable<'s>(
);
return None;
};
if !has_readable_stream_brand(scope, readable) {
if !is_readable_stream_object(scope, readable) {
throw_type_error(
scope,
"ReadableWritablePair.readable must be a ReadableStream",
@@ -309,13 +309,6 @@ fn readable_writable_pair_writable<'s>(
Some(writable)
}
fn has_readable_stream_brand<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> bool {
moli_webapi_declare::implements_interface(scope, object, "ReadableStream")
}
fn parse_stream_pipe_options<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: Option<v8::Local<'s, v8::Value>>,
@@ -447,7 +440,7 @@ pub(in crate::context_bootstrap) fn readable_stream_pipe_to_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let stream = args.this();
if !has_readable_stream_brand(scope, stream) {
if !is_readable_stream_object(scope, stream) {
set_rejected_pipe_to_type_error(scope, &mut rv, "Cannot pipe an invalid ReadableStream");
return;
}
@@ -971,7 +971,7 @@ pub(super) fn svg_transform_value_or_throw<'s>(
) -> Option<v8::Local<'s, v8::Object>> {
let object = v8::Local::<v8::Object>::try_from(value).ok();
if let Some(object) = object
&& get_private_value(scope, object, SVG_TRANSFORM_MATRIX_SLOT).is_some()
&& moli_webapi_declare::implements_interface(scope, object, "SVGTransform")
{
return Some(object);
}
@@ -1003,7 +1003,7 @@ pub(super) fn svg_matrix_value_or_throw<'s>(
) -> Option<v8::Local<'s, v8::Object>> {
let object = v8::Local::<v8::Object>::try_from(value).ok();
if let Some(object) = object
&& get_private_value(scope, object, SVG_MATRIX_A_SLOT).is_some()
&& moli_webapi_declare::implements_interface(scope, object, "SVGMatrix")
{
return Some(object);
}
@@ -253,7 +253,11 @@ struct OfflineAudioCompletePayloadDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(
interface = "OfflineAudioCompletionEvent",
prototype = "Object",
parent = "Event"
)]
struct OfflineAudioCompletionEventDeclaration<'scope> {
#[webapi(data_property = "type")]
event_type: &'static str,
@@ -2,7 +2,7 @@ use super::*;
use moli_webapi_declare::WebApiObject;
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "Event", prototype = "Object")]
struct WebSocketSimpleEventFallbackDeclaration {
#[webapi(data_property, enumerable)]
r#type: String,
@@ -116,7 +116,7 @@ struct ServiceWorkerNavigationPreloadStateDeclaration {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "PushSubscriptionOptions", prototype = "Object")]
struct ServiceWorkerPushSubscriptionOptionsDeclaration<'scope> {
#[webapi(data_property = "userVisibleOnly", readonly)]
user_visible_only: bool,
@@ -296,7 +296,7 @@ struct ServiceWorkerMessageEventInitDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "Event", prototype = "Object")]
struct ServiceWorkerSimpleEventDeclaration<'scope> {
#[webapi(data_property = "type", enumerable)]
event_type: v8::Local<'scope, v8::String>,
@@ -21,7 +21,7 @@ struct WorkerHostEventInitDeclaration {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "Event", prototype = "Object")]
struct WorkerHostEventFallbackDeclaration {
#[webapi(data_property, enumerable)]
r#type: String,
@@ -39,7 +39,7 @@ struct WorkerHostMessageEventInitDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", scope_lifetime = 'scope)]
#[webapi(interface = "MessageEvent", prototype = "Object", scope_lifetime = 'scope)]
struct WorkerHostMessageEventFallbackDeclaration<'scope, 'event> {
#[webapi(data_property, enumerable)]
data: v8::Local<'scope, v8::Value>,
@@ -67,7 +67,7 @@ struct WorkerHostErrorEventInitDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", scope_lifetime = 'scope)]
#[webapi(interface = "ErrorEvent", prototype = "Object", scope_lifetime = 'scope)]
struct WorkerHostErrorEventFallbackDeclaration<'scope, 'text> {
#[webapi(data_property, enumerable)]
r#type: &'static str,
@@ -8,7 +8,7 @@ use anyhow::Result;
use moli_webapi_declare::WebApiObject;
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "CustomElementRegistry", prototype = "Object")]
struct CustomElementsRegistryDeclaration<'scope> {
#[webapi(prototype)]
prototype: v8::Local<'scope, v8::Object>,
@@ -2,7 +2,7 @@ use super::{CustomElementRegistryAssociation, CustomElementRegistryKey};
use crate::{
document_runtime::DomHandle,
native_bridge::JsContextHost,
util::{get_private_value, global_constructor_object, set_private_value, v8str},
util::{get_private_value, set_private_value, v8str},
};
pub(super) const CUSTOM_ELEMENTS_REGISTRY_CHILD_HANDLE_SLOT: &str =
@@ -49,15 +49,7 @@ pub(crate) fn registry_association_from_value<'s>(
return Some(CustomElementRegistryAssociation::Null);
}
let registry = v8::Local::<v8::Object>::try_from(value).ok()?;
if registry_u64_private_slot(scope, registry, CUSTOM_ELEMENTS_REGISTRY_SCOPED_ID_SLOT).is_some()
|| registry_child_window_handle(scope, registry).is_some()
{
return Some(CustomElementRegistryAssociation::Registry(
registry_store_key(scope, registry),
));
}
let constructor = global_constructor_object(scope, "CustomElementRegistry")?;
if !value.instance_of(scope, constructor).unwrap_or(false) {
if !moli_webapi_declare::implements_interface(scope, registry, "CustomElementRegistry") {
return None;
}
Some(CustomElementRegistryAssociation::Registry(
@@ -64,13 +64,6 @@ pub(super) use templates::{
build_static_handle_node_list_wrapper_template,
};
pub(crate) fn blob_parts_platform_collection_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<&'static str> {
collection_kind_from_object(scope, object).map(collection_interface_name)
}
pub(in crate::native_bridge::collections) fn collection_kind_from_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -142,13 +142,13 @@ struct LightweightPopupDocumentStreamMethodsDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", data_properties, enumerable)]
#[webapi(interface = "Event", prototype = "Object", data_properties, enumerable)]
struct LightweightPopupEventDeclaration<'scope> {
r#type: v8::Local<'scope, v8::String>,
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "PopStateEvent", prototype = "Object")]
struct LightweightPopupPopStateEventDeclaration<'scope> {
#[webapi(data_property)]
state: v8::Local<'scope, v8::Value>,
@@ -9,7 +9,11 @@ use super::iframe_style::install_detached_iframe_get_computed_style;
use super::iframe_window_messaging::install_detached_iframe_window_messaging;
#[derive(WebApiObject)]
#[webapi(interface = "Object", own_to_string_tag = "Window")]
#[webapi(
interface = "Window",
prototype = "Object",
own_to_string_tag = "Window"
)]
struct DetachedIframeWindowDeclaration<'scope> {
#[webapi(data_property = "self", value = object)]
self_value: (),
@@ -1,22 +1,12 @@
use super::super::*;
use crate::util::global_constructor_object;
fn value_is_document_type<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> bool {
if let Ok(object) = v8::Local::<v8::Object>::try_from(value)
&& detached_state_object(scope, object)
.and_then(|state| state.get(scope, v8str(scope, "nodeType").into()))
.and_then(|node_type| node_type.uint32_value(scope))
== Some(10)
{
return true;
}
let Some(constructor) = global_constructor_object(scope, "DocumentType") else {
return false;
};
value.instance_of(scope, constructor).unwrap_or(false)
v8::Local::<v8::Object>::try_from(value).is_ok_and(|object| {
moli_webapi_declare::implements_interface(scope, object, "DocumentType")
})
}
pub(in crate::native_bridge) fn bridge_create_detached_document_callback<'a>(
@@ -1410,7 +1410,7 @@ struct ElementStylePrototypeDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "HTMLElement")]
#[webapi(name = "HTMLElement", receiver = "HTMLElement")]
struct HtmlElementStandardPrototypeDeclaration {
#[webapi(
accessor_property,
+1 -3
View File
@@ -49,9 +49,7 @@ pub(crate) use active_child_window::{
restore_deferred_active_child_window_scope_if_present,
};
pub(crate) use child_window_surface::CALLBACK_ERROR_WINDOW_HANDLE_SLOT;
pub(crate) use collections::{
blob_parts_platform_collection_kind, install_collection_template_bindings,
};
pub(crate) use collections::install_collection_template_bindings;
pub(crate) use context_host::{
DetachedChildBrowsingContextDocumentSnapshot, ImageDecodeRequestId,
RuntimeObservableContextToken, cross_origin_lightweight_popup_id,
@@ -119,21 +119,11 @@ pub(in crate::native_bridge) fn node_compare_document_position_callback<'s>(
return;
}
// Spec compatibility: argument may still be a Node — just from a foreign
// realm / detached document we can't pair with the live tree. Per DOM
// spec, cross-tree comparison must return DISCONNECTED |
// IMPLEMENTATION_SPECIFIC | (PRECEDING or FOLLOWING) rather than throwing.
//
// We must NOT take this branch for arbitrary JS objects (`{}`, Arrays,
// etc.) — WebIDL requires throwing TypeError when the argument isn't a
// Node. is_node_like_object below is the discriminator: it accepts
// either a live Node wrapper (one internal reflector-id field) or a
// detached-doc node wrapper (has the
// __moliDetachedState private slot, which only detached node
// builders set).
// A native Node from another realm or detached document still compares as
// disconnected even when this runtime cannot resolve its live tree handle.
let other_value = args.get(0);
if let Ok(other_object) = v8::Local::<v8::Object>::try_from(other_value)
&& is_node_like_object(scope, other_object)
&& moli_webapi_declare::implements_interface(scope, other_object, "Node")
{
let order_bit = disconnected_order_bit(args.this(), other_object);
rv.set(
@@ -156,32 +146,6 @@ pub(in crate::native_bridge) fn node_compare_document_position_callback<'s>(
scope.throw_exception(v8::Exception::type_error(scope, message));
}
/// Returns true if `object` carries the wrapper shape of a Node, including
/// foreign-realm / detached-document nodes that aren't paired with a live
/// DomHandle in this runtime.
fn is_node_like_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> bool {
use crate::util::get_private_object;
// Live wrapper: its sole internal field is a reflector identity. The
// existing helper distinguishes Node wrappers from Window / ClassList /
// Style / etc., so we only need to know that it succeeds.
if node_runtime_and_handle_from_object(scope, object).is_ok() {
return true;
}
// Detached / foreign-document Node wrapper: every builder under
// native_bridge/document/detached_objects/builders stores its state
// object under this private slot. Plain JS objects, Arrays, function
// returns etc. never carry this slot.
get_private_object(
scope,
object,
crate::native_bridge::document::DETACHED_STATE_SLOT,
)
.is_some()
}
/// Stable PRECEDING/FOLLOWING choice for two disconnected nodes.
///
/// V8 identity hashes are i32 values, occasionally negative — promote to
@@ -25,6 +25,7 @@ mod misc;
mod navigation;
mod performance;
mod performance_memory;
mod platform_identity;
mod pointer_lock;
mod promise_rejection;
mod security_policy;
@@ -0,0 +1,174 @@
use super::*;
#[test]
fn file_reader_fallback_inherits_event_without_a_public_progress_event_constructor() {
let mut vm = new_storage_page_task_executor_test_vm("https://fallback-event-parent.test/");
vm.eval(
r#"
// Remove the lazy property before replacing its descriptor.
delete globalThis.ProgressEvent;
Object.defineProperty(globalThis, 'ProgressEvent', {
get() { throw new Error('the public constructor must not be consulted'); }
});
const reader = new FileReader();
reader.onload = event => {
Object.setPrototypeOf(event, null);
globalThis.readEvent = event;
};
reader.readAsText(new Blob(['identity']));
"#,
)
.expect("start a FileReader read without materializing ProgressEvent");
assert_eq!(
vm.eval_after_selected_page_tasks("readEvent.type").unwrap(),
"load"
);
vm.with_default_context_scope_and_checkpoint_for_test(|scope, _host_ptr| {
let global = scope.get_current_context().global(scope);
let key = v8::String::new(scope, "readEvent").unwrap();
let value = global.get(scope, key.into()).unwrap();
let event = v8::Local::<v8::Object>::try_from(value).unwrap();
assert_eq!(
moli_webapi_declare::web_api_object_type(scope, event)
.unwrap()
.name(),
"ProgressEvent"
);
assert!(moli_webapi_declare::implements_interface(
scope, event, "Event"
));
assert!(!moli_webapi_declare::implements_interface(
scope,
event,
"EventTarget"
));
assert_eq!(
crate::context_bootstrap::exposed_interfaces::interface_template_build_count(
scope,
"ProgressEvent"
),
0
);
Ok(())
})
.expect("the fallback factory should inherit the shared registry's parent metadata");
}
#[test]
fn native_receivers_survive_prototype_changes_and_reject_author_wrappers() {
let mut vm = new_storage_test_vm("https://native-receiver-identity.test/");
let result = vm.eval(r#"
(() => {
const width = Object.getOwnPropertyDescriptor(ImageData.prototype, 'width').get;
const family = Object.getOwnPropertyDescriptor(FontFace.prototype, 'family').get;
const hidden = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'hidden').get;
const detached = document.implementation.createHTMLDocument('detached');
const cases = [
['ImageData', new ImageData(2, 1), value => width.call(value) === 2],
['PerformanceEntry', new PerformanceMark('identity'), value => PerformanceEntry.prototype.toJSON.call(value).name === 'identity'],
['DOMMatrixReadOnly', new DOMMatrix(), value => DOMMatrixReadOnly.prototype.toFloat64Array.call(value).length === 16],
['FormData', new FormData(), value => FormData.prototype.has.call(value, 'x') === false],
['URLSearchParams', new URLSearchParams('x=1'), value => URLSearchParams.prototype.get.call(value, 'x') === '1'],
['Headers', new Headers({x: '1'}), value => Headers.prototype.get.call(value, 'x') === '1'],
['FontFace', new FontFace('Identity', 'local(Identity)'), value => family.call(value) === 'Identity'],
['EventTarget', new EventTarget(), value => EventTarget.prototype.dispatchEvent.call(value, new Event('test')) === true],
['HTMLElement', document.createElement('div'), value => hidden.call(value) === false],
['detached HTMLElement', detached.createElement('div'), value => hidden.call(value) === false],
];
const failures = [];
for (const [name, real, check] of cases) {
const prototype = Object.getPrototypeOf(real);
Object.setPrototypeOf(real, null);
if (!check(real)) failures.push(`${name}:real`);
for (const fake of [{}, Object.create(prototype), Object.create(real), new Proxy(real, {})]) {
try { check(fake); failures.push(`${name}:accepted`); }
catch (error) { if (error.name !== 'TypeError') failures.push(`${name}:${error.name}`); }
}
}
return failures.join('|');
})()
"#).expect("native receiver identity matrix should evaluate");
assert_eq!(result, "");
}
#[test]
fn document_type_conversion_ignores_public_constructor_and_prototype_forgery() {
let mut vm = new_storage_test_vm("https://document-type-identity.test/");
let result = vm.eval(r#"
(() => {
const prototype = DocumentType.prototype;
const real = document.implementation.createDocumentType('html', '', '');
Object.setPrototypeOf(real, null);
globalThis.DocumentType = function ForgedDocumentType() {};
Object.defineProperty(DocumentType, Symbol.hasInstance, {value() { throw Error('must not run'); }});
const created = document.implementation.createDocument(null, '', real);
const outcomes = [created.doctype === real];
for (const fake of [Object.create(prototype), Object.create(real), {nodeType: 10}, new Proxy(real, {})]) {
try { document.implementation.createDocument(null, '', fake); outcomes.push('accepted'); }
catch (error) { outcomes.push(error.name); }
}
return outcomes.join('|');
})()
"#).expect("DocumentType conversion should use native identity");
assert_eq!(result, "true|TypeError|TypeError|TypeError|TypeError");
}
#[test]
fn platform_subtypes_do_not_implicitly_inherit_clone_or_transfer_codecs() {
let mut vm = new_storage_page_task_executor_test_vm("https://primary-interface-codecs.test/");
vm.eval(
"globalThis.futureBlob = new Blob(['x']); globalThis.futureStream = new WritableStream();",
)
.expect("create native base payloads");
vm.with_default_context_scope_and_checkpoint_for_test(|scope, _host_ptr| {
moli_webapi_declare::register_web_api_interfaces(
scope,
[
("FutureBlob", Some("Blob")),
("FutureWritableStream", Some("WritableStream")),
],
)?;
let global = scope.get_current_context().global(scope);
for (key, interface) in [
("futureBlob", "FutureBlob"),
("futureStream", "FutureWritableStream"),
] {
let key = v8::String::new(scope, key).unwrap();
let value = global.get(scope, key.into()).unwrap();
let object = v8::Local::<v8::Object>::try_from(value).unwrap();
moli_webapi_declare::initialize_web_api_object(scope, object, interface)?;
}
Ok(())
})
.expect("model native subinterfaces with inherited state but no codecs");
let result = vm.eval(r#"
(() => {
const probe = fn => { try { fn(); return 'accepted'; } catch (error) { return error.name; } };
const size = Object.getOwnPropertyDescriptor(Blob.prototype, 'size').get.call(futureBlob);
const locked = Object.getOwnPropertyDescriptor(WritableStream.prototype, 'locked').get;
return [size, probe(() => structuredClone({value: futureBlob})),
probe(() => structuredClone(futureStream, {transfer: [futureStream]})),
probe(() => structuredClone({}, {transfer: [futureStream]})), locked.call(futureStream)].join('|');
})()
"#).expect("receiver inheritance must not grant serialization capability");
assert_eq!(
result,
"1|DataCloneError|DataCloneError|DataCloneError|false"
);
vm.eval(
r#"
globalThis.futureStored = 'pending';
const open = indexedDB.open('future-interface', 1);
open.onupgradeneeded = () => {
const store = open.result.createObjectStore('values');
try { store.put({value: futureBlob}, 1); futureStored = 'accepted'; }
catch (error) { futureStored = error.name; }
};
"#,
)
.expect("schedule future native interface storage");
assert_eq!(
vm.eval_after_selected_page_tasks("futureStored").unwrap(),
"DataCloneError"
);
}
@@ -511,8 +511,8 @@ fn structured_clone_rejects_platform_objects_in_nested_graphs_without_running_th
new URL('https://example.test/'), new URLSearchParams('a=b'), new Headers(),
new Request('https://example.test/'), new Response('body'), new Response().headers, new FormData(),
new Event('event'), new EventTarget(), new AbortController(), new AbortController().signal,
navigator, performance, history, localStorage, new Highlight(), CSS.highlights,
document, document.implementation, new XMLHttpRequest().upload, document.createElement('select'), document.createTextNode('text'),
navigator, performance, history, localStorage, new Highlight(), CSS.highlights, customElements, new CustomElementRegistry(),
document, document.implementation, document.childNodes, document.children, document.querySelectorAll('*'), document.createElement('div').attributes, new XMLHttpRequest().upload, document.createElement('select'), document.createTextNode('text'),
document.implementation.createHTMLDocument('detached').body,
new TextEncoder(), new TextDecoder(), new TextEncoderStream(), new TextDecoderStream(),
new CompressionStream('gzip'), new DecompressionStream('gzip'),
@@ -570,3 +570,85 @@ fn encoding_stream_wrappers_do_not_inherit_transform_transferability() {
"DataCloneError|DataCloneError|DataCloneError|DataCloneError"
);
}
#[test]
fn shared_array_buffers_fail_consistently_at_root_and_inside_message_graphs() {
let mut vm = new_storage_test_vm("https://shared-buffer-clone.test/");
let result = vm.eval(r#"
(() => {
const shared = new SharedArrayBuffer(8);
const channel = new MessageChannel();
const probe = fn => { try { fn(); return 'accepted'; } catch (error) { return error.name; } };
return [
probe(() => structuredClone(shared)),
probe(() => structuredClone({nested: new Map([['shared', shared]])})),
probe(() => channel.port1.postMessage({shared})),
probe(() => window.postMessage({shared}, '*')),
shared.byteLength
].join('|');
})()
"#).expect("unsupported shared buffers must report DataCloneError at every depth");
assert_eq!(
result,
"DataCloneError|DataCloneError|DataCloneError|DataCloneError|8"
);
}
#[test]
fn supported_platform_codecs_preserve_payloads_after_prototype_changes() {
let mut vm = new_storage_test_vm("https://supported-platform-codecs.test/");
let result = vm.eval(r#"
(() => {
const blob = new (class extends Blob {})(['abc'], {type: 'text/plain'});
const file = new File(['def'], 'note.txt', {type: 'text/plain', lastModified: 17});
const pixels = new ImageData(new Uint8ClampedArray([1, 2, 3, 4]), 1, 1);
const exception = new DOMException('message', 'AbortError');
let getterCalls = 0;
for (const object of [blob, file, pixels, exception]) {
Object.setPrototypeOf(object, null);
Object.defineProperty(object, 'expando', {enumerable: true, get() { getterCalls++; return 1; }});
}
const clone = structuredClone({blob, file, pixels, exception, again: blob});
return [clone.blob instanceof Blob, clone.blob.size, clone.blob.type, clone.blob === clone.again,
clone.file instanceof File, clone.file.name, clone.file.lastModified,
clone.pixels instanceof ImageData, Array.from(clone.pixels.data).join(','),
clone.exception instanceof DOMException, clone.exception.name, clone.exception.message,
getterCalls, clone.blob.expando === undefined].join('|');
})()
"#).expect("native codecs should ignore mutable prototypes and author expandos");
assert_eq!(
result,
"true|3|text/plain|true|true|note.txt|17|true|1,2,3,4|true|AbortError|message|0|true"
);
}
#[test]
fn observer_and_webgl_factory_results_have_native_identity() {
let mut vm = new_storage_test_vm("https://factory-result-identity.test/");
let result = vm
.eval(
r#"
(() => {
const target = document.createElement('div');
target.style.cssText = 'width: 41px; height: 23px';
const html = document.documentElement || document.appendChild(document.createElement('html'));
const body = document.body || html.appendChild(document.createElement('body'));
body.appendChild(target);
const observer = new ResizeObserver(() => {});
observer.observe(target);
const entry = observer.takeRecords()[0];
const gl = document.createElement('canvas').getContext('webgl');
const precision = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT);
return [entry, entry.contentBoxSize[0], entry.borderBoxSize[0], precision].map(value => {
try { structuredClone({value}); return 'accepted'; }
catch (error) { return error.name; }
}).join('|');
})()
"#,
)
.expect("factory results must be identified before structured clone");
assert_eq!(
result,
"DataCloneError|DataCloneError|DataCloneError|DataCloneError"
);
}
@@ -3247,6 +3247,30 @@ fn indexed_db_put_rejects_performance_entries_without_rejecting_plain_objects()
);
}
#[test]
fn indexed_db_rejects_shared_buffers_at_any_depth() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-shared-buffer.test/");
vm.eval(
r#"
globalThis.sharedStored = 'pending';
const open = indexedDB.open('shared-buffers', 1);
open.onupgradeneeded = () => {
const store = open.result.createObjectStore('values');
const shared = new SharedArrayBuffer(8);
sharedStored = [shared, {nested: shared}].map((value, key) => {
try { store.put(value, key); return 'accepted'; }
catch (error) { return error.name; }
}).join('|');
};
"#,
)
.expect("schedule SharedArrayBuffer storage checks");
assert_eq!(
vm.eval_after_selected_page_tasks("sharedStored").unwrap(),
"DataCloneError|DataCloneError"
);
}
#[test]
fn indexed_db_put_webassembly_module_throws_data_clone_error_for_storage() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-wasm-dataclone.test/");
+146 -126
View File
@@ -20,8 +20,7 @@ use crate::{
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, is_readable_stream_object,
is_transform_stream_object, is_writable_stream_object, message_port_id_from_object,
initialize_writable_stream_clone_shell, message_port_id_from_object,
new_dom_exception_value, new_quota_exceeded_error_value, prepare_readable_stream_transfer,
prepare_transform_stream_transfer, prepare_writable_stream_transfer,
quota_exceeded_error_clone_fields, require_internal_stream_value,
@@ -315,131 +314,152 @@ impl v8::ValueSerializerImpl for WireSerializer {
object: v8::Local<'s, v8::Object>,
serializer: &dyn v8::ValueSerializerHelper,
) -> Option<bool> {
if let Some(port_id) = message_port_id_from_object(scope, object) {
if !self.allowed_message_port_ids.contains(&port_id) {
throw_data_clone_exception(
scope,
"MessagePort must be listed in the postMessage transfer list.",
);
return None;
match moli_webapi_declare::web_api_object_type(scope, object).map(|kind| kind.name()) {
Some("MessagePort") => {
if let Some(port_id) = message_port_id_from_object(scope, object) {
if !self.allowed_message_port_ids.contains(&port_id) {
throw_data_clone_exception(
scope,
"MessagePort must be listed in the postMessage transfer list.",
);
return None;
}
serializer.write_uint32(HOST_OBJECT_TAG_MESSAGE_PORT);
serializer.write_uint64(port_id);
return Some(true);
}
}
serializer.write_uint32(HOST_OBJECT_TAG_MESSAGE_PORT);
serializer.write_uint64(port_id);
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 write_crypto_key_payload(scope, object, serializer).is_some() {
return Some(true);
}
if is_readable_stream_object(scope, object) {
let Some(index) = self
.allowed_readable_streams
.iter()
.position(|allowed| v8::Local::new(scope, allowed).strict_equals(object.into()))
else {
throw_data_clone_exception(
scope,
"ReadableStream must be listed in the postMessage transfer list.",
);
return None;
};
let Ok(clone_id) = u32::try_from(index) else {
throw_data_clone_exception(scope, "Too many ReadableStreams in structured clone.");
return None;
};
serializer.write_uint32(HOST_OBJECT_TAG_READABLE_STREAM);
serializer.write_uint32(clone_id);
return Some(true);
}
if is_writable_stream_object(scope, object) {
let Some(index) = self
.allowed_writable_streams
.iter()
.position(|allowed| v8::Local::new(scope, allowed).strict_equals(object.into()))
else {
throw_data_clone_exception(
scope,
"WritableStream must be listed in the postMessage transfer list.",
);
return None;
};
let Ok(clone_id) = u32::try_from(index) else {
throw_data_clone_exception(scope, "Too many WritableStreams in structured clone.");
return None;
};
serializer.write_uint32(HOST_OBJECT_TAG_WRITABLE_STREAM);
serializer.write_uint32(clone_id);
return Some(true);
}
if is_transform_stream_object(scope, object) {
let Some(index) = self
.allowed_transform_streams
.iter()
.position(|allowed| v8::Local::new(scope, allowed).strict_equals(object.into()))
else {
throw_data_clone_exception(
scope,
"TransformStream must be listed in the postMessage transfer list.",
);
return None;
};
let Ok(clone_id) = u32::try_from(index) else {
throw_data_clone_exception(scope, "Too many TransformStreams in structured clone.");
return None;
};
serializer.write_uint32(HOST_OBJECT_TAG_TRANSFORM_STREAM);
serializer.write_uint32(clone_id);
return Some(true);
}
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
let mut store = self.blobs.borrow_mut();
let clone_id = store.next_id;
let Some(next_id) = store.next_id.checked_add(1) else {
drop(store);
throw_data_clone_exception(scope, "Too many Blobs in structured clone.");
return None;
};
store.next_id = next_id;
store.blobs.push(ClonedBlob { clone_id, payload });
serializer.write_uint32(HOST_OBJECT_TAG_BLOB);
serializer.write_uint32(clone_id);
return Some(true);
}
if let Some(payload) = file_system_handle_clone_payload_from_object(scope, object) {
let mut store = self.file_system_handles.borrow_mut();
let clone_id = store.next_id;
let Some(next_id) = store.next_id.checked_add(1) else {
drop(store);
throw_data_clone_exception(
scope,
"Too many FileSystemHandles in structured clone.",
);
return None;
};
store.next_id = next_id;
store
.handles
.push(ClonedFileSystemHandle { clone_id, payload });
serializer.write_uint32(HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE);
serializer.write_uint32(clone_id);
return Some(true);
}
if let Some((message, quota, requested)) = quota_exceeded_error_clone_fields(scope, object)
{
serializer.write_uint32(HOST_OBJECT_TAG_QUOTA_EXCEEDED_ERROR);
write_string(serializer, &message);
write_optional_double(serializer, quota);
write_optional_double(serializer, requested);
return Some(true);
}
if let Some((message, name)) = dom_exception_clone_fields(scope, object) {
serializer.write_uint32(HOST_OBJECT_TAG_DOM_EXCEPTION);
write_string(serializer, &message);
write_string(serializer, &name);
return Some(true);
Some("ImageData") => {
if let Some(payload) = image_data_clone_payload_from_object(scope, object) {
write_image_data_payload(serializer, payload);
return Some(true);
}
}
Some("CryptoKey") => {
if write_crypto_key_payload(scope, object, serializer).is_some() {
return Some(true);
}
}
Some("ReadableStream") => {
let Some(index) = self.allowed_readable_streams.iter().position(|allowed| {
v8::Local::new(scope, allowed).strict_equals(object.into())
}) else {
throw_data_clone_exception(
scope,
"ReadableStream must be listed in the postMessage transfer list.",
);
return None;
};
let Ok(clone_id) = u32::try_from(index) else {
throw_data_clone_exception(
scope,
"Too many ReadableStreams in structured clone.",
);
return None;
};
serializer.write_uint32(HOST_OBJECT_TAG_READABLE_STREAM);
serializer.write_uint32(clone_id);
return Some(true);
}
Some("WritableStream") => {
let Some(index) = self.allowed_writable_streams.iter().position(|allowed| {
v8::Local::new(scope, allowed).strict_equals(object.into())
}) else {
throw_data_clone_exception(
scope,
"WritableStream must be listed in the postMessage transfer list.",
);
return None;
};
let Ok(clone_id) = u32::try_from(index) else {
throw_data_clone_exception(
scope,
"Too many WritableStreams in structured clone.",
);
return None;
};
serializer.write_uint32(HOST_OBJECT_TAG_WRITABLE_STREAM);
serializer.write_uint32(clone_id);
return Some(true);
}
Some("TransformStream") => {
let Some(index) = self.allowed_transform_streams.iter().position(|allowed| {
v8::Local::new(scope, allowed).strict_equals(object.into())
}) else {
throw_data_clone_exception(
scope,
"TransformStream must be listed in the postMessage transfer list.",
);
return None;
};
let Ok(clone_id) = u32::try_from(index) else {
throw_data_clone_exception(
scope,
"Too many TransformStreams in structured clone.",
);
return None;
};
serializer.write_uint32(HOST_OBJECT_TAG_TRANSFORM_STREAM);
serializer.write_uint32(clone_id);
return Some(true);
}
Some("Blob" | "File") => {
if let Some(payload) = blob_clone_payload_from_object(scope, object) {
let mut store = self.blobs.borrow_mut();
let clone_id = store.next_id;
let Some(next_id) = store.next_id.checked_add(1) else {
drop(store);
throw_data_clone_exception(scope, "Too many Blobs in structured clone.");
return None;
};
store.next_id = next_id;
store.blobs.push(ClonedBlob { clone_id, payload });
serializer.write_uint32(HOST_OBJECT_TAG_BLOB);
serializer.write_uint32(clone_id);
return Some(true);
}
}
Some("FileSystemFileHandle" | "FileSystemDirectoryHandle") => {
if let Some(payload) = file_system_handle_clone_payload_from_object(scope, object) {
let mut store = self.file_system_handles.borrow_mut();
let clone_id = store.next_id;
let Some(next_id) = store.next_id.checked_add(1) else {
drop(store);
throw_data_clone_exception(
scope,
"Too many FileSystemHandles in structured clone.",
);
return None;
};
store.next_id = next_id;
store
.handles
.push(ClonedFileSystemHandle { clone_id, payload });
serializer.write_uint32(HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE);
serializer.write_uint32(clone_id);
return Some(true);
}
}
Some("QuotaExceededError") => {
if let Some((message, quota, requested)) =
quota_exceeded_error_clone_fields(scope, object)
{
serializer.write_uint32(HOST_OBJECT_TAG_QUOTA_EXCEEDED_ERROR);
write_string(serializer, &message);
write_optional_double(serializer, quota);
write_optional_double(serializer, requested);
return Some(true);
}
}
Some("DOMException") => {
if let Some((message, name)) = dom_exception_clone_fields(scope, object) {
serializer.write_uint32(HOST_OBJECT_TAG_DOM_EXCEPTION);
write_string(serializer, &message);
write_string(serializer, &name);
return Some(true);
}
}
_ => {}
}
throw_data_clone_exception(scope, "Unsupported host object during structured clone.");
None
@@ -542,7 +542,7 @@ struct ServiceWorkerGlobalEventHandlerStateDeclaration {
}
#[derive(Default, WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "Clients", prototype = "Object")]
struct ServiceWorkerClientsDeclaration {
#[webapi(method, callback = service_worker_clients_claim_callback, length = 0)]
claim: (),
@@ -839,7 +839,7 @@ struct ServiceWorkerPushSubscriptionDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "PushSubscriptionOptions", prototype = "Object")]
struct ServiceWorkerPushSubscriptionOptionsDeclaration<'scope> {
#[webapi(data_property = "userVisibleOnly", readonly)]
user_visible_only: bool,
+36 -10
View File
@@ -126,7 +126,7 @@ struct WorkerPromiseRejectionEventInitDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "ErrorEvent", prototype = "Object")]
struct WorkerErrorEventFallbackDeclaration<'scope> {
#[webapi(data_property, enumerable)]
message: v8::Local<'scope, v8::String>,
@@ -141,7 +141,7 @@ struct WorkerErrorEventFallbackDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "PromiseRejectionEvent", prototype = "Object")]
struct WorkerPromiseRejectionEventFallbackDeclaration<'scope> {
#[webapi(data_property, enumerable)]
promise: v8::Local<'scope, v8::Promise>,
@@ -150,7 +150,7 @@ struct WorkerPromiseRejectionEventFallbackDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "Event", prototype = "Object")]
struct WorkerBasicEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -161,7 +161,7 @@ struct WorkerBasicEventDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(interface = "ExtendableEvent", prototype = "Object", parent = "Event")]
struct ServiceWorkerLifecycleEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -174,7 +174,11 @@ struct ServiceWorkerLifecycleEventDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(
interface = "FetchEvent",
prototype = "Object",
parent = "ExtendableEvent"
)]
struct ServiceWorkerFetchEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -201,7 +205,11 @@ struct ServiceWorkerFetchEventDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(
interface = "ExtendableMessageEvent",
prototype = "Object",
parent = "ExtendableEvent"
)]
struct ServiceWorkerMessageEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -254,7 +262,11 @@ struct ServiceWorkerMessageDispatchMethodsDeclaration {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(
interface = "NotificationEvent",
prototype = "Object",
parent = "ExtendableEvent"
)]
struct ServiceWorkerNotificationEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -271,7 +283,11 @@ struct ServiceWorkerNotificationEventDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(
interface = "PushEvent",
prototype = "Object",
parent = "ExtendableEvent"
)]
struct ServiceWorkerPushEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -299,7 +315,11 @@ struct ServiceWorkerPushMessageDataDeclaration {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(
interface = "SyncEvent",
prototype = "Object",
parent = "ExtendableEvent"
)]
struct ServiceWorkerSyncEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -316,7 +336,11 @@ struct ServiceWorkerSyncEventDeclaration<'scope> {
}
#[derive(WebApiObject)]
#[webapi(interface = "Object")]
#[webapi(
interface = "PeriodicSyncEvent",
prototype = "Object",
parent = "ExtendableEvent"
)]
struct ServiceWorkerPeriodicSyncEventDeclaration<'scope> {
#[webapi(data_property, enumerable)]
r#type: v8::Local<'scope, v8::String>,
@@ -729,6 +753,8 @@ fn new_worker_message_event<'s>(
ports,
)
.initialize(scope, event);
moli_webapi_declare::initialize_web_api_object(scope, event, "MessageEvent")
.expect("worker fallback MessageEvent identity should initialize");
event
}
@@ -2805,3 +2805,31 @@ async fn worker_postmessage_workernavigator_throws_datacloneerror() {
.expect("channel closed");
assert_eq!(expect_post_json(msg), r#""DataCloneError""#);
}
#[tokio::test]
async fn worker_fallback_message_events_are_native_platform_objects() {
ensure_v8();
let mut handle = spawn_worker(
r#"
MessageEvent = undefined;
onmessage = event => {
let cloneResult = 'accepted';
try { structuredClone({event}); }
catch (error) { cloneResult = error.name; }
postMessage({data: event.data, cloneResult});
close();
};
postMessage('ready');
"#
.into(),
"https://worker-fallback-identity.test/worker.js".into(),
);
let ready = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(expect_post_json(ready), r#""ready""#);
handle.post_message(serialize_test_string("ping"));
let result = timeout(TIMEOUT, handle.recv()).await.unwrap().unwrap();
assert_eq!(
expect_post_json(result),
r#"{"data":"ping","cloneResult":"DataCloneError"}"#
);
}
+19 -5
View File
@@ -92,10 +92,21 @@ pub fn register_web_api_interfaces<C>(
v8::scope!(let scope, scope.as_mut());
let registry = registry(scope);
let mut registry = registry.borrow_mut();
// Object declarations can register their parent on every construction.
// Repeated metadata needs no copies or cycle validation of the whole graph.
let mut changes = interfaces.into_iter().filter(|(name, parent)| {
!registry.names.get(name).is_some_and(|id| {
let entry = &registry.entries[*id];
entry.declared && entry.parent.map(|id| registry.entries[id].name) == *parent
})
});
let Some(first) = changes.next() else {
return Ok(());
};
// Validate the whole batch before publishing any inheritance changes.
let mut entries = registry.entries.clone();
let mut names = registry.names.clone();
for (name, parent) in interfaces {
for (name, parent) in std::iter::once(first).chain(changes) {
if name == "Object" || parent == Some("Object") {
return Err(BindError::new("Object is not a Web IDL interface"));
}
@@ -149,13 +160,14 @@ fn object_type_id<'s>(
(id < registry.borrow().entries.len()).then_some(id)
}
/// Reads only the object's own native identity. Prototype inheritance and
/// JavaScript properties (including symbols) cannot supply this identity.
/// Reads the object's own native identity, or the target identity of an
/// explicitly registered native Proxy. Prototype inheritance and JavaScript
/// properties (including symbols) cannot supply identity or invoke author code.
pub fn web_api_object_type<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<WebApiType> {
let registry = registry(scope);
let registry = scope.get_slot::<Rc<RefCell<TypeRegistry>>>()?.clone();
let id = object_type_id(scope, object, &registry)?;
Some(WebApiType {
name: registry.borrow().entries[id].name,
@@ -167,7 +179,9 @@ pub fn implements_interface<'s>(
object: v8::Local<'s, v8::Object>,
expected: &str,
) -> bool {
let registry = registry(scope);
let Some(registry) = scope.get_slot::<Rc<RefCell<TypeRegistry>>>().cloned() else {
return false;
};
let Some(id) = object_type_id(scope, object, &registry) else {
return false;
};
+58 -10
View File
@@ -90,6 +90,16 @@
//! `#[webapi(unbranded)]` explicitly opts named prototype/constructor installers,
//! dictionaries, and shared initialization fragments out of instance branding.
//! These fragments never erase an identity already present on their target.
//! Use payload slots for data, handles, ownership, and lifecycle state instead
//! of maintaining separate boolean brands. A named native declaration can use
//! `prototype = "Object"` to preserve an existing plain public prototype while
//! still assigning native identity.
//!
//! Implementation sharing does not imply interface inheritance. For example,
//! encoding streams share TransformStream internals but have distinct native
//! interfaces. Their factory assigns the concrete type while the shared state
//! fragment stays unbranded. Do not opt actual instances out of branding to
//! make them pass structured clone.
//!
//! Register inheritance with `register_web_api_interfaces` using the same
//! interface metadata that drives constructor installation. `WebApiInterface`
@@ -97,13 +107,48 @@
//! realm exposure and mutable JavaScript prototypes. Use `web_api_object_type`
//! for the primary interface and `implements_interface` for inherited receiver
//! checks. Native factories can call `initialize_web_api_object` directly.
//! An object declaration outside the exposed-interface registry can declare
//! `parent = "BaseInterface"`; this registers inheritance before initialization.
//! JavaScript subclasses retain the native interface implemented by their
//! constructor. Base initialization never downgrades an existing derived type.
//! Inconsistent parents, cycles, and unrelated rebranding fail. The renderer
//! registers inheritance from `ConstructorSpec.name` and `.parent` before lazy
//! constructor exposure, without maintaining another type list.
//! Factories using that registry should not repeat `parent`: deleting a public
//! constructor or taking a fallback path does not remove registered inheritance.
//!
//! `WebApiInterface` and `WebApiFunctionTemplate` adapt native constructor
//! callbacks automatically. Hand-written templates can use
//! `web_api_constructor!("Interface", callback)` when construction does not
//! already go through a named object declaration. Only successful construction
//! brands the resulting native object; constructors and prototypes are not
//! instances. A factory implemented with a native Proxy must brand its target
//! and call `register_web_api_proxy` with its private handler before publication.
//! The handler belongs to that exact Proxy; author wrappers and revoked Proxies
//! fail identity checks without invoking traps. Registration grants no clone
//! capability. Native factories select names from their own descriptors, never
//! from JavaScript properties such as `new.target.name`. Include detached nodes,
//! workers, iterators, fallback events, and deserialization paths when adding a
//! factory. Receiver identity does not replace resource or realm authorization.
//!
//! Identity alone grants neither serialization nor transfer capability.
//! Structured-clone serializers route every identified object through their
//! explicit codecs and reject unsupported types, including nested instances.
//! Both message serialization and IndexedDB dispatch on the primary interface,
//! as required by the [HTML serialization and transfer rules]. A native derived
//! interface does not implicitly inherit a base interface's codec. The numeric
//! type IDs are isolate-local and must never be persisted or used as wire tags;
//! deserializers use their existing wire tags and native construction paths to
//! assign identity in the receiving isolate.
//!
//! [HTML serialization and transfer rules]: https://html.spec.whatwg.org/multipage/structured-data.html#serializable-objects
//!
//! The derive generates a Rust-side `new(...)` constructor by default. The
//! generated constructor takes every non-`()` declaration field as a named
//! argument and fills `()` declaration fields with `()`. If every field is
//! `()`, the generated constructor is `new()`. This keeps dynamic state
//! explicit while removing boilerplate such as `brand: ()` for fixed
//! initialized slots or accessor/method declaration fields. A field can declare
//! explicit while removing boilerplate for fixed initialized slots or
//! accessor/method declaration fields. A field can declare
//! `#[webapi(constructor_default = expr)]`, or bare
//! `#[webapi(constructor_default)]` for `Default::default()`, to keep a
//! Rust-side default out of the generated constructor while still installing
@@ -114,19 +159,22 @@
//!
//! # Receiver checks and Promise-returning members
//!
//! `receiver = path` on a method or `accessor_property` declares a native brand
//! predicate with signature `fn(&mut v8::PinScope, v8::Local<v8::Object>) -> bool`.
//! The generated callback checks it before running the implementation, throwing
//! `TypeError("Illegal invocation")` on failure. The predicate must inspect native
//! identity or private slots, not public constructors, prototypes, or properties;
//! it must not execute JavaScript or throw. This preserves cross-realm receivers
//! without accepting forged prototypes or Proxy wrappers.
//! `receiver = "Interface"` on a method or `accessor_property` generates an
//! `implements_interface` check before argument conversion and implementation.
//! Invalid receivers throw `TypeError("Illegal invocation")`. Native subtypes
//! and cross-realm objects are accepted even after their JavaScript prototypes
//! change. Forged prototypes, public properties, and author Proxies cannot pass.
//!
//! Struct-level `receiver = path` supplies the default for instance methods and
//! Struct-level `receiver` supplies the default for instance methods and
//! accessor properties in all three derives; a field can override it. Static
//! methods, data properties, and holder-based native data properties do not
//! inherit this policy. Already-built `getter_value` functions cannot use it.
//!
//! `receiver = path` remains available for a custom native predicate with
//! signature `fn(&mut v8::PinScope, v8::Local<v8::Object>) -> bool`. It must not
//! execute JavaScript or throw. Use it for checks that additionally require
//! native resource state; ordinary interface checks should use the string form.
//!
//! `returns_promise` on a method (including a static method) or accessor getter
//! converts synchronous exceptions from both the receiver check and the callback
//! into rejected Promises in the callback's realm. Successful return values are