From b205dc2b8eca249bb6bf1b1ef6eb74cfb4d76f50 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 1 Sep 2026 09:41:51 +0800 Subject: [PATCH] fix(svg): enforce value list mutation semantics --- .../context_bootstrap/svg_runtime/builders.rs | 115 ++++++-- .../svg_runtime/callbacks.rs | 267 ++++++++++++++++-- .../src/context_bootstrap/svg_runtime/mod.rs | 1 + .../dom_interfaces_and_collections.rs | 85 ++++++ 4 files changed, 423 insertions(+), 45 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs index 95963e355b..ffba045e6e 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -209,6 +209,8 @@ struct SvgAnimatedTransformListObjectDeclaration<'scope> { struct SvgLengthListObjectDeclaration<'scope> { #[webapi(slot = SVG_LENGTH_LIST_ITEMS_SLOT)] items: Vec>, + #[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)] + read_only: bool, #[webapi(method, callback = svg_length_list_clear_callback, length = 0)] clear: (), #[webapi(method, callback = svg_length_list_initialize_callback, length = 1)] @@ -234,6 +236,8 @@ struct SvgLengthListObjectDeclaration<'scope> { struct SvgNumberListObjectDeclaration<'scope> { #[webapi(slot = SVG_NUMBER_LIST_ITEMS_SLOT)] items: Vec>, + #[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)] + read_only: bool, #[webapi(method, callback = svg_number_list_clear_callback, length = 0)] clear: (), #[webapi(method, callback = svg_number_list_initialize_callback, length = 1)] @@ -451,8 +455,8 @@ pub(super) fn reflect_svg_animated_boolean_to_owner_attribute<'s>( pub(super) fn build_svg_animated_length_list<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { - let base_val = build_svg_length_list(scope); - let anim_val = build_svg_length_list(scope); + let base_val = build_svg_length_list(scope, false); + let anim_val = build_svg_length_list(scope, true); SvgAnimatedLengthListObjectDeclaration::new(base_val, anim_val) .bind(scope) .expect("SVGAnimatedLengthList declaration should bind") @@ -461,8 +465,8 @@ pub(super) fn build_svg_animated_length_list<'s>( pub(super) fn build_svg_animated_number_list<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { - let base_val = build_svg_number_list(scope); - let anim_val = build_svg_number_list(scope); + let base_val = build_svg_number_list(scope, false); + let anim_val = build_svg_number_list(scope, true); SvgAnimatedNumberListObjectDeclaration::new(base_val, anim_val) .bind(scope) .expect("SVGAnimatedNumberList declaration should bind") @@ -847,18 +851,32 @@ pub(super) fn serialize_svg_animated_enumeration( pub(super) fn build_svg_length_list<'s>( scope: &mut v8::PinScope<'s, '_>, + read_only: bool, ) -> v8::Local<'s, v8::Object> { - SvgLengthListObjectDeclaration::new(Vec::new()) - .bind(scope) - .expect("SVGLengthList declaration should bind") + let template = v8::ObjectTemplate::new(scope); + configure_svg_value_list_indexed_property_handler(template); + let object = template + .new_instance(scope) + .expect("SVGLengthList object template should instantiate"); + SvgLengthListObjectDeclaration::new(Vec::new(), read_only) + .bind_into(scope, object) + .expect("SVGLengthList declaration should bind"); + object } pub(super) fn build_svg_number_list<'s>( scope: &mut v8::PinScope<'s, '_>, + read_only: bool, ) -> v8::Local<'s, v8::Object> { - SvgNumberListObjectDeclaration::new(Vec::new()) - .bind(scope) - .expect("SVGNumberList declaration should bind") + let template = v8::ObjectTemplate::new(scope); + configure_svg_value_list_indexed_property_handler(template); + let object = template + .new_instance(scope) + .expect("SVGNumberList object template should instantiate"); + SvgNumberListObjectDeclaration::new(Vec::new(), read_only) + .bind_into(scope, object) + .expect("SVGNumberList declaration should bind"); + object } pub(super) fn build_svg_string_list_for_attribute<'s>( @@ -1724,15 +1742,34 @@ pub(super) fn svg_matrix_value_or_throw<'s>( None } -pub(super) fn svg_value_list_item_or_default<'s>( +pub(super) fn svg_value_list_item_or_throw<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, kind: SvgListKind, -) -> v8::Local<'s, v8::Object> { - v8::Local::::try_from(value).unwrap_or_else(|_| match kind { - SvgListKind::Length => build_svg_length(scope, 0.0), - SvgListKind::Number => build_svg_number(scope, 0.0), - }) +) -> Option> { + let (brand_slot, interface) = match kind { + SvgListKind::Length => (SVG_LENGTH_VALUE_SLOT, "SVGLength"), + SvgListKind::Number => (SVG_NUMBER_VALUE_SLOT, "SVGNumber"), + }; + let object = v8::Local::::try_from(value).ok(); + if let Some(object) = object + && get_private_value(scope, object, brand_slot).is_some() + { + return Some(object); + } + webidl::throw_type_error( + scope, + &format!("Argument 1 can not be converted to {interface}"), + ); + None +} + +pub(super) fn svg_value_list_is_read_only<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> bool { + get_private_value(scope, object, SVG_VALUE_LIST_READ_ONLY_SLOT) + .is_some_and(|value| value.is_true()) } pub(super) fn svg_value_list_items<'s>( @@ -1865,27 +1902,47 @@ pub(super) fn sync_svg_animated_value_list_from_owner_attribute<'s>( ) { let raw = svg_owner_attribute_value(scope, owner, attribute); let raw_value = raw.clone().unwrap_or_default(); - if let Some(base_val) = svg_animated_value_list_member(scope, animated, "baseVal", kind) - && svg_value_list_synced_attribute_value(scope, base_val) - .as_deref() - .is_some_and(|synced| synced == raw_value) - { - set_svg_value_list_owner_attribute(scope, base_val, owner, attribute); - return; - } - let base_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); - let anim_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); if let Some(base_val) = svg_animated_value_list_member(scope, animated, "baseVal", kind) { - set_svg_value_list_items(scope, base_val, base_items, kind); set_svg_value_list_owner_attribute(scope, base_val, owner, attribute); - set_svg_value_list_synced_attribute_value(scope, base_val, &raw_value); + sync_svg_value_list_from_owner_attribute(scope, base_val, kind); } - if let Some(anim_val) = svg_animated_value_list_member(scope, animated, "animVal", kind) { + if let Some(anim_val) = svg_animated_value_list_member(scope, animated, "animVal", kind) + && svg_value_list_synced_attribute_value(scope, anim_val).as_deref() + != Some(raw_value.as_str()) + { + let anim_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); set_svg_value_list_items(scope, anim_val, anim_items, kind); set_svg_value_list_synced_attribute_value(scope, anim_val, &raw_value); } } +pub(super) fn sync_svg_value_list_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + kind: SvgListKind, +) { + let Some(owner) = get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(attribute) = get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .filter(|value| !value.is_empty()) + else { + return; + }; + let raw = svg_owner_attribute_value(scope, owner, &attribute); + let raw_value = raw.clone().unwrap_or_default(); + if svg_value_list_synced_attribute_value(scope, list).as_deref() == Some(raw_value.as_str()) { + return; + } + let items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind); + set_svg_value_list_items(scope, list, items, kind); + set_svg_value_list_synced_attribute_value(scope, list, &raw_value); +} + pub(super) fn build_svg_value_list_items_from_attribute<'s>( scope: &mut v8::PinScope<'s, '_>, raw: Option<&str>, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs index dacf36ded5..7640463a3c 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -34,6 +34,21 @@ pub(super) fn configure_svg_string_list_indexed_property_handler( ); } +pub(super) fn configure_svg_value_list_indexed_property_handler( + template: v8::Local<'_, v8::ObjectTemplate>, +) { + template.set_indexed_property_handler( + v8::IndexedPropertyHandlerConfiguration::new() + .getter(svg_value_list_indexed_getter) + .setter(svg_value_list_indexed_setter) + .query(svg_value_list_indexed_query) + .deleter(svg_value_list_indexed_deleter) + .enumerator(svg_value_list_indexed_enumerator) + .definer(svg_value_list_indexed_definer) + .descriptor(svg_value_list_indexed_descriptor), + ); +} + fn svg_dom_matrix_2d_init_arg<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, @@ -1946,12 +1961,56 @@ fn svg_string_list_indexed_descriptor<'s>( v8::Intercepted::kYes } +fn svg_value_list_metadata(kind: SvgListKind) -> (&'static str, &'static str) { + match kind { + SvgListKind::Length => (SVG_LENGTH_LIST_ITEMS_SLOT, "SVGLengthList"), + SvgListKind::Number => (SVG_NUMBER_LIST_ITEMS_SLOT, "SVGNumberList"), + } +} + +fn svg_value_list_kind<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option { + if web_api_interfaces::SVGLengthList::is_instance(scope, list) { + Some(SvgListKind::Length) + } else if web_api_interfaces::SVGNumberList::is_instance(scope, list) { + Some(SvgListKind::Number) + } else { + None + } +} + +fn require_svg_value_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + kind: SvgListKind, + member: &str, + writable: bool, +) -> Option> { + let (_, interface) = svg_value_list_metadata(kind); + if !require_svg_receiver(scope, list, interface, member) { + return None; + } + if writable && svg_value_list_is_read_only(scope, list) { + throw_dom_exception( + scope, + "NoModificationAllowedError", + 7, + "The SVG value list is read-only.", + ); + return None; + } + sync_svg_value_list_from_owner_attribute(scope, list, kind); + Some(svg_value_list_items(scope, list, kind)) +} + pub(super) fn svg_length_list_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { - svg_value_list_length_getter(scope, args, rv, SvgListKind::Length, "SVGLengthList"); + svg_value_list_length_getter(scope, args, rv, SvgListKind::Length); } pub(super) fn svg_number_list_length_getter<'s>( @@ -1959,7 +2018,7 @@ pub(super) fn svg_number_list_length_getter<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { - svg_value_list_length_getter(scope, args, rv, SvgListKind::Number, "SVGNumberList"); + svg_value_list_length_getter(scope, args, rv, SvgListKind::Number); } pub(super) fn svg_value_list_length_getter<'s>( @@ -1967,13 +2026,13 @@ pub(super) fn svg_value_list_length_getter<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, - interface: &'static str, ) { - if !require_svg_receiver(scope, args.this(), interface, "length getter") { + let Some(items) = + require_svg_value_list_items(scope, args.this(), kind, "length getter", false) + else { return; - } - let length = svg_value_list_items(scope, args.this(), kind).length(); - rv.set(v8::Integer::new_from_unsigned(scope, length).into()); + }; + rv.set(v8::Integer::new_from_unsigned(scope, items.length()).into()); } pub(super) fn svg_length_list_clear_callback<'s>( @@ -1998,6 +2057,9 @@ pub(super) fn svg_value_list_clear_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + if require_svg_value_list_items(scope, args.this(), kind, "clear", true).is_none() { + return; + } set_svg_value_list_items(scope, args.this(), v8::Array::new(scope, 0), kind); reflect_svg_value_list_to_owner_attribute(scope, args.this(), kind); rv.set_undefined(); @@ -2025,10 +2087,15 @@ pub(super) fn svg_value_list_initialize_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + if require_svg_value_list_items(scope, args.this(), kind, "initialize", true).is_none() { + return; + } let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let item = svg_value_list_item_or_default(scope, parsed.item, kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; let Some(items) = serialize_v8_array(scope, [item]) else { return; }; @@ -2059,10 +2126,13 @@ pub(super) fn svg_value_list_get_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "getItem", false) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let items = svg_value_list_items(scope, args.this(), kind); let Some(item) = svg_list_item_or_throw(scope, items, parsed.index) else { return; }; @@ -2091,11 +2161,17 @@ pub(super) fn svg_value_list_insert_item_before_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = + require_svg_value_list_items(scope, args.this(), kind, "insertItemBefore", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let item = svg_value_list_item_or_default(scope, parsed.item, kind); - let items = svg_value_list_items(scope, args.this(), kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; let length = items.length(); let index = parsed.index.min(length); let next = v8::Array::new(scope, (length + 1) as i32); @@ -2137,15 +2213,20 @@ pub(super) fn svg_value_list_replace_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "replaceItem", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let items = svg_value_list_items(scope, args.this(), kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; if parsed.index >= items.length() { webidl::throw_index_size_error(scope); return; } - let item = svg_value_list_item_or_default(scope, parsed.item, kind); if let Some(replaced) = items .get_index(scope, parsed.index) .and_then(|value| v8::Local::::try_from(value).ok()) @@ -2182,11 +2263,14 @@ pub(super) fn svg_value_list_remove_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "removeItem", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; let index = parsed.index; - let items = svg_value_list_items(scope, args.this(), kind); let length = items.length(); if index >= length { webidl::throw_index_size_error(scope); @@ -2236,17 +2320,168 @@ pub(super) fn svg_value_list_append_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, kind: SvgListKind, ) { + let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "appendItem", true) + else { + return; + }; let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let item = svg_value_list_item_or_default(scope, parsed.item, kind); - let items = svg_value_list_items(scope, args.this(), kind); + let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else { + return; + }; set_svg_value_list_item_owner_list(scope, item, args.this()); let _ = items.set_index(scope, items.length(), item.into()); reflect_svg_value_list_to_owner_attribute(scope, args.this(), kind); rv.set(item.into()); } +fn svg_value_list_intercepted_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option<(SvgListKind, v8::Local<'s, v8::Array>)> { + let kind = svg_value_list_kind(scope, list)?; + sync_svg_value_list_from_owner_attribute(scope, list, kind); + Some((kind, svg_value_list_items(scope, list, kind))) +} + +fn svg_value_list_indexed_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_value_list_intercepted_items(scope, args.holder()) + .map(|(_, items)| items) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + rv.set(value); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + value: v8::Local<'s, v8::Value>, + args: v8::PropertyCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let list = args.holder(); + let Some(kind) = svg_value_list_kind(scope, list) else { + return v8::Intercepted::kNo; + }; + let Some(items) = require_svg_value_list_items(scope, list, kind, "indexed setter", true) + else { + return v8::Intercepted::kYes; + }; + let Some(item) = svg_value_list_item_or_throw(scope, value, kind) else { + return v8::Intercepted::kYes; + }; + if index >= items.length() { + webidl::throw_index_size_error(scope); + return v8::Intercepted::kYes; + } + if let Some(replaced) = items + .get_index(scope, index) + .and_then(|value| v8::Local::::try_from(value).ok()) + { + clear_svg_value_list_item_owner_list(scope, replaced); + } + let _ = items.set_index(scope, index, item.into()); + set_svg_value_list_item_owner_list(scope, item, list); + reflect_svg_value_list_to_owner_attribute(scope, list, kind); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_query<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Integer>, +) -> v8::Intercepted { + let Some((_, items)) = svg_value_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_int32(v8::PropertyAttribute::NONE.as_u32() as i32); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_deleter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let Some((_, items)) = svg_value_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_bool(false); + v8::Intercepted::kYes +} + +fn svg_value_list_indexed_definer<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + descriptor: &v8::PropertyDescriptor, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + if descriptor.has_get() || descriptor.has_set() { + rv.set_bool(false); + return v8::Intercepted::kYes; + } + let value = if descriptor.has_value() { + v8::Local::new(scope, descriptor.value()) + } else { + v8::undefined(scope).into() + }; + svg_value_list_indexed_setter(scope, index, value, args, rv) +} + +fn svg_value_list_indexed_enumerator<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Array>, +) { + let length = svg_value_list_intercepted_items(scope, args.holder()) + .map(|(_, items)| items.length()) + .unwrap_or(0); + let keys = (0..length) + .map(|index| v8::Integer::new_from_unsigned(scope, index).into()) + .collect::>(); + rv.set(v8::Array::new_with_elements(scope, &keys)); +} + +fn svg_value_list_indexed_descriptor<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_value_list_intercepted_items(scope, args.holder()) + .map(|(_, items)| items) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + let Ok(descriptor) = DataPropertyDescriptorDeclaration::new(value, true, true).bind(scope) + else { + return v8::Intercepted::kNo; + }; + rv.set(descriptor.into()); + v8::Intercepted::kYes +} + pub(super) fn svg_transform_list_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs index bb5d37e0dd..e6f0951afa 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -79,6 +79,7 @@ const SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT: &str = const SVG_VALUE_LIST_OWNER_ELEMENT_SLOT: &str = "__moliSvgValueListOwnerElement"; const SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgValueListOwnerAttribute"; const SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT: &str = "__moliSvgValueListItemOwnerList"; +const SVG_VALUE_LIST_READ_ONLY_SLOT: &str = "__moliSvgValueListReadOnly"; const SVG_VALUE_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT: &str = "__moliSvgValueListSyncedAttributeValue"; const SVG_ANIMATED_ENUMERATION_BASE_VAL_SLOT: &str = "__moliSvgAnimatedEnumerationBaseVal"; const SVG_ANIMATED_ENUMERATION_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedEnumerationAnimVal"; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs index 4df8ce801d..c22e8496f4 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs @@ -3017,3 +3017,88 @@ fn svg_href_animated_string_prefers_href_and_falls_back_to_xlink_href() { assert_eq!(result, "ok"); } + +#[test] +fn svg_value_lists_enforce_item_types_indices_and_read_only_anim_values() { + let mut vm = new_storage_test_vm("https://svg-value-list-semantics.test/"); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const errorName = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error.name; + } + }; + const SVG_NS = "http://www.w3.org/2000/svg"; + const text = document.createElementNS(SVG_NS, "text"); + const svg = document.createElementNS(SVG_NS, "svg"); + text.setAttribute("x", "10 20"); + text.setAttribute("rotate", "15 30"); + + const lengths = text.x; + const lengthBase = lengths.baseVal; + const lengthAnim = lengths.animVal; + const numbers = text.rotate; + const numberBase = numbers.baseVal; + const numberAnim = numbers.animVal; + const invalidItems = [30, "invalid", text, null]; + + for (const item of invalidItems) { + assert(errorName(() => lengthBase.initialize(item)) === "TypeError", + "SVGLengthList.initialize item type"); + assert(errorName(() => lengthBase.insertItemBefore(item, 0)) === "TypeError", + "SVGLengthList.insertItemBefore item type"); + assert(errorName(() => lengthBase.replaceItem(item, 0)) === "TypeError", + "SVGLengthList.replaceItem item type"); + assert(errorName(() => lengthBase.appendItem(item)) === "TypeError", + "SVGLengthList.appendItem item type"); + assert(errorName(() => { lengthBase[0] = item; }) === "TypeError", + "SVGLengthList indexed setter item type"); + } + + const length = svg.createSVGLength(); + length.value = 42; + lengthBase[0] = length; + assert(lengthBase[0] === length, "SVGLengthList indexed getter"); + assert(text.getAttribute("x") === "42 20", "SVGLengthList indexed reflection"); + + const number = svg.createSVGNumber(); + number.value = 7; + numberBase[1] = number; + assert(numberBase[1] === number, "SVGNumberList indexed getter"); + assert(text.getAttribute("rotate") === "15 7", "SVGNumberList indexed reflection"); + assert(errorName(() => lengthBase.appendItem(number)) === "TypeError", + "SVGLengthList rejects SVGNumber"); + assert(errorName(() => numberBase.appendItem(length)) === "TypeError", + "SVGNumberList rejects SVGLength"); + + text.setAttribute("x", "1 2 3"); + assert(lengthBase.length === 3 && lengthBase[2].value === 3, + "saved baseVal resynchronizes"); + assert(text.x.animVal.length === 3 && text.x.animVal[2].value === 3, + "animVal resynchronizes after direct baseVal access"); + + assert(errorName(() => lengthAnim.clear()) === "NoModificationAllowedError", + "SVGLengthList animVal clear"); + assert(errorName(() => { lengthAnim[0] = length; }) === "NoModificationAllowedError", + "SVGLengthList animVal indexed setter"); + assert(errorName(() => numberAnim.appendItem(number)) === "NoModificationAllowedError", + "SVGNumberList animVal appendItem"); + assert(errorName(() => SVGLengthList.prototype.clear.call({})) === "TypeError", + "SVGLengthList receiver brand"); + return "ok"; + })() + "#, + ) + .expect("SVG value list semantics probe should evaluate"); + + assert_eq!(result, "ok"); +}