fix(webidl): validate DOM receivers with native brands

Enable shared receiver checks for Document structure, CharacterData, Text,
ProcessingInstruction and affected HTML element templates. Keep registered
native Proxies valid EventTargets while rejecting author-created Proxies.

Preserve windowless node identity and UTF-16 data when routing CharacterData
through native handles, including Text/CDATA wholeText, live ranges and XPath
iterator invalidation. Validate raw reflected/event handler receivers before
conversion or mutation, with the LegacyLenientThis mouse event exceptions.

Cover cross-realm error constructors, forged and proxy receivers, conversion
ordering, native Proxy event dispatch and detached CharacterData mutation.
This commit is contained in:
ldm0
2026-09-17 01:00:47 +08:00
committed by Donough Liu
parent 59ed855b9d
commit f92c9bda50
19 changed files with 611 additions and 122 deletions
@@ -181,8 +181,8 @@ pub(in crate::native_bridge) use detached_objects::{
detached_clone_node_method_callback, detached_doctype_name, detached_doctype_public_id,
detached_doctype_system_id, detached_insert_before_method_callback,
detached_parent_node_object, detached_processing_instruction_target,
detached_remove_child_method_callback, detached_replace_child_method_callback,
detached_set_owner_document,
detached_record_tree_mutation, detached_remove_child_method_callback,
detached_replace_child_method_callback, detached_set_owner_document,
};
pub(in crate::native_bridge) use detached_objects::{
detached_attach_shadow_method_callback, detached_blur_method_callback,
@@ -330,7 +330,7 @@ struct DocumentMetadataPrototypeDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::Document, enumerable)]
#[webapi(interface = web_api_interfaces::Document, enumerable, receiver)]
struct DocumentStructurePrototypeDeclaration {
#[webapi(
accessor_property,
@@ -657,7 +657,7 @@ fn document_title_setter_function<'s>(
) {
let Some((runtime_ptr, handle)) = document_receiver_runtime_and_handle(scope, args.this())
else {
rv.set_undefined();
throw_type_error(scope, "Illegal invocation");
return;
};
let Some(value) = args.get(0).to_string(scope) else {
@@ -726,7 +726,7 @@ fn document_head_getter_function<'s>(
) {
let receiver = args.this();
let Some((runtime_ptr, handle)) = document_receiver_runtime_and_handle(scope, receiver) else {
rv.set_null();
throw_type_error(scope, "Illegal invocation");
return;
};
let runtime = unsafe { &*runtime_ptr };
@@ -49,7 +49,8 @@ pub(crate) use self::state_tree::{
pub(in crate::native_bridge) use self::state_tree::{
define_detached_native_handle, detached_doctype_name, detached_doctype_public_id,
detached_doctype_system_id, detached_parent_node_object,
detached_processing_instruction_target, detached_set_owner_document,
detached_processing_instruction_target, detached_record_tree_mutation,
detached_set_owner_document,
};
pub(crate) use self::state_tree::{
detached_native_handle_for_runtime, detached_native_object_for_handle,
@@ -1191,7 +1191,7 @@ pub(in crate::native_bridge::document) fn detached_tree_query_version<'s>(
value.uint32_value(scope).map(u64::from)
}
pub(in crate::native_bridge::document) fn detached_record_tree_mutation<'s>(
pub(in crate::native_bridge) fn detached_record_tree_mutation<'s>(
scope: &mut v8::PinScope<'s, '_>,
node: v8::Local<'s, v8::Object>,
) {
@@ -1546,7 +1546,7 @@ struct HtmlElementStandardPrototypeDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::HTMLElement)]
#[webapi(interface = web_api_interfaces::HTMLElement, receiver)]
struct HtmlElementActionPrototypeDeclaration {
#[webapi(method, length = 0, enumerable, callback = node_focus_callback)]
focus: (),
@@ -4136,7 +4136,7 @@ struct HtmlLinkElementUrlPrototypeDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::HTMLStyleElement, enumerable)]
#[webapi(interface = web_api_interfaces::HTMLStyleElement, enumerable, receiver)]
struct HtmlStyleElementPrototypeDeclaration {
#[webapi(
accessor_property,
@@ -5139,7 +5139,7 @@ struct HtmlOptionElementLabelPrototypeDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::HTMLTrackElement, enumerable)]
#[webapi(interface = web_api_interfaces::HTMLTrackElement, enumerable, receiver)]
struct HtmlTrackElementPrototypeDeclaration {
#[webapi(
accessor_property = "default",
@@ -1,6 +1,9 @@
use crate::{
document_runtime::EventTargetHandle,
util::{context_host_ptr_from_global_bridge, node_wrapper_from_handle, v8_string, v8str},
util::{
context_host_ptr_from_global_bridge, node_wrapper_from_handle, throw_type_error, v8_string,
v8str,
},
};
use super::super::super::node::{
@@ -208,14 +211,18 @@ fn document_event_handler_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(handler_name) = event_handler_name_from_data(scope, args.data()) else {
rv.set_undefined();
return;
};
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
rv.set_null();
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_is_document(unsafe { &*runtime_ptr }, handle) {
rv.set_null();
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
}
rv.set(event_handler_property_value_for_target(
@@ -231,18 +238,27 @@ fn document_event_handler_setter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if let Ok((runtime_ptr, handle)) =
let Some(handler_name) = event_handler_name_from_data(scope, args.data()) else {
rv.set_undefined();
return;
};
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
&& node_is_document(unsafe { &*runtime_ptr }, handle)
{
set_event_handler_property_for_target(
scope,
runtime_ptr,
EventTargetHandle::Node(handle),
args.data(),
args.get(0),
);
else {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_is_document(unsafe { &*runtime_ptr }, handle) {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
}
set_event_handler_property_for_target(
scope,
runtime_ptr,
EventTargetHandle::Node(handle),
args.data(),
args.get(0),
);
rv.set_undefined();
}
@@ -258,11 +274,11 @@ pub(crate) fn node_event_handler_getter_function<'s>(
let object = args.this();
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
rv.set_null();
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_is_element(unsafe { &*runtime_ptr }, handle) || !handler_name.starts_with("on") {
rv.set_null();
if !node_is_element(unsafe { &*runtime_ptr }, handle) {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
}
if let Some(event_type) = event_handler_event_type(&handler_name)
@@ -287,6 +303,10 @@ pub(crate) fn node_event_handler_getter_function<'s>(
rv.set(current);
return;
}
if !handler_name.starts_with("on") {
rv.set_null();
return;
}
let Some(source) = element_attribute(unsafe { &*runtime_ptr }, handle, &handler_name) else {
rv.set(v8::null(scope).into());
return;
@@ -432,16 +452,22 @@ pub(crate) fn node_event_handler_setter_function<'s>(
};
let object = args.this();
let value = args.get(0);
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_is_element(unsafe { &*runtime_ptr }, handle) {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
}
let stored = if value.is_function() {
value
} else {
v8::null(scope).into()
};
let _ = object.set(scope, slot_key.into(), stored);
let runtime_and_handle = node_runtime_and_handle_from_object_or_detached(scope, object).ok();
if let Some(event_type) = event_handler_event_type(&handler_name)
&& let Some((_runtime_ptr, handle)) = runtime_and_handle
{
if let Some(event_type) = event_handler_event_type(&handler_name) {
let handler = v8::Local::<v8::Function>::try_from(value).ok();
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
unsafe { &mut *host_ptr }.set_registered_event_handler_property(
@@ -453,7 +479,6 @@ pub(crate) fn node_event_handler_setter_function<'s>(
}
}
if matches!(handler_name.as_str(), "onload" | "onerror")
&& let Some((runtime_ptr, handle)) = runtime_and_handle
&& unsafe { &*runtime_ptr }
.dom_host()
.is_html_element_named(handle, "track")
@@ -481,6 +506,22 @@ fn event_handler_event_type(name: &str) -> Option<&str> {
.filter(|event_type| !event_type.is_empty())
}
fn legacy_lenient_this_event_handler(name: &str) -> bool {
matches!(name, "onmouseenter" | "onmouseleave")
}
fn handle_invalid_event_handler_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
rv: &mut v8::ReturnValue<'s, v8::Value>,
handler_name: &str,
) {
if legacy_lenient_this_event_handler(handler_name) {
rv.set_undefined();
} else {
throw_type_error(scope, "Illegal invocation");
}
}
pub(super) fn invalidate_node_event_attribute_handler(
runtime: &mut super::super::super::JsContextHost,
handle: crate::document_runtime::DomHandle,
@@ -2372,13 +2372,8 @@ pub(in crate::native_bridge) fn node_hidden_getter_function<'s>(
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
rv.set_undefined();
return;
};
if !node_is_element(unsafe { &*runtime_ptr }, handle) {
rv.set_undefined();
return;
}
match element_attribute(unsafe { &*runtime_ptr }, handle, "hidden") {
Some(value) if value.eq_ignore_ascii_case("until-found") => {
if let Some(value) = v8_string(scope, "until-found") {
@@ -2398,13 +2393,8 @@ pub(in crate::native_bridge) fn node_hidden_setter_function<'s>(
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
rv.set_undefined();
return;
};
if !node_is_element(unsafe { &*runtime_ptr }, handle) {
rv.set_undefined();
return;
}
let value = args.get(0);
if value.is_null_or_undefined() || value.is_boolean() || value.is_number() {
set_reflected_boolean_attribute(
@@ -1171,11 +1171,10 @@ pub(super) fn set_dom_string_attribute_property_on_object<'s>(
owner: &'static str,
property: &'static str,
) {
let Some(value) = property_dom_string_value(scope, value, owner, property) else {
let Some((runtime_ptr, handle)) = element_reflection_receiver_or_throw(scope, object) else {
return;
};
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
let Some(value) = property_dom_string_value(scope, value, owner, property) else {
return;
};
set_reflected_attribute(scope, runtime_ptr, handle, name, &value);
@@ -1208,15 +1207,9 @@ pub(super) fn attribute_property_getter_from_object_or_detached<'s>(
name: &str,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
rv.set_null();
let Some((runtime_ptr, handle)) = element_reflection_receiver_or_throw(scope, object) else {
return;
};
if !node_is_element(unsafe { &*runtime_ptr }, handle) {
rv.set_undefined();
return;
}
let value = element_attribute(unsafe { &*runtime_ptr }, handle, name).unwrap_or_default();
let Some(value) = v8_string(scope, &value) else {
rv.set_null();
@@ -19,5 +19,5 @@ pub(in crate::native_bridge) use helpers::{
};
pub(crate) use prototype::install_character_data_template_bindings;
pub(in crate::native_bridge) use text::{
node_split_text_callback, node_whole_text_value_from_object,
node_split_text_callback, node_whole_text_utf16_units_from_object,
};
@@ -1,11 +1,12 @@
use super::helpers::character_data_utf16_units;
use super::*;
pub(in crate::native_bridge) fn node_character_data_length_from_object(
scope: &mut v8::PinScope<'_, '_>,
object: v8::Local<'_, v8::Object>,
pub(in crate::native_bridge) fn node_character_data_length_from_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> i32 {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, object) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
return 0;
};
character_data_utf16_units(unsafe { &*runtime_ptr }, handle)
@@ -1,4 +1,4 @@
use crate::{context_bootstrap, webidl};
use crate::{context_bootstrap, native_bridge::document, webidl};
use super::helpers::{
character_data_utf16_units, dom_string_utf16_value_or_throw, set_utf16_return_value,
@@ -43,7 +43,8 @@ pub(in crate::native_bridge) fn node_append_data_callback<'s>(
if !require_argument_count(scope, &args, "CharacterData", "appendData", 1) {
return;
}
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args)
else {
return;
};
let Some(mut next) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else {
@@ -59,7 +60,9 @@ pub(in crate::native_bridge) fn node_append_data_callback<'s>(
};
next.extend_from_slice(&data);
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next);
if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) {
document::detached_record_tree_mutation(scope, args.this());
}
}
pub(in crate::native_bridge) fn node_delete_data_callback<'s>(
@@ -71,7 +74,8 @@ pub(in crate::native_bridge) fn node_delete_data_callback<'s>(
else {
return;
};
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args)
else {
return;
};
let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else {
@@ -86,7 +90,9 @@ pub(in crate::native_bridge) fn node_delete_data_callback<'s>(
next.extend_from_slice(&units[..start]);
next.extend_from_slice(&units[end..]);
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next);
if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) {
document::detached_record_tree_mutation(scope, args.this());
}
context_bootstrap::live_ranges_character_data_edit(
scope,
handle,
@@ -104,7 +110,8 @@ pub(in crate::native_bridge) fn node_insert_data_callback<'s>(
if !require_argument_count(scope, &args, "CharacterData", "insertData", 2) {
return;
}
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args)
else {
return;
};
let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else {
@@ -131,7 +138,9 @@ pub(in crate::native_bridge) fn node_insert_data_callback<'s>(
next.extend_from_slice(&insert);
next.extend_from_slice(&units[start..]);
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next);
if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) {
document::detached_record_tree_mutation(scope, args.this());
}
context_bootstrap::live_ranges_character_data_edit(
scope,
handle,
@@ -149,7 +158,8 @@ pub(in crate::native_bridge) fn node_replace_data_callback<'s>(
if !require_argument_count(scope, &args, "CharacterData", "replaceData", 3) {
return;
}
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args)
else {
return;
};
let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else {
@@ -184,7 +194,9 @@ pub(in crate::native_bridge) fn node_replace_data_callback<'s>(
next.extend_from_slice(&replacement);
next.extend_from_slice(&units[end..]);
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next);
if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) {
document::detached_record_tree_mutation(scope, args.this());
}
context_bootstrap::live_ranges_character_data_edit(
scope,
handle,
@@ -203,7 +215,8 @@ pub(in crate::native_bridge) fn node_substring_data_callback<'s>(
else {
return;
};
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args)
else {
return;
};
let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else {
@@ -1,6 +1,6 @@
use crate::web_api_interfaces;
use crate::{
native_bridge::{document, node::node_runtime_and_handle_from_args},
native_bridge::{document, node::node_runtime_and_handle_from_args_or_detached},
webidl,
};
use moli_webapi_declare::WebApiFunctionTemplate;
@@ -8,7 +8,7 @@ use moli_webapi_declare::WebApiFunctionTemplate;
use super::*;
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::CharacterData, enumerable)]
#[webapi(interface = web_api_interfaces::CharacterData, enumerable, receiver)]
struct CharacterDataPrototypeDeclaration {
#[webapi(
accessor_property,
@@ -31,7 +31,7 @@ struct CharacterDataPrototypeDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::Text, enumerable)]
#[webapi(interface = web_api_interfaces::Text, enumerable, receiver)]
struct TextPrototypeDeclaration {
#[webapi(accessor_property, getter = text_whole_text_getter_callback)]
whole_text: (),
@@ -40,17 +40,17 @@ struct TextPrototypeDeclaration {
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::ProcessingInstruction, enumerable)]
#[webapi(interface = web_api_interfaces::ProcessingInstruction, enumerable, receiver)]
struct ProcessingInstructionPrototypeDeclaration {
#[webapi(accessor_property, getter = processing_instruction_target_getter_callback)]
target: (),
}
fn receiver_is_live_node<'a>(
fn receiver_has_native_node<'a>(
scope: &mut v8::PinScope<'a, '_>,
args: &v8::FunctionCallbackArguments<'a>,
) -> bool {
node_runtime_and_handle_from_args(scope, args).is_ok()
node_runtime_and_handle_from_args_or_detached(scope, args).is_ok()
}
fn character_data_append_data_callback<'a>(
@@ -58,7 +58,7 @@ fn character_data_append_data_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
rv: v8::ReturnValue<'a, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
if receiver_has_native_node(scope, &args) {
node_append_data_callback(scope, args, rv);
} else {
document::detached_character_data_append_data_callback(scope, args, rv);
@@ -70,7 +70,7 @@ fn character_data_delete_data_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
rv: v8::ReturnValue<'a, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
if receiver_has_native_node(scope, &args) {
node_delete_data_callback(scope, args, rv);
} else {
document::detached_character_data_delete_data_callback(scope, args, rv);
@@ -82,7 +82,7 @@ fn character_data_insert_data_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
rv: v8::ReturnValue<'a, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
if receiver_has_native_node(scope, &args) {
node_insert_data_callback(scope, args, rv);
} else {
document::detached_character_data_insert_data_callback(scope, args, rv);
@@ -94,7 +94,7 @@ fn character_data_replace_data_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
rv: v8::ReturnValue<'a, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
if receiver_has_native_node(scope, &args) {
node_replace_data_callback(scope, args, rv);
} else {
document::detached_character_data_replace_data_callback(scope, args, rv);
@@ -106,7 +106,7 @@ fn character_data_substring_data_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
rv: v8::ReturnValue<'a, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
if receiver_has_native_node(scope, &args) {
node_substring_data_callback(scope, args, rv);
} else {
document::detached_character_data_substring_data_callback(scope, args, rv);
@@ -118,7 +118,7 @@ fn text_split_text_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
rv: v8::ReturnValue<'a, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
if receiver_has_native_node(scope, &args) {
node_split_text_callback(scope, args, rv);
} else {
document::detached_text_split_text_callback(scope, args, rv);
@@ -130,8 +130,9 @@ fn character_data_data_getter_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, args.this())
if receiver_has_native_node(scope, &args) {
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
rv.set_undefined();
return;
@@ -145,11 +146,7 @@ fn character_data_data_getter_callback<'a>(
super::helpers::set_utf16_return_value(scope, &mut rv, &units);
return;
}
let value = Some(document::detached_character_data_value(scope, args.this()));
let Some(value) = value else {
rv.set_undefined();
return;
};
let value = document::detached_character_data_value(scope, args.this());
let Some(value) = v8_string(scope, &value) else {
rv.set_null();
return;
@@ -162,7 +159,7 @@ fn character_data_data_setter_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if receiver_is_live_node(scope, &args) {
if receiver_has_native_node(scope, &args) {
let Some(value) = super::helpers::dom_string_utf16_value_or_throw(
scope,
args.get(0),
@@ -171,7 +168,8 @@ fn character_data_data_setter_callback<'a>(
) else {
return;
};
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, args.this())
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
return;
};
@@ -181,7 +179,9 @@ fn character_data_data_setter_callback<'a>(
.map(|units| units.len() as u32)
.unwrap_or(0);
let inserted_count = value.len() as u32;
let _ = runtime.set_character_data_utf16_units(scope, runtime_ptr, handle, &value);
if runtime.set_character_data_utf16_units(scope, runtime_ptr, handle, &value) {
document::detached_record_tree_mutation(scope, args.this());
}
crate::context_bootstrap::live_ranges_character_data_reset(
scope,
handle,
@@ -216,7 +216,7 @@ fn character_data_length_getter_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let length = if receiver_is_live_node(scope, &args) {
let length = if receiver_has_native_node(scope, &args) {
node_character_data_length_from_object(scope, args.this())
} else {
document::detached_character_data_length(scope, args.this())
@@ -229,12 +229,15 @@ fn text_whole_text_getter_callback<'a>(
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let value = if receiver_is_live_node(scope, &args) {
node_whole_text_value_from_object(scope, args.this())
} else {
document::detached_text_whole_text_value(scope, args.this())
};
let Some(value) = value else {
if receiver_has_native_node(scope, &args) {
let Some(units) = node_whole_text_utf16_units_from_object(scope, args.this()) else {
rv.set_undefined();
return;
};
super::helpers::set_utf16_return_value(scope, &mut rv, &units);
return;
}
let Some(value) = document::detached_text_whole_text_value(scope, args.this()) else {
rv.set_undefined();
return;
};
@@ -254,7 +257,8 @@ fn processing_instruction_target_getter_callback<'a>(
if let Some(value) = document::detached_processing_instruction_target(scope, args.this()) {
value
} else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, args.this())
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
webidl::throw_type_error(
scope,
@@ -1,25 +1,30 @@
use super::helpers::{character_data_string, require_argument_count, utf16_index_value_or_throw};
use super::helpers::{
character_data_string, character_data_utf16_units, require_argument_count,
utf16_index_value_or_throw,
};
use super::*;
use crate::{util::utf16_len, webidl};
use crate::{native_bridge::document, util::utf16_len, webidl};
use moli_dom::native::NodeType;
pub(in crate::native_bridge) fn node_whole_text_value_from_object(
scope: &mut v8::PinScope<'_, '_>,
object: v8::Local<'_, v8::Object>,
) -> Option<String> {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, object) else {
pub(in crate::native_bridge) fn node_whole_text_utf16_units_from_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<Vec<u16>> {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
return None;
};
let runtime = unsafe { &*runtime_ptr };
let dom = runtime.dom_host();
let node = dom.node(handle)?;
if !node.is_text() {
let is_text = |node: &Node| matches!(node.node_type(), NodeType::Text | NodeType::CDataSection);
if !is_text(node) {
return None;
}
let mut parts = Vec::new();
let mut units = Vec::new();
let mut start = handle;
while let Some(prev_id) = dom.node(start).and_then(|n| n.prev_sibling()) {
if dom.node(prev_id).is_some_and(Node::is_text) {
if dom.node(prev_id).is_some_and(is_text) {
start = prev_id;
} else {
break;
@@ -28,15 +33,13 @@ pub(in crate::native_bridge) fn node_whole_text_value_from_object(
let mut current = Some(start);
while let Some(h) = current {
let Some(n) = dom.node(h) else { break };
if !n.is_text() {
if !is_text(n) {
break;
}
if let Some(data) = n.node_value() {
parts.push(data.to_owned());
}
units.extend(character_data_utf16_units(runtime, h)?);
current = n.next_sibling();
}
Some(parts.join(""))
Some(units)
}
pub(in crate::native_bridge) fn node_split_text_callback<'s>(
@@ -47,7 +50,8 @@ pub(in crate::native_bridge) fn node_split_text_callback<'s>(
if !require_argument_count(scope, &args, "Text", "splitText", 1) {
return;
}
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else {
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args)
else {
rv.set_undefined();
return;
};
@@ -81,6 +85,7 @@ pub(in crate::native_bridge) fn node_split_text_callback<'s>(
rv.set_undefined();
return;
};
document::detached_record_tree_mutation(scope, args.this());
set_wrapped_handle_or_null_for_receiver(
scope,
&mut rv,
@@ -598,6 +598,15 @@ fn element_prototype_accessors_are_hookable() {
Object.defineProperty(copiedIframe, "src", iframeSrcDescriptor);
copiedIframe.src = "/copied.html";
const outcome = callback => {
try {
callback();
return "return";
} catch (error) {
return error.name;
}
};
return JSON.stringify({
clickGet: typeof clickDescriptor.get,
clickSet: typeof clickDescriptor.set,
@@ -610,7 +619,7 @@ fn element_prototype_accessors_are_hookable() {
divHandler: div.onclick === handler,
otherNull: other.onclick === null,
formHandler: form.onsubmit === handler,
prototypeNull: HTMLElement.prototype.onsubmit === null,
prototypeError: outcome(() => HTMLElement.prototype.onsubmit),
iframePrototypeSrc: HTMLIFrameElement.prototype.src,
iframeSrcValue: originalIframeSrcGet.apply(iframe, []),
iframeSrcAttribute: iframe.getAttribute("src"),
@@ -624,7 +633,7 @@ fn element_prototype_accessors_are_hookable() {
assert_eq!(
result,
r#"{"clickGet":"function","clickSet":"function","clickConfigurable":true,"submitGet":"function","submitSet":"function","iframeSrcGet":"function","iframeSrcSet":"function","iframeSrcConfigurable":true,"divHandler":true,"otherNull":true,"formHandler":true,"prototypeNull":true,"iframePrototypeSrc":"","iframeSrcValue":"https://element-prototype-accessors.test/child.html","iframeSrcAttribute":"/child.html","copiedIframeSrcValue":"https://element-prototype-accessors.test/copied.html","copiedIframeSrcAttribute":"/copied.html"}"#
r#"{"clickGet":"function","clickSet":"function","clickConfigurable":true,"submitGet":"function","submitSet":"function","iframeSrcGet":"function","iframeSrcSet":"function","iframeSrcConfigurable":true,"divHandler":true,"otherNull":true,"formHandler":true,"prototypeError":"TypeError","iframePrototypeSrc":"","iframeSrcValue":"https://element-prototype-accessors.test/child.html","iframeSrcAttribute":"/child.html","copiedIframeSrcValue":"https://element-prototype-accessors.test/copied.html","copiedIframeSrcAttribute":"/copied.html"}"#
);
}
@@ -3537,7 +3537,7 @@ fn webidl_attribute_setters_preserve_undefined_and_replaceable_semantics() {
return JSON.stringify({
animation: [animation.id, outcome(() => animationId.set.call({}))].join(","),
document: [outcome(() => bodySetter.call({})), detached.fullscreenEnabled].join(","),
lenient: [outcome(() => mouseEnter.set.call({})), element.onmouseenter].join(","),
eventHandler: [outcome(() => mouseEnter.set.call({})), element.onmouseenter].join(","),
replaceableShape,
replacements: [window.scrollX, window.screenLeft, window.screenTop].join(","),
failures: [selfFailure, screenFailure].join(",")
@@ -3549,7 +3549,84 @@ fn webidl_attribute_setters_preserve_undefined_and_replaceable_semantics() {
assert_eq!(
result,
r#"{"animation":"undefined,TypeError","document":"TypeError,false","lenient":"return,","replaceableShape":true,"replacements":",,foo","failures":"TypeError,TypeError"}"#
r#"{"animation":"undefined,TypeError","document":"TypeError,false","eventHandler":"return,","replaceableShape":true,"replacements":",,foo","failures":"TypeError,TypeError"}"#
);
}
#[test]
fn legacy_lenient_this_event_handlers_ignore_incompatible_receivers() {
let mut vm = new_storage_test_vm("https://legacy-lenient-this.test/");
let result = vm
.eval(
r#"
(() => {
const text = document.createTextNode("text");
const invalidObject = {};
const element = document.createElement("div");
const shadow = element.attachShadow({mode: "open"});
const invalidReceivers = [
undefined, null, 1, invalidObject, text, shadow,
Object.create(element), new Proxy(element, {}), new Proxy(document, {})
];
const lenientDescriptors = [
Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onmouseenter"),
Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onmouseleave"),
Object.getOwnPropertyDescriptor(Document.prototype, "onmouseenter"),
Object.getOwnPropertyDescriptor(Document.prototype, "onmouseleave")
];
const lenient = lenientDescriptors.every(descriptor =>
invalidReceivers.every(receiver =>
descriptor.get.call(receiver) === undefined &&
descriptor.set.call(receiver) === undefined &&
descriptor.set.call(receiver, undefined) === undefined &&
descriptor.set.call(receiver, "ignored") === undefined
)
);
const strict = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onclick");
const documentStrict = Object.getOwnPropertyDescriptor(Document.prototype, "onclick");
const outcome = callback => {
try {
callback();
return "return";
} catch (error) {
return error && error.name;
}
};
let documentCalls = 0;
const handler = () => documentCalls++;
documentStrict.set.call(document, handler);
const documentHandlerPreserved = documentStrict.get.call(document) === handler;
document.dispatchEvent(new Event("click"));
documentStrict.set.call(document, null);
document.dispatchEvent(new Event("click"));
return [
lenient,
outcome(() => strict.get.call({})),
outcome(() => strict.set.call({})),
outcome(() => strict.get.call(text)),
outcome(() => strict.set.call(text)),
outcome(() => strict.get.call(shadow)),
outcome(() => strict.set.call(shadow)),
outcome(() => documentStrict.get.call(element)),
outcome(() => documentStrict.set.call(element)),
outcome(() => documentStrict.get.call({})),
outcome(() => documentStrict.set.call({})),
Object.getOwnPropertyNames(invalidObject).length,
documentHandlerPreserved,
documentCalls
].join("|");
})()
"#,
)
.expect("LegacyLenientThis event handler probe should evaluate");
assert_eq!(
result,
"true|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|0|true|1"
);
}
@@ -6643,10 +6643,21 @@ fn detached_global_event_handler_accessors_use_owner_prototypes() {
assert(descriptor.configurable === true, `${name} configurable`);
return descriptor;
};
const throwsTypeError = callback => {
try {
callback();
return false;
} catch (error) {
return error instanceof TypeError;
}
};
const click = accessor(HTMLElement.prototype, "onclick");
accessor(HTMLElement.prototype, "onsubmit");
assert(HTMLElement.prototype.onclick === null, "HTMLElement.prototype.onclick default");
assert(
throwsTypeError(() => HTMLElement.prototype.onclick),
"HTMLElement.prototype.onclick receiver brand"
);
assert(Object.getOwnPropertyDescriptor(HTMLBodyElement.prototype, "onload").get !==
Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onload").get,
"HTMLBodyElement.onload should keep body/window override");
@@ -6671,7 +6682,7 @@ fn detached_global_event_handler_accessors_use_owner_prototypes() {
assert(element[name] === handler, `${label}.${name} after delete`);
}
assert(click.get.call({}) === null, "forged getter is lenient null");
assert(throwsTypeError(() => click.get.call({})), "forged getter receiver brand");
return "ok";
})()
"#,
@@ -1,5 +1,88 @@
use super::*;
#[test]
fn cross_realm_dom_bindings_reject_incompatible_receivers_in_their_own_realm() {
let mut vm = new_storage_test_vm("https://cross-realm-dom-receivers.test/");
vm.eval(
r#"
(() => {
const root = document.documentElement ||
document.appendChild(document.createElement("html"));
const body = document.body || root.appendChild(document.createElement("body"));
const frame = document.createElement("iframe");
frame.id = "cross-realm-dom-receivers";
body.appendChild(frame);
})()
"#,
)
.expect("cross-realm receiver child frame should be created");
materialize_single_child_default_realm_for_test(
&mut vm,
"cross-realm DOM receiver child Realm",
);
let result = vm
.eval(
r#"
(() => {
const other = document.getElementById("cross-realm-dom-receivers").contentWindow;
const notElement = Object.create(other.HTMLElement.prototype);
const notText = Object.create(other.Text.prototype);
const notDocument = Object.create(other.HTMLDocument.prototype);
const element = other.document.createElement("button");
const text = other.document.createTextNode("foo");
const outcome = callback => {
try {
callback();
return "no throw";
} catch (error) {
return [
error.name,
error instanceof other.TypeError,
error instanceof TypeError
].join(":");
}
};
return [
outcome(() => { Object.create(other.document).head; }),
outcome(() => {
Object.getOwnPropertyDescriptor(other.HTMLElement.prototype, "title")
.get.call(notElement);
}),
outcome(() => {
Reflect.get(other.document.createElement("div"), "hidden", notElement);
}),
outcome(() => { new Proxy(text, {}).nodeType; }),
outcome(() => { Object.create(element).innerHTML = ""; }),
outcome(() => {
Object.getOwnPropertyDescriptor(other.HTMLElement.prototype, "onclick")
.set.call(notElement, null);
}),
outcome(() => { Reflect.set(new other.Text("foo"), "data", "foo", notText); }),
outcome(() => { new Proxy(other.document, {}).title = ""; }),
outcome(() => { Object.create(element).click(); }),
outcome(() => { other.document.querySelector.call(notDocument, "*"); }),
outcome(() => { Reflect.apply(text.remove, notText, []); }),
outcome(() => {
new Proxy(other.document.createElement("a"), {})
.addEventListener("foo", () => {});
})
].join("|");
})()
"#,
)
.expect("cross-realm receiver brand checks should evaluate");
assert_eq!(
result,
std::iter::repeat_n("TypeError:true:false", 12)
.collect::<Vec<_>>()
.join("|")
);
}
#[test]
fn document_compat_mode_reflects_parser_quirks_mode() {
let cases = [
@@ -4163,13 +4246,24 @@ fn global_event_handler_accessors_live_on_owner_prototypes() {
assert(descriptor.configurable === true, `${name} configurable`);
return descriptor;
};
const throwsTypeError = callback => {
try {
callback();
return false;
} catch (error) {
return error instanceof TypeError;
}
};
const click = accessor(HTMLElement.prototype, "onclick");
const submit = accessor(HTMLElement.prototype, "onsubmit");
const load = accessor(HTMLElement.prototype, "onload");
assert(HTMLElement.prototype.onclick === null, "HTMLElement.prototype.onclick default");
assert(HTMLElement.prototype.onsubmit === null, "HTMLElement.prototype.onsubmit default");
assert(HTMLElement.prototype.onload === null, "HTMLElement.prototype.onload default");
assert(throwsTypeError(() => click.get.call(HTMLElement.prototype)),
"HTMLElement.prototype.onclick receiver brand");
assert(throwsTypeError(() => submit.get.call(HTMLElement.prototype)),
"HTMLElement.prototype.onsubmit receiver brand");
assert(throwsTypeError(() => load.get.call(HTMLElement.prototype)),
"HTMLElement.prototype.onload receiver brand");
assert(Object.getOwnPropertyDescriptor(HTMLBodyElement.prototype, "onload").get !== load.get,
"HTMLBodyElement.onload should keep body/window override");
@@ -1,5 +1,7 @@
use super::*;
mod dom;
#[test]
fn webidl_receiver_checks_reject_prototypes_plain_objects_and_forged_instances() {
let mut vm = new_storage_test_vm("https://receiver-check.test/");
@@ -0,0 +1,246 @@
use super::*;
#[test]
fn dom_receiver_templates_reject_invalid_interfaces_before_conversion() {
let mut vm = new_storage_test_vm("https://dom-receiver-templates.test/");
let result = vm.eval(r#"
(() => {
const failures = [];
let conversions = 0;
const value = {toString() { conversions++; return 'changed'; }, valueOf() { conversions++; return 0; }};
const element = document.createElement('div');
const text = document.createTextNode('original');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const track = document.createElement('track');
const style = document.createElement('style');
const cases = [];
for (const key of ['title', 'head', 'body', 'doctype', 'documentElement']) {
cases.push([Document, key, 'get', document, element, []]);
}
cases.push([Document, 'title', 'set', document, element, [value]]);
for (const key of ['data', 'length']) {
cases.push([CharacterData, key, 'get', text, element, []]);
}
cases.push([CharacterData, 'data', 'set', text, element, [value]]);
for (const key of ['appendData', 'insertData', 'deleteData', 'replaceData', 'substringData']) {
cases.push([CharacterData, key, 'method', text, element, [value, value, value]]);
}
cases.push([Text, 'wholeText', 'get', text, document.createComment('comment'), []]);
cases.push([Text, 'splitText', 'method', text, document.createComment('comment'), [value]]);
for (const key of ['click', 'focus', 'blur']) {
cases.push([HTMLElement, key, 'method', element, svg, []]);
}
for (const [C, key, real] of [[HTMLTrackElement, 'srclang', track], [HTMLStyleElement, 'media', style]]) {
cases.push([C, key, 'get', real, element, []]);
cases.push([C, key, 'set', real, element, [value]]);
}
let traps = 0;
for (const [C, key, kind, real, wrong, args] of cases) {
const descriptor = Object.getOwnPropertyDescriptor(C.prototype, key);
const fn = kind === 'method' ? descriptor.value : descriptor[kind];
const revoked = Proxy.revocable(real, {});
revoked.revoke();
const authorProxy = new Proxy(real, {get() { traps++; throw new Error('proxy trap'); }});
for (const receiver of [wrong, {}, C.prototype, Object.create(C.prototype), Object.create(real), authorProxy, revoked.proxy, null]) {
try {
Reflect.apply(fn, receiver, args);
failures.push(`${C.name}.${key} ${kind}: accepted`);
} catch (error) {
if (!(error instanceof TypeError)) failures.push(`${C.name}.${key} ${kind}: ${error.name}`);
}
}
}
if (text.data !== 'original') failures.push('mutated real target');
return JSON.stringify([conversions, traps, failures]);
})()
"#).unwrap();
assert_eq!(result, "[0,0,[]]");
}
#[test]
fn dom_receiver_templates_preserve_cross_realm_native_identity() {
let mut vm = new_storage_test_vm("https://dom-receiver-realms.test/");
vm.eval(
r#"
const root = document.appendChild(document.createElement('html'));
const body = root.appendChild(document.createElement('body'));
body.appendChild(document.createElement('iframe')).id = 'receiver-frame';
"#,
)
.unwrap();
materialize_single_child_default_realm_for_test(&mut vm, "DOM receiver template child Realm");
let result = vm.eval(r#"
(() => {
const child = document.getElementById('receiver-frame').contentWindow;
const failures = [];
for (const [callee, owner] of [[window, child], [child, window]]) {
const doc = owner.document.implementation.createHTMLDocument('original');
const title = Object.getOwnPropertyDescriptor(callee.Document.prototype, 'title');
const data = Object.getOwnPropertyDescriptor(callee.CharacterData.prototype, 'data');
const length = Object.getOwnPropertyDescriptor(callee.CharacterData.prototype, 'length').get;
const text = doc.createTextNode('a');
const button = doc.createElement('button');
const click = callee.HTMLElement.prototype.click;
const hidden = Object.getOwnPropertyDescriptor(callee.HTMLElement.prototype, 'hidden');
Object.setPrototypeOf(text, null);
Object.setPrototypeOf(button, null);
Object.setPrototypeOf(doc, null);
title.set.call(doc, 'changed');
if (title.get.call(doc) !== 'changed') failures.push('Document native identity');
data.set.call(text, 'a\uD800');
callee.CharacterData.prototype.appendData.call(text, 'b');
if (data.get.call(text) !== 'a\uD800b' || length.call(text) !== 3) failures.push('CharacterData native identity');
hidden.set.call(button, true);
if (hidden.get.call(button) !== true) failures.push('HTMLElement native identity');
click.call(button);
for (const invoke of [
() => title.set.call({}, 'bad'),
() => length.call({}),
() => callee.CharacterData.prototype.appendData.call({}, 'bad'),
() => click.call({})
]) {
try { invoke(); failures.push('accepted invalid cross-realm receiver'); }
catch (error) {
if (!(error instanceof callee.TypeError) || error instanceof owner.TypeError) failures.push('wrong error realm');
}
}
}
return JSON.stringify(failures);
})()
"#).unwrap();
assert_eq!(result, "[]");
}
#[test]
fn dom_receiver_templates_allow_registered_native_proxy_event_targets() {
let mut vm = new_storage_test_vm("https://native-proxy-event-target.test/");
vm.eval(
r#"
globalThis.nativeReceiver = document.implementation.createHTMLDocument('').createElement('select');
"#,
)
.unwrap();
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, "nativeReceiver").unwrap();
let value = global.get(scope, key.into()).unwrap();
assert!(
value.is_proxy(),
"fixture must exercise a registered native Proxy"
);
let object = v8::Local::<v8::Object>::try_from(value).unwrap();
assert!(crate::web_api_interfaces::EventTarget::is_instance(
scope, object
));
Ok(())
})
.unwrap();
let result = vm.eval(r#"
(() => {
const receiver = nativeReceiver;
const failures = [];
const {addEventListener: add, removeEventListener: remove, dispatchEvent: dispatch} = EventTarget.prototype;
let calls = 0;
const listener = () => calls++;
add.call(receiver, 'test', listener);
if (dispatch.call(receiver, new Event('test')) !== true) failures.push('first dispatch');
remove.call(receiver, 'test', listener);
if (dispatch.call(receiver, new Event('test')) !== true) failures.push('second dispatch');
if (calls !== 1) failures.push(`listener calls: ${calls}`);
let conversions = 0;
const type = {toString() { conversions++; return 'test'; }};
for (const proxy of [new Proxy(receiver, {}), Object.create(receiver)]) {
for (const invoke of [() => add.call(proxy, type, listener), () => remove.call(proxy, type, listener), () => dispatch.call(proxy, new Event('test'))]) {
try { invoke(); failures.push('accepted author receiver'); }
catch (error) { if (!(error instanceof TypeError)) failures.push(error.name); }
}
}
if (conversions !== 0) failures.push('converted author receiver arguments');
return JSON.stringify(failures);
})()
"#).unwrap();
assert_eq!(result, "[]");
}
#[test]
fn dom_receiver_templates_whole_text_preserves_cdata_and_utf16() {
let mut vm = new_parsed_test_vm(
"https://dom-receiver-whole-text.test/",
"<html><body></body></html>",
);
let result = vm
.eval(
r#"
(() => {
const failures = [];
const xml = document.implementation.createDocument(null, 'root');
const get = Object.getOwnPropertyDescriptor(Text.prototype, 'wholeText').get;
const data = Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data');
for (const doc of [document, document.implementation.createHTMLDocument(''), xml]) {
const parent = doc.createElement('section');
(doc.body || doc.documentElement).appendChild(parent);
const left = doc.createTextNode('a');
const middle = doc.adoptNode(xml.createCDATASection('b'));
const right = doc.createTextNode('c');
parent.append(left, middle, right);
for (const node of [left, middle, right]) {
if (get.call(node) !== 'abc') failures.push('contiguous Text and CDATA');
}
data.set.call(left, 'a\uD800');
data.set.call(middle, '\uDC00b');
data.set.call(right, 'c\uD800');
for (const node of [left, middle, right]) {
if (get.call(node) !== 'a\uD800\uDC00bc\uD800') failures.push('UTF-16 across nodes');
}
const barrier = parent.insertBefore(doc.createComment('barrier'), middle);
if (get.call(left) !== 'a\uD800') failures.push('left boundary');
for (const node of [middle, right]) {
if (get.call(node) !== '\uDC00bc\uD800') failures.push('right boundary');
}
parent.removeChild(barrier);
if (get.call(middle) !== 'a\uD800\uDC00bc\uD800') failures.push('removed boundary');
}
return JSON.stringify(failures);
})()
"#,
)
.unwrap();
assert_eq!(result, "[]");
}
#[test]
fn dom_receiver_templates_character_data_edits_invalidate_detached_xpath_iterators() {
let mut vm = new_storage_test_vm("https://dom-receiver-text-mutations.test/");
let result = vm.eval(r#"
(() => {
const failures = [];
const iterate = doc => doc.evaluate('//div', doc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
const otherDoc = document.implementation.createHTMLDocument('');
const other = iterate(otherDoc);
for (const [name, edit, expected] of [
['data', text => { text.data = 'after'; }, 'after'],
['appendData', text => text.appendData('!'), 'before!'],
['insertData', text => text.insertData(1, '!'), 'b!efore'],
['deleteData', text => text.deleteData(1, 2), 'bore'],
['replaceData', text => text.replaceData(1, 2, '!'), 'b!ore'],
['splitText', text => text.splitText(3), 'before']
]) {
const doc = document.implementation.createHTMLDocument('');
const parent = doc.body.appendChild(doc.createElement('div'));
const text = parent.appendChild(doc.createTextNode('before'));
const iterator = iterate(doc);
const snapshot = doc.evaluate('//div', doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
edit(text);
if (!iterator.invalidIteratorState) failures.push(`${name}: iterator remained valid`);
try { iterator.iterateNext(); failures.push(`${name}: iterateNext succeeded`); }
catch (error) { if (error.name !== 'InvalidStateError') failures.push(`${name}: ${error.name}`); }
if (snapshot.invalidIteratorState || snapshot.snapshotItem(0) !== parent) failures.push(`${name}: snapshot changed`);
const value = doc.evaluate('string(//div)', doc, null, XPathResult.STRING_TYPE).stringValue;
if (value !== expected) failures.push(`${name}: stale data ${value}`);
}
if (other.invalidIteratorState) failures.push('invalidated unrelated document');
return JSON.stringify(failures);
})()
"#).unwrap();
assert_eq!(result, "[]");
}
+3 -1
View File
@@ -296,6 +296,7 @@ pub(super) fn event_target_add_event_listener_callback<'s>(
event_target_handle_from_this(scope, &args, host_ptr, host)
};
let Some(target) = target else {
throw_type_error(scope, "Illegal invocation");
return;
};
let passive = call
@@ -400,6 +401,7 @@ pub(super) fn event_target_remove_event_listener_callback<'s>(
event_target_handle_from_this(scope, &args, host_ptr, host)
};
let Some(target) = target else {
throw_type_error(scope, "Illegal invocation");
return;
};
host.remove_registered_event_listener(scope, target, &call.event_type, call.callback, capture);
@@ -426,7 +428,7 @@ pub(super) fn event_target_dispatch_event_callback<'s>(
None
};
if child_window_target.is_none() && target.is_none() {
rv.set_bool(false);
throw_type_error(scope, "Illegal invocation");
return;
};
let event_value = args.get(0);