From 376fc2d6fb298b8c4e40f3d704298a45e371fef6 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 3 Oct 2026 10:23:55 +0800 Subject: [PATCH] fix(events): convert and brand ErrorEvent payloads --- .../src/context_bootstrap/event_template.rs | 36 +- .../events/subclasses/constructor.rs | 513 ++++++++---------- .../tests/browser_api/history_worlds.rs | 7 +- .../src/script_vm/tests/browser_api/mod.rs | 1 + 4 files changed, 251 insertions(+), 306 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/event_template.rs b/moli-renderer-v8/src/context_bootstrap/event_template.rs index 24b72307da..273189c244 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_template.rs @@ -294,6 +294,25 @@ struct InputEventTemplateAccessorsDeclaration { input_type: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::ErrorEvent, enumerable, receiver)] +struct ErrorEventTemplateAccessorsDeclaration { + #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "message"))] + message: (), + + #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "filename"))] + filename: (), + + #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "lineno"))] + lineno: (), + + #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "colno"))] + colno: (), + + #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "error"))] + error: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::StorageEvent, enumerable, receiver)] struct StorageEventTemplateMethodsDeclaration { @@ -502,24 +521,7 @@ struct SecurityPolicyViolationEventTemplateAccessorsDeclaration { column_number: (), } -#[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::ErrorEvent, enumerable, receiver)] -struct ErrorEventTemplateAccessorsDeclaration { - #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "message"))] - message: (), - #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "filename"))] - filename: (), - - #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "lineno"))] - lineno: (), - - #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "colno"))] - colno: (), - - #[webapi(accessor_property, getter = event_value_attribute_getter, data = crate::util::v8str(scope, "error"))] - error: (), -} struct EventTemplateDeclaration { diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs index d0aaeefd05..89a2128be7 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs @@ -9,6 +9,113 @@ fn event_subclass_kind_from_callback_data( EventSubclassKind::from_i32(value) } +// A converted dictionary retains exactly its payload type. Parsing, base +// flags and initialization share this dispatch instead of independent Options. +enum EventSubclassInit<'s> { + Ui(ui::UiEventInit<'s>), + Composition(ui::CompositionEventInit<'s>), + Clipboard(data::ClipboardEventInitMembers<'s>), + ClipboardChange(data::ClipboardChangeEventInitMembers<'s>), + Keyboard(keyboard::KeyboardEventInit<'s>), + Message(message::MessageEventInit<'s>), + Storage(data::StorageEventInitMembers<'s>), + Error(error::ErrorEventInit<'s>), + CurrentEntryChange(navigation_init::NavigationCurrentEntryChangeEventInitMembers<'s>), + Navigate(navigation_init::NavigateEventInitMembers<'s>), + SecurityPolicy(security_policy::SecurityPolicyViolationEventInit), + Legacy, +} + +impl<'s> EventSubclassInit<'s> { + fn parse( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + kind: EventSubclassKind, + ) -> Option { + Some(match kind { + EventSubclassKind::UiEvent => Self::Ui(ui::parse_ui_event_init(scope, args)?), + EventSubclassKind::CompositionEvent => { + Self::Composition(ui::parse_composition_event_init(scope, args)?) + } + EventSubclassKind::ClipboardEvent => { + Self::Clipboard(data::parse_clipboard_event_init(scope, args)?) + } + EventSubclassKind::ClipboardChangeEvent => { + Self::ClipboardChange(data::parse_clipboard_change_event_init(scope, args)?) + } + EventSubclassKind::KeyboardEvent => { + Self::Keyboard(keyboard::parse_keyboard_event_init(scope, args)?) + } + EventSubclassKind::MessageEvent => { + Self::Message(message::parse_message_event_init(scope, args)?) + } + EventSubclassKind::StorageEvent => { + Self::Storage(data::parse_storage_event_init(scope, args)?) + } + EventSubclassKind::ErrorEvent => { + Self::Error(error::parse_error_event_init(scope, args)?) + } + EventSubclassKind::NavigationCurrentEntryChangeEvent => Self::CurrentEntryChange( + navigation_init::parse_current_entry_change_event_init(scope, args)?, + ), + EventSubclassKind::NavigateEvent => { + Self::Navigate(navigation_init::parse_navigate_event_init(scope, args)?) + } + EventSubclassKind::SecurityPolicyViolationEvent => Self::SecurityPolicy(security_policy::parse_security_policy_violation_event_init(scope, args)?), + _ => Self::Legacy, + }) + } + + fn event_flags( + &self, + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + ) -> (bool, bool, bool) { + match self { + Self::Ui(init) => init.event_flags(), + Self::Composition(init) => init.event_flags(), + Self::Clipboard(init) => init.event_flags(), + Self::ClipboardChange(init) => init.event_flags(), + Self::Keyboard(init) => init.event_flags(), + Self::Message(init) => init.event_flags(), + Self::Storage(init) => init.event_flags(), + Self::Error(init) => init.event_flags(), + Self::CurrentEntryChange(init) => init.event_flags(), + Self::Navigate(init) => init.event_flags(), + Self::SecurityPolicy(init) => init.event_flags(), + Self::Legacy => read_event_init(scope, args), + } + } + + fn initialize( + self, + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + kind: EventSubclassKind, + legacy_init: Option>, + ) -> bool { + match self { + Self::Ui(init) => init.initialize(scope, event), + Self::Composition(init) => ui::initialize_composition_event(scope, event, init), + Self::Clipboard(init) => data::initialize_clipboard_event(scope, event, init), + Self::ClipboardChange(init) => { + return data::initialize_clipboard_change_event(scope, event, init); + } + Self::Keyboard(init) => keyboard::initialize_keyboard_event(scope, event, init), + Self::Message(init) => message::initialize_message_event(scope, event, init), + Self::Storage(init) => data::initialize_storage_event(scope, event, init), + Self::Error(init) => init.initialize(scope, event), + Self::CurrentEntryChange(init) => { + data::initialize_navigation_current_entry_change_event(scope, event, init) + } + Self::Navigate(init) => data::initialize_navigate_event(scope, event, init), + Self::SecurityPolicy(init) => init.initialize(scope, event), + Self::Legacy => return initialize_legacy_event(scope, event, kind, legacy_init), + } + true + } +} + fn event_subclass_constructor_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -45,8 +152,6 @@ fn event_subclass_constructor_callback<'s>( return; } - let wrapper = args.this(); - let event = new_event_state(scope); let Some(event_type) = event_type_argument(scope, &args, kind.constructor_name()) else { return; }; @@ -58,131 +163,11 @@ fn event_subclass_constructor_callback<'s>( init_arg.to_object(scope) } }; - let storage_event_init = if kind == EventSubclassKind::StorageEvent { - let Some(init) = data::parse_storage_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None + let Some(parsed_init) = EventSubclassInit::parse(scope, &args, kind) else { + return; }; - let security_policy_init = if kind == EventSubclassKind::SecurityPolicyViolationEvent { - let Some(init) = security_policy::parse_security_policy_violation_event_init(scope, &args) else { return; }; - Some(init) - } else { None }; - let clipboard_event_init = if kind == EventSubclassKind::ClipboardEvent { - let Some(init) = data::parse_clipboard_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let clipboard_change_event_init = if kind == EventSubclassKind::ClipboardChangeEvent { - let Some(init) = data::parse_clipboard_change_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let error_init = if kind == EventSubclassKind::ErrorEvent { - let Some(init) = error::parse_error_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let navigate_event_init = if kind == EventSubclassKind::NavigateEvent { - let Some(init) = navigation_init::parse_navigate_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let current_entry_change_event_init = - if kind == EventSubclassKind::NavigationCurrentEntryChangeEvent { - let Some(init) = navigation_init::parse_current_entry_change_event_init(scope, &args) - else { - return; - }; - Some(init) - } else { - None - }; - let message_event_init = if kind == EventSubclassKind::MessageEvent { - let Some(init) = message::parse_message_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let keyboard_event_init = if kind == EventSubclassKind::KeyboardEvent { - let Some(init) = keyboard::parse_keyboard_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let ui_event_init = if kind == EventSubclassKind::UiEvent { - let Some(init) = ui::parse_ui_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let composition_event_init = if kind == EventSubclassKind::CompositionEvent { - let Some(init) = ui::parse_composition_event_init(scope, &args) else { - return; - }; - Some(init) - } else { - None - }; - let (bubbles, cancelable, composed) = security_policy_init - .as_ref() - .map(security_policy::SecurityPolicyViolationEventInit::event_flags) - .or_else(|| error_init.as_ref().map(error::ErrorEventInit::event_flags)) - .or_else(|| clipboard_event_init.as_ref().map(data::ClipboardEventInitMembers::event_flags)) - .or_else(|| { - clipboard_change_event_init - .as_ref() - .map(data::ClipboardChangeEventInitMembers::event_flags) - }) - .or_else(|| { - navigate_event_init - .as_ref() - .map(navigation_init::NavigateEventInitMembers::event_flags) - }) - .or_else(|| { - current_entry_change_event_init - .as_ref() - .map(navigation_init::NavigationCurrentEntryChangeEventInitMembers::event_flags) - }) - .or_else(|| { - message_event_init - .as_ref() - .map(message::MessageEventInit::event_flags) - }) - .or_else(|| { - keyboard_event_init - .as_ref() - .map(keyboard::KeyboardEventInit::event_flags) - }) - .or_else(|| ui_event_init.as_ref().map(ui::UiEventInit::event_flags)) - .or_else(|| { - composition_event_init - .as_ref() - .map(ui::CompositionEventInit::event_flags) - }) - .or_else(|| storage_event_init.as_ref().map(data::StorageEventInitMembers::event_flags)) - .unwrap_or_else(|| read_event_init(scope, &args)); - + let (bubbles, cancelable, composed) = parsed_init.event_flags(scope, &args); + let event = new_event_state(scope); initialize_event_object_with_type(scope, event, event_type, bubbles, cancelable); define_event_property( scope, @@ -190,168 +175,10 @@ fn event_subclass_constructor_callback<'s>( "composed", v8::Boolean::new(scope, composed).into(), ); - - match kind { - EventSubclassKind::UiEvent => { - ui_event_init - .expect("UIEvent init should be parsed") - .initialize(scope, event); - } - EventSubclassKind::FocusEvent => { - if !basic::initialize_focus_event(scope, event, init) { - return; - } - } - EventSubclassKind::TextEvent => { - if !basic::initialize_text_event(scope, event, init) { - return; - } - } - EventSubclassKind::CompositionEvent => { - ui::initialize_composition_event( - scope, - event, - composition_event_init.expect("CompositionEvent init should be parsed"), - ); - } - EventSubclassKind::CustomEvent => basic::initialize_custom_event(scope, event, init), - EventSubclassKind::MouseEvent => { - if !pointer::initialize_mouse_event(scope, event, init) { - return; - } - } - EventSubclassKind::DragEvent => { - if !data::initialize_drag_event(scope, event, init) { - return; - } - } - EventSubclassKind::ClipboardEvent => { - data::initialize_clipboard_event( - scope, - event, - clipboard_event_init.expect("ClipboardEvent init should be parsed"), - ); - } - EventSubclassKind::ClipboardChangeEvent => { - if !data::initialize_clipboard_change_event( - scope, - event, - clipboard_change_event_init.expect("ClipboardChangeEvent init should be parsed"), - ) { - return; - } - } - EventSubclassKind::CapturedMouseEvent => { - if !data::initialize_captured_mouse_event(scope, event, init) { - return; - } - } - EventSubclassKind::KeyboardEvent => { - keyboard::initialize_keyboard_event( - scope, - event, - keyboard_event_init.expect("KeyboardEvent init should be parsed"), - ); - } - EventSubclassKind::InputEvent => data::initialize_input_event(scope, event, init), - EventSubclassKind::WheelEvent => { - if !pointer::initialize_wheel_event(scope, event, init) { - return; - } - } - EventSubclassKind::PointerEvent => { - if !pointer::initialize_pointer_event(scope, event, init) { - return; - } - } - EventSubclassKind::TouchEvent => { - crate::context_bootstrap::touch_runtime::initialize_touch_event(scope, event, init); - } - EventSubclassKind::MessageEvent => message::initialize_message_event( - scope, - event, - message_event_init.expect("MessageEvent init should be parsed"), - ), - EventSubclassKind::StorageEvent => { - data::initialize_storage_event( - scope, - event, - storage_event_init.expect("StorageEvent init should be parsed"), - ); - } - EventSubclassKind::ErrorEvent => error_init - .expect("ErrorEvent init is parsed") - .initialize(scope, event), - EventSubclassKind::PromiseRejectionEvent => { - if !data::initialize_promise_rejection_event(scope, event, init) { - return; - } - } - EventSubclassKind::SecurityPolicyViolationEvent => { - security_policy_init - .expect("SecurityPolicyViolationEvent init is parsed") - .initialize(scope, event); - } - EventSubclassKind::NavigationCurrentEntryChangeEvent => { - data::initialize_navigation_current_entry_change_event( - scope, - event, - current_entry_change_event_init - .expect("NavigationCurrentEntryChangeEvent init should be parsed"), - ); - } - EventSubclassKind::NavigateEvent => { - data::initialize_navigate_event( - scope, - event, - navigate_event_init.expect("NavigateEvent init should be parsed"), - ); - } - EventSubclassKind::CloseEvent => data::initialize_close_event(scope, event, init), - EventSubclassKind::SubmitEvent => { - if !data::initialize_submit_event(scope, event, init) { - return; - } - } - EventSubclassKind::FormDataEvent => { - if !data::initialize_form_data_event(scope, event, init) { - return; - } - } - EventSubclassKind::CommandEvent => { - if !data::initialize_command_event(scope, event, init) { - return; - } - } - EventSubclassKind::ToggleEvent => { - if !data::initialize_toggle_event(scope, event, init) { - return; - } - } - EventSubclassKind::InterestEvent => data::initialize_interest_event(scope, event, init), - EventSubclassKind::PopStateEvent => { - if !data::initialize_pop_state_event(scope, event, init) { - return; - } - } - EventSubclassKind::HashChangeEvent => { - if !data::initialize_hash_change_event(scope, event, init) { - return; - } - } - EventSubclassKind::PageTransitionEvent => { - data::initialize_page_transition_event(scope, event, init); - } - EventSubclassKind::TrackEvent => data::initialize_track_event(scope, event, init), - EventSubclassKind::FontFaceSetLoadEvent => { - if !crate::context_bootstrap::css_fontface_runtime::initialize_font_face_set_load_event( - scope, event, init, - ) { - return; - } - } + if !parsed_init.initialize(scope, event, kind, init) { + return; } - + let wrapper = args.this(); web_api_interfaces::initialize(scope, event, kind.constructor_name()) .expect("event primary interface should initialize"); set_private_value( @@ -365,6 +192,118 @@ fn event_subclass_constructor_callback<'s>( } } +fn initialize_legacy_event<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + kind: EventSubclassKind, + init: Option>, +) -> bool { + match kind { + EventSubclassKind::FocusEvent => { + if !basic::initialize_focus_event(scope, event, init) { + return false; + } + } + EventSubclassKind::TextEvent => { + if !basic::initialize_text_event(scope, event, init) { + return false; + } + } + EventSubclassKind::CustomEvent => basic::initialize_custom_event(scope, event, init), + EventSubclassKind::MouseEvent => { + if !pointer::initialize_mouse_event(scope, event, init) { + return false; + } + } + EventSubclassKind::DragEvent => { + if !data::initialize_drag_event(scope, event, init) { + return false; + } + } + EventSubclassKind::CapturedMouseEvent => { + if !data::initialize_captured_mouse_event(scope, event, init) { + return false; + } + } + EventSubclassKind::InputEvent => data::initialize_input_event(scope, event, init), + EventSubclassKind::WheelEvent => { + if !pointer::initialize_wheel_event(scope, event, init) { + return false; + } + } + EventSubclassKind::PointerEvent => { + if !pointer::initialize_pointer_event(scope, event, init) { + return false; + } + } + EventSubclassKind::TouchEvent => { + crate::context_bootstrap::touch_runtime::initialize_touch_event(scope, event, init); + } + EventSubclassKind::PromiseRejectionEvent => { + if !data::initialize_promise_rejection_event(scope, event, init) { + return false; + } + } + EventSubclassKind::SecurityPolicyViolationEvent => unreachable!("SecurityPolicyViolationEvent init is typed"), + EventSubclassKind::CloseEvent => data::initialize_close_event(scope, event, init), + EventSubclassKind::SubmitEvent => { + if !data::initialize_submit_event(scope, event, init) { + return false; + } + } + EventSubclassKind::FormDataEvent => { + if !data::initialize_form_data_event(scope, event, init) { + return false; + } + } + EventSubclassKind::CommandEvent => { + if !data::initialize_command_event(scope, event, init) { + return false; + } + } + EventSubclassKind::ToggleEvent => { + if !data::initialize_toggle_event(scope, event, init) { + return false; + } + } + EventSubclassKind::InterestEvent => data::initialize_interest_event(scope, event, init), + EventSubclassKind::PopStateEvent => { + if !data::initialize_pop_state_event(scope, event, init) { + return false; + } + } + EventSubclassKind::HashChangeEvent => { + if !data::initialize_hash_change_event(scope, event, init) { + return false; + } + } + EventSubclassKind::PageTransitionEvent => { + data::initialize_page_transition_event(scope, event, init); + } + EventSubclassKind::TrackEvent => data::initialize_track_event(scope, event, init), + EventSubclassKind::FontFaceSetLoadEvent => { + if !crate::context_bootstrap::css_fontface_runtime::initialize_font_face_set_load_event( + scope, event, init, + ) { + return false; + } + } + EventSubclassKind::ClipboardChangeEvent + | EventSubclassKind::ClipboardEvent + | EventSubclassKind::CompositionEvent + | EventSubclassKind::ErrorEvent + | EventSubclassKind::KeyboardEvent + | EventSubclassKind::MessageEvent + | EventSubclassKind::NavigateEvent + | EventSubclassKind::NavigationCurrentEntryChangeEvent + | EventSubclassKind::StorageEvent + | EventSubclassKind::UiEvent => { + unreachable!("typed event initialization must retain its payload") + } + } + true +} + pub(in crate::context_bootstrap) fn build_event_subclass_template<'s>( scope: &mut v8::PinScope<'s, '_, ()>, kind: EventSubclassKind, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs index 7171143a42..bd108398f9 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/history_worlds.rs @@ -30,6 +30,7 @@ fn event_field_getters_are_shared_within_a_realm_without_capturing_instances() { const withoutError = new ErrorEvent('without'); const nullError = new ErrorEvent('null', { error: null }); const withError = new ErrorEvent('with', { error: first }); + const errorGetter = own(ErrorEvent.prototype, 'error').get; // Prototype source accessors must not become cached own fields on wrappers. const prototypeSource = [ToggleEvent, CommandEvent].every(ctor => { const descriptor = own(ctor.prototype, 'source'); @@ -52,8 +53,10 @@ fn event_field_getters_are_shared_within_a_realm_without_capturing_instances() { descriptor: [type.get.name, type.get.length, type.set === undefined, type.enumerable, type.configurable], trusted: own(first, 'isTrusted').configurable === false && first.isTrusted === false, errorValues: withoutError.error === undefined && nullError.error === null && withError.error === first && - [withoutError, nullError, withError].every(event => !Object.hasOwn(event, 'error')) && - own(ErrorEvent.prototype, 'error').get.call(withError) === first, + [withoutError, nullError, withError].every(event => !own(event, 'error') && + own(Object.getPrototypeOf(event), 'error').get === errorGetter) && + errorGetter.call(withoutError) === undefined && errorGetter.call(nullError) === null && + errorGetter.call(withError) === first, prototypeSource, original: derived === derived.original && derived instanceof DerivedEvent, illegal diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index bf9dd8e9fb..9f0f6b7fe9 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -26,6 +26,7 @@ mod document_domain_setter; mod document_open_navigation_abort; mod document_open_origin; mod dom_event_worlds; +mod error_event_init; mod event_constructor_type; mod event_handlers; mod event_listener_options;