mirror of
https://github.com/lexmount/moli.git
synced 2026-09-29 00:01:38 +00:00
refactor(webapi): complete identity migration and codec dispatch
This commit is contained in:
@@ -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,
|
||||
|
||||
+6
-1
@@ -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>,
|
||||
|
||||
+5
-1
@@ -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: (),
|
||||
|
||||
+3
-13
@@ -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,
|
||||
|
||||
@@ -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/");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"}"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = ®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<WebApiType> {
|
||||
let registry = registry(scope);
|
||||
let registry = scope.get_slot::<Rc<RefCell<TypeRegistry>>>()?.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::<Rc<RefCell<TypeRegistry>>>().cloned() else {
|
||||
return false;
|
||||
};
|
||||
let Some(id) = object_type_id(scope, object, ®istry) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user