Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions core/src/avm2/e4x.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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;
Expand Down Expand Up @@ -89,6 +89,11 @@

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)]
Expand Down Expand Up @@ -393,17 +398,39 @@
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;

Check warning on line 411 in core/src/avm2/e4x.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (411)
};

self.remove_matching_nodes(gc_context, attributes, name)
}

fn remove_matching_nodes(
self,
gc_context: &Mutation<'gc>,
nodes: &mut Vec<E4XNode<'gc>>,
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);
Expand Down Expand Up @@ -1156,6 +1183,37 @@
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,
Expand Down
109 changes: 76 additions & 33 deletions core/src/avm2/object/xml_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -478,24 +478,79 @@
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");

Check warning on line 498 in core/src/avm2/object/xml_object.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (498)
};
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(());

Check warning on line 505 in core/src/avm2/object/xml_object.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (505)
};
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(());

Check warning on line 538 in core/src/avm2/object/xml_object.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (538)
};
attributes.push(attr);
}

self.node().trigger_notification(
activation,
self.into(),
E4XNotification::AttributeAdded,
self.into(),
local_name.into(),
value.into(),
);
}

// 6.h. Return
return Ok(());
}

Expand Down Expand Up @@ -635,25 +690,13 @@

// 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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!
Loading