diff --git a/moli-renderer-v8/src/blob.rs b/moli-renderer-v8/src/blob.rs index 6da24d2930..27febbab6d 100644 --- a/moli-renderer-v8/src/blob.rs +++ b/moli-renderer-v8/src/blob.rs @@ -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 { - 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), diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 8fd68e1d7e..2205898d73 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs b/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs index ab185f2d1f..be2b82056b 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs @@ -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")] diff --git a/moli-renderer-v8/src/context_bootstrap/event_template.rs b/moli-renderer-v8/src/context_bootstrap/event_template.rs index 2d83bb0bf9..6c2f33c527 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_template.rs @@ -280,7 +280,7 @@ struct PointerEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(name = "EventTarget", enumerable)] +#[webapi(name = "EventTarget", enumerable, receiver = "EventTarget")] struct EventTargetTemplateMethodsDeclaration { #[webapi( method = "addEventListener", diff --git a/moli-renderer-v8/src/context_bootstrap/file_api/file.rs b/moli-renderer-v8/src/context_bootstrap/file_api/file.rs index d4c635d99a..90470cf0b4 100644 --- a/moli-renderer-v8/src/context_bootstrap/file_api/file.rs +++ b/moli-renderer-v8/src/context_bootstrap/file_api/file.rs @@ -222,15 +222,13 @@ pub(crate) fn selected_file_from_object<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, ) -> Option { - 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); diff --git a/moli-renderer-v8/src/context_bootstrap/file_api/file_reader/events.rs b/moli-renderer-v8/src/context_bootstrap/file_api/file_reader/events.rs index 3118ca09c5..9ab1984ec7 100644 --- a/moli-renderer-v8/src/context_bootstrap/file_api/file_reader/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/file_api/file_reader/events.rs @@ -3,7 +3,7 @@ use moli_webapi_declare::WebApiObject; #[derive(WebApiObject)] #[webapi( - interface = "Object", + interface = "ProgressEvent", prototype = "Object", scope_lifetime = 'scope, data_properties, enumerable diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs index 77cdda0f01..b1d472ed4c 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/clone.rs @@ -48,52 +48,61 @@ impl v8::ValueSerializerImpl for IndexedDbStructuredCloneSerializer { object: v8::Local<'s, v8::Object>, serializer: &dyn v8::ValueSerializerHelper, ) -> Option { - 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, diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/install.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/install.rs index 18ceb5b327..c5382b14e7 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/install.rs @@ -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: (), diff --git a/moli-renderer-v8/src/context_bootstrap/opfs.rs b/moli-renderer-v8/src/context_bootstrap/opfs.rs index e8acaea915..53d0dc4967 100644 --- a/moli-renderer-v8/src/context_bootstrap/opfs.rs +++ b/moli-renderer-v8/src/context_bootstrap/opfs.rs @@ -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 { - 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); diff --git a/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs b/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs index e71931277f..03e55ff7fb 100644 --- a/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/resize_observer_runtime.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/shared/clone.rs b/moli-renderer-v8/src/context_bootstrap/shared/clone.rs index e936a66afa..b2881098ab 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared/clone.rs @@ -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::::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::::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::::try_from(candidate) - && is_readable_stream_object(scope, stream) - { - seen.push(stream.into()); - readable_streams.push(stream); - continue; - } - if let Ok(stream) = v8::Local::::try_from(candidate) - && is_writable_stream_object(scope, stream) - { - seen.push(stream.into()); - writable_streams.push(stream); - continue; - } - if let Ok(stream) = v8::Local::::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."); diff --git a/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs b/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs index 6bf2fb41f9..bdd8a98bea 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared_worker_host.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/streams.rs b/moli-renderer-v8/src/context_bootstrap/streams.rs index d12882c3ba..f769a773df 100644 --- a/moli-renderer-v8/src/context_bootstrap/streams.rs +++ b/moli-renderer-v8/src/context_bootstrap/streams.rs @@ -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>, diff --git a/moli-renderer-v8/src/context_bootstrap/streams/readable.rs b/moli-renderer-v8/src/context_bootstrap/streams/readable.rs index 7b6a29f40b..64102cdc8c 100644 --- a/moli-renderer-v8/src/context_bootstrap/streams/readable.rs +++ b/moli-renderer-v8/src/context_bootstrap/streams/readable.rs @@ -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>, @@ -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; } diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs index b8ce2f6630..9c133dc6ac 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -971,7 +971,7 @@ pub(super) fn svg_transform_value_or_throw<'s>( ) -> Option> { let object = v8::Local::::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> { let object = v8::Local::::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); } diff --git a/moli-renderer-v8/src/context_bootstrap/web_audio_runtime.rs b/moli-renderer-v8/src/context_bootstrap/web_audio_runtime.rs index 3d1514c672..ded8499b64 100644 --- a/moli-renderer-v8/src/context_bootstrap/web_audio_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/web_audio_runtime.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/websocket/events.rs b/moli-renderer-v8/src/context_bootstrap/websocket/events.rs index a6c5f51623..27f2148d07 100644 --- a/moli-renderer-v8/src/context_bootstrap/websocket/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/websocket/events.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs index 033ceaaabd..04749e8f4e 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/service_worker.rs @@ -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>, diff --git a/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs index 991c3a4ddc..31fbeb38a5 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs @@ -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, diff --git a/moli-renderer-v8/src/custom_elements/registry_install.rs b/moli-renderer-v8/src/custom_elements/registry_install.rs index a62c01b78c..8f9e6c6178 100644 --- a/moli-renderer-v8/src/custom_elements/registry_install.rs +++ b/moli-renderer-v8/src/custom_elements/registry_install.rs @@ -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>, diff --git a/moli-renderer-v8/src/custom_elements/registry_runtime.rs b/moli-renderer-v8/src/custom_elements/registry_runtime.rs index fe5ad92005..4667f2f54d 100644 --- a/moli-renderer-v8/src/custom_elements/registry_runtime.rs +++ b/moli-renderer-v8/src/custom_elements/registry_runtime.rs @@ -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::::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( diff --git a/moli-renderer-v8/src/native_bridge/collections.rs b/moli-renderer-v8/src/native_bridge/collections.rs index ab3f90f533..eba9754d67 100644 --- a/moli-renderer-v8/src/native_bridge/collections.rs +++ b/moli-renderer-v8/src/native_bridge/collections.rs @@ -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>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index fa8bf834a9..314494b589 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -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>, diff --git a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/iframe_window.rs b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/iframe_window.rs index 20f24b160d..f7812c7da4 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/iframe_window.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_install/accessors/iframe_window.rs @@ -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: (), diff --git a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/dom_implementation.rs b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/dom_implementation.rs index e63f280f7a..e741ab4ac4 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/dom_implementation.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/dom_implementation.rs @@ -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::::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::::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>( diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 05c92ac4fa..823c1cb65a 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -1410,7 +1410,7 @@ struct ElementStylePrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(name = "HTMLElement")] +#[webapi(name = "HTMLElement", receiver = "HTMLElement")] struct HtmlElementStandardPrototypeDeclaration { #[webapi( accessor_property, diff --git a/moli-renderer-v8/src/native_bridge/mod.rs b/moli-renderer-v8/src/native_bridge/mod.rs index 71c9a54aff..5f9cdf755f 100644 --- a/moli-renderer-v8/src/native_bridge/mod.rs +++ b/moli-renderer-v8/src/native_bridge/mod.rs @@ -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, diff --git a/moli-renderer-v8/src/native_bridge/node/tree/methods.rs b/moli-renderer-v8/src/native_bridge/node/tree/methods.rs index 52a6bb1a53..9fce671b25 100644 --- a/moli-renderer-v8/src/native_bridge/node/tree/methods.rs +++ b/moli-renderer-v8/src/native_bridge/node/tree/methods.rs @@ -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::::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 diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index d3f2f16b4a..f42bbc4dae 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -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; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/platform_identity.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/platform_identity.rs new file mode 100644 index 0000000000..cf59bf8b16 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/platform_identity.rs @@ -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::::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::::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" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs index da59804667..b068638fc1 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs @@ -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" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs index 7548f0996b..8b1620a6b6 100644 --- a/moli-renderer-v8/src/script_vm/tests/indexed_db.rs +++ b/moli-renderer-v8/src/script_vm/tests/indexed_db.rs @@ -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/"); diff --git a/moli-renderer-v8/src/structured_clone.rs b/moli-renderer-v8/src/structured_clone.rs index eeccae96ee..dc527fef7d 100644 --- a/moli-renderer-v8/src/structured_clone.rs +++ b/moli-renderer-v8/src/structured_clone.rs @@ -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 { - 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 diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 0c83ad35ae..1549ff960a 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -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, diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index 4969248827..270599aa79 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -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 } diff --git a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs index 0eb95d5ff7..43c461caf9 100644 --- a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs @@ -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"}"# + ); +} diff --git a/moli-webapi-declare/src/brand.rs b/moli-webapi-declare/src/brand.rs index 915024ec89..e971bf5d58 100644 --- a/moli-webapi-declare/src/brand.rs +++ b/moli-webapi-declare/src/brand.rs @@ -92,10 +92,21 @@ pub fn register_web_api_interfaces( 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 = ®istry.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 { - let registry = registry(scope); + let registry = scope.get_slot::>>()?.clone(); let id = object_type_id(scope, object, ®istry)?; 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::>>().cloned() else { + return false; + }; let Some(id) = object_type_id(scope, object, ®istry) else { return false; }; diff --git a/moli-webapi-declare/src/lib.rs b/moli-webapi-declare/src/lib.rs index 21f1b781a5..0320c062d8 100644 --- a/moli-webapi-declare/src/lib.rs +++ b/moli-webapi-declare/src/lib.rs @@ -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) -> 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) -> 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