diff --git a/core/src/avm2/e4x.rs b/core/src/avm2/e4x.rs index c383daabd7c2..3930ab3ebb94 100644 --- a/core/src/avm2/e4x.rs +++ b/core/src/avm2/e4x.rs @@ -5,7 +5,7 @@ use crate::avm2::error::{ use crate::avm2::function::FunctionArgs; use crate::avm2::multiname::NamespaceSet; use crate::avm2::object::{E4XOrXml, FunctionObject, NamespaceObject}; -use crate::avm2::{Activation, Error, Multiname, Namespace, Value}; +use crate::avm2::{Activation, Error, Multiname, Namespace, Object, Value}; use crate::string::{AvmString, StringContext, WStr, WString}; use gc_arena::barrier::unlock; @@ -89,6 +89,11 @@ pub fn handle_input_multiname<'gc>( pub use is_xml_name::is_xml_name; +pub enum E4XNotification { + AttributeAdded, + AttributeChanged, +} + /// The underlying XML node data, based on E4XNode in avmplus /// This wrapped by XMLObject when necessary (see `E4XOrXml`) #[derive(Copy, Clone, Collect, Debug)] @@ -393,17 +398,39 @@ impl<'gc> E4XNode<'gc> { return None; }; - let index = children + self.remove_matching_nodes(gc_context, children, name) + } + + /// Removes all matching attributes matching provided name, returns the first attribute removed along with its index (if any). + pub fn remove_matching_attribute( + self, + gc_context: &Mutation<'gc>, + name: &Multiname<'gc>, + ) -> Option<(usize, E4XNode<'gc>)> { + let E4XNodeKind::Element { attributes, .. } = &mut *self.kind_mut(gc_context) else { + return None; + }; + + self.remove_matching_nodes(gc_context, attributes, name) + } + + fn remove_matching_nodes( + self, + gc_context: &Mutation<'gc>, + nodes: &mut Vec>, + name: &Multiname<'gc>, + ) -> Option<(usize, E4XNode<'gc>)> { + let index = nodes .iter() .position(|x| name.is_any_name() || x.matches_name(name)); let val = if let Some(index) = index { - Some((index, children[index])) + Some((index, nodes[index])) } else { None }; - children.retain(|x| { + nodes.retain(|x| { if name.is_any_name() || x.matches_name(name) { // Remove parent. x.set_parent(None, gc_context); @@ -1156,6 +1183,37 @@ impl<'gc> E4XNode<'gc> { self.0.notification.get() } + pub fn trigger_notification( + self, + activation: &mut Activation<'_, 'gc>, + target_current: Object<'gc>, + command: E4XNotification, + target: Object<'gc>, + value: Value<'gc>, + detail: Value<'gc>, + ) { + let Some(function) = self.notification() else { + return; + }; + + let command = AvmString::new_utf8( + activation.gc(), + match command { + E4XNotification::AttributeAdded => "attributeAdded", + E4XNotification::AttributeChanged => "attributeChanged", + }, + ); + + let args = [ + target_current.into(), + command.into(), + target.into(), + value, + detail, + ]; + let _ = function.call(activation, Value::Null, FunctionArgs::from_slice(&args)); + } + // 13.3.5.4 [[GetNamespace]] ( [ InScopeNamespaces ] ) pub fn get_namespace( self, diff --git a/core/src/avm2/object/xml_object.rs b/core/src/avm2/object/xml_object.rs index 97ee6440d0cb..4ecbec902afc 100644 --- a/core/src/avm2/object/xml_object.rs +++ b/core/src/avm2/object/xml_object.rs @@ -2,8 +2,8 @@ use crate::avm2::activation::Activation; use crate::avm2::e4x::{ - E4XNamespace, E4XNode, E4XNodeKind, handle_input_multiname, namespace_for_multiname, - string_to_multiname, + E4XNamespace, E4XNode, E4XNodeKind, E4XNotification, handle_input_multiname, + namespace_for_multiname, string_to_multiname, }; use crate::avm2::error::make_error_1087; use crate::avm2::function::FunctionArgs; @@ -478,24 +478,79 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> { AvmString::new(activation.gc(), out) // 6.c. Else } else { + // 6.c.i. Let c = ToString(c) value.coerce_to_string(activation)? }; - let mc = activation.gc(); - self.delete_property_local(activation, &name)?; - let Some(local_name) = name.local_name() else { - return Err(format!("Cannot set attribute {name:?} without a local name").into()); - }; - let ns = name.explicit_namespace().map(E4XNamespace::new_uri); - let new_attr = E4XNode::attribute(mc, ns, local_name, value, Some(self.node())); + let gc = activation.gc(); + + // 6.d. Let a = null + // 6.e. For each j in x.[[Attributes]] + // 6.e.i. If (n.[[Name]].localName == j.[[Name]].localName) + // and ((n.[[Name]].uri == null) or (n.[[Name]].uri == j.[[Name]].uri)) + // 6.e.i.1. If (a == null), a = j + // 6.e.i.2. Else call the [[Delete]] method of x with argument j.[[Name]] + if let Some((index, old_attr)) = self.node().remove_matching_attribute(gc, &name) { + // NOTE: In this branch we modify the value of the first matching attribute. + let old_value = { + // 6.g. Let a.[[Value]] = c + let E4XNodeKind::Attribute(old_value) = &mut *old_attr.kind_mut(gc) else { + unreachable!("Node should be of Attribute kind"); + }; + let old_value_copy = *old_value; + *old_value = value; + + let node = self.0.node.get(); + let E4XNodeKind::Element { attributes, .. } = &mut *node.kind_mut(gc) else { + return Ok(()); + }; + old_attr.set_parent(Some(self.node()), gc); + attributes.insert(index, old_attr); + old_value_copy + }; - let node = self.0.node.get(); - let mut kind = node.kind_mut(mc); - let E4XNodeKind::Element { attributes, .. } = &mut *kind else { - return Ok(()); - }; + if let Some(name) = old_attr.local_name() { + self.node().trigger_notification( + activation, + self.into(), + E4XNotification::AttributeChanged, + self.into(), + name.into(), + old_value.into(), + ); + } + } else { + // 6.f. If a == null - attributes.push(new_attr); + // TODO: Make this closer to the specification. + let Some(local_name) = name.local_name() else { + return Err( + format!("Cannot set attribute {name:?} without a local name").into(), + ); + }; + let ns = name.explicit_namespace().map(E4XNamespace::new_uri); + let attr = E4XNode::attribute(gc, ns, local_name, value, Some(self.node())); + + { + // 6.f.iv. Let x.[[Attributes]] = x.[[Attributes]] ∪ { a } + let node = self.0.node.get(); + let E4XNodeKind::Element { attributes, .. } = &mut *node.kind_mut(gc) else { + return Ok(()); + }; + attributes.push(attr); + } + + self.node().trigger_notification( + activation, + self.into(), + E4XNotification::AttributeAdded, + self.into(), + local_name.into(), + value.into(), + ); + } + + // 6.h. Return return Ok(()); } @@ -635,25 +690,13 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> { // 3. If Type(n) is AttributeName if name.is_attribute() { - let E4XNodeKind::Element { attributes, .. } = &mut *node.kind_mut(activation.gc()) - else { - return Ok(true); - }; - // 3.a. For each a in x.[[Attributes]] - attributes.retain(|attr| { - // 3.a.i. If ((n.[[Name]].localName == "*") or - // (n.[[Name]].localName == a.[[Name]].localName)) - // and ((n.[[Name]].uri == null) or (n.[[Name]].uri == a.[[Name]].uri)) - if attr.matches_name(&name) { - // 3.a.i.1. Let a.[[Parent]] = null - attr.set_parent(None, activation.gc()); - // 3.a.i.2. Remove the attribute a from x.[[Attributes]] - false - } else { - true - } - }); + // 3.a.i. If ((n.[[Name]].localName == "*") or + // (n.[[Name]].localName == a.[[Name]].localName)) + // and ((n.[[Name]].uri == null) or (n.[[Name]].uri == a.[[Name]].uri)) + // 3.a.i.1. Let a.[[Parent]] = null + // 3.a.i.2. Remove the attribute a from x.[[Attributes]] + node.remove_matching_attribute(activation.gc(), &name); // 3.b. Return true return Ok(true); diff --git a/tests/tests/swfs/from_avmplus/e4x/XML/setNotification/output.ruffle.txt b/tests/tests/swfs/from_avmplus/e4x/XML/setNotification/output.ruffle.txt index ba27c88ad1fd..c19d05b65456 100644 --- a/tests/tests/swfs/from_avmplus/e4x/XML/setNotification/output.ruffle.txt +++ b/tests/tests/swfs/from_avmplus/e4x/XML/setNotification/output.ruffle.txt @@ -1,2 +1,8 @@ +attributeAdded-command PASSED! +attributeAdded-value PASSED! +attributeAdded-detail PASSED! +attributeChanged-command PASSED! +attributeChanged-value PASSED! +attributeChanged-detail PASSED! Asserting for TypeError PASSED! StringNotifier PASSED!