Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avm2: Improve XML set_property_local implementation #13041

Merged
merged 9 commits into from
Sep 14, 2023
51 changes: 51 additions & 0 deletions core/src/avm2/e4x.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,57 @@ impl<'gc> E4XNode<'gc> {
node
}

/// Returns the amount of children in this node if this node is of Element kind, otherwise returns [None].
pub fn length(&self) -> Option<usize> {
let E4XNodeKind::Element { children, .. } = &*self.kind() else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd say do it the other way around?

it let E4XNodeKind::Element { children, .. } = &*self.kind() {
    Some(children.len())
} else {
    None
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can cleanup in follow up PR.

return None;
};

Some(children.len())
}

/// Removes all matching children matching provided name, returns the first child removed along with its index (if any).
pub fn remove_matching_children(
&self,
gc_context: &Mutation<'gc>,
name: &Multiname<'gc>,
) -> Option<(usize, E4XNode<'gc>)> {
let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(gc_context) else {
return None;
};

let index = children
.iter()
.position(|x| name.is_any_name() || x.matches_name(name));

let val = if let Some(index) = index {
Some((index, children[index]))
} else {
None
};

children.retain(|x| {
if name.is_any_name() || x.matches_name(name) {
// Remove parent.
x.set_parent(None, gc_context);
false
} else {
true
}
});

val
}

pub fn insert_at(&self, gc_context: &Mutation<'gc>, index: usize, node: E4XNode<'gc>) {
let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(gc_context) else {
return;
};

node.set_parent(Some(*self), gc_context);
children.insert(index, node);
}

pub fn remove_all_children(&self, gc_context: &Mutation<'gc>) {
let mut this = self.0.write(gc_context);
if let E4XNodeKind::Element { children, .. } = &mut this.kind {
Expand Down
14 changes: 14 additions & 0 deletions core/src/avm2/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ pub fn make_error_1004<'gc>(activation: &mut Activation<'_, 'gc>, method_name: &
}
}

#[inline(never)]
#[cold]
pub fn make_error_1087<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {
let err = type_error(
activation,
"Error #1087: Assignment to indexed XML is not allowed.",
1087,
);
match err {
Ok(err) => Error::AvmError(err),
Err(err) => err,
}
}

#[inline(never)]
#[cold]
pub fn make_error_1118<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {
Expand Down
30 changes: 6 additions & 24 deletions core/src/avm2/globals/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,7 @@ pub fn copy<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let xml = this.as_xml_object().unwrap();
let node = xml.node();
Ok(XmlObject::new(node.deep_copy(activation.context.gc_context), activation).into())
Ok(xml.deep_copy(activation).into())
}

pub fn parent<'gc>(
Expand Down Expand Up @@ -690,29 +689,12 @@ pub fn replace<'gc>(
// 2. And then we insert a dummy E4XNode at the previously stored index, and use the replace method to correct it.

let index =
if let E4XNodeKind::Element { children, .. } = &mut *self_node.kind_mut(activation.gc()) {
let index = children
.iter()
.position(|x| multiname.is_any_name() || x.matches_name(&multiname));
children.retain(|x| {
if multiname.is_any_name() || x.matches_name(&multiname) {
// Remove parent.
x.set_parent(None, activation.gc());
false
} else {
true
}
});

if let Some(index) = index {
children.insert(index, E4XNode::dummy(activation.gc()));
index
// 8. If i == undefined, return x
} else {
return Ok(xml.into());
}
if let Some((index, _)) = self_node.remove_matching_children(activation.gc(), &multiname) {
self_node.insert_at(activation.gc(), index, E4XNode::dummy(activation.gc()));
index
// 8. If i == undefined, return x
} else {
unreachable!("E4XNode kind should be of element kind");
return Ok(xml.into());
};

// 9. Call the [[Replace]] method of x with arguments ToString(i) and c
Expand Down
134 changes: 100 additions & 34 deletions core/src/avm2/object/xml_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::avm2::activation::Activation;
use crate::avm2::e4x::{name_to_multiname, E4XNode, E4XNodeKind};
use crate::avm2::error::make_error_1087;
use crate::avm2::object::script_object::ScriptObjectData;
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject, XmlListObject};
use crate::avm2::string::AvmString;
Expand Down Expand Up @@ -82,6 +83,11 @@ impl<'gc> XmlObject<'gc> {
Ref::map(self.0.read(), |data| &data.node)
}

pub fn deep_copy(&self, activation: &mut Activation<'_, 'gc>) -> XmlObject<'gc> {
let node = self.node();
XmlObject::new(node.deep_copy(activation.gc()), activation)
}

pub fn equals(
&self,
other: &Value<'gc>,
Expand Down Expand Up @@ -302,15 +308,41 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> {
Ok(self.has_own_property(&name))
}

// ECMA-357 9.1.1.2 [[Put]] (P, V)
fn set_property_local(
self,
name: &Multiname<'gc>,
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
let mc = activation.context.gc_context;
// 1. If ToString(ToUint32(P)) == P, throw a TypeError exception
if let Some(local_name) = name.local_name() {
if local_name.parse::<usize>().is_ok() {
return Err(make_error_1087(activation));
}
}

// 2. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return
if !matches!(*self.node().kind(), E4XNodeKind::Element { .. }) {
return Ok(());
}

// 4. Else
// 4.a. Let c be the result of calling the [[DeepCopy]] method of V
let value = if let Some(xml) = value.as_object().and_then(|x| x.as_xml_object()) {
xml.deep_copy(activation).into()
} else if let Some(list) = value.as_object().and_then(|x| x.as_xml_list_object()) {
list.deep_copy(activation).into()
// 3. If (Type(V) ∉ {XML, XMLList}) or (V.[[Class]] ∈ {"text", "attribute"})
// 3.a. Let c = ToString(V)
} else {
value
};

// 5. Let n = ToXMLName(P)
// 6. If Type(n) is AttributeName
if name.is_attribute() {
let mc = activation.context.gc_context;
self.delete_property_local(activation, name)?;
let Some(local_name) = name.local_name() else {
return Err(format!("Cannot set attribute {:?} without a local name", name).into());
Expand All @@ -328,50 +360,84 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> {
return Ok(());
}

let self_node = *self.node();
let write = self.0.write(mc);
let mut kind = write.node.kind_mut(mc);
let E4XNodeKind::Element { children, .. } = &mut *kind else {
// 7. Let isValidName be the result of calling the function isXMLName (section 13.1.2.1) with argument n
let is_valid_name = name.local_name().map_or(Ok(false), |x| {
crate::avm2::e4x::is_xml_name(activation, x.into())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(in retrospect, maybe passing an AvmString to is_xml_name would have been a nicer design, so it wouldn't need a Result here)

Copy link
Member Author

@sleepycatcoding sleepycatcoding Sep 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also realized this myself, I should have left the null and undefined handling in AS3 isXMLName function. Can cleanup in follow up PR.

})?;
// 8. If isValidName is false and n.localName is not equal to the string "*", return
if !is_valid_name && !name.is_any_name() {
return Ok(());
};

if value.as_object().map_or(false, |obj| {
obj.as_xml_object().is_some() || obj.as_xml_list_object().is_some()
}) {
return Err(
format!("Cannot set an XML/XMLList object {value:?} as an element yet").into(),
);
}

if name.is_any_name() {
return Err("Any name (*) not yet implemented for set".into());
}
// 10. Let primitiveAssign = (Type(c) ∉ {XML, XMLList}) and (n.localName is not equal to the string "*")
let primitive_assign = !value.as_object().map_or(false, |x| {
x.as_xml_list_object().is_some() || x.as_xml_object().is_some()
}) && !name.is_any_name();

let text = value.coerce_to_string(activation)?;
let self_node = self.node();

let matches: Vec<_> = children
.iter()
.filter(|child| child.matches_name(name))
.collect();
match matches.as_slice() {
[] => {
let element_with_text = E4XNode::element(
mc,
// 9. Let i = undefined
// 11.
let index = self_node.remove_matching_children(activation.gc(), name);

let index = if let Some((index, node)) = index {
self_node.insert_at(activation.gc(), index, node);
index
// 12. If i == undefined
} else {
// 12.a. Let i = x.[[Length]]
let index = self_node.length().expect("Node should be of element kind");
self_node.insert_at(activation.gc(), index, E4XNode::dummy(activation.gc()));

// 12.b. If (primitiveAssign == true)
if primitive_assign {
// 12.b.i. If (n.uri == null)
// 12.b.i.1. Let name be a new QName created as if by calling the constructor new
// QName(GetDefaultNamespace(), n)
// 12.b.ii. Else
// 12.b.ii.1. Let name be a new QName created as if by calling the constructor new QName(n)

// 12.b.iii. Create a new XML object y with y.[[Name]] = name, y.[[Class]] = "element" and y.[[Parent]] = x
let node = E4XNode::element(
activation.gc(),
name.explict_namespace(),
name.local_name().unwrap(),
write.node,
*self_node,
);
element_with_text.append_child(mc, E4XNode::text(mc, text, Some(self_node)))?;
children.push(element_with_text);
Ok(())
// 12.b.v. Call the [[Replace]] method of x with arguments ToString(i) and y
self_node.replace(index, XmlObject::new(node, activation).into(), activation)?;
// FIXME: 12.b.iv. Let ns be the result of calling [[GetNamespace]] on name with no arguments
// 12.b.vi. Call [[AddInScopeNamespace]] on y with argument ns
}
[child] => {
child.remove_all_children(mc);
child.append_child(mc, E4XNode::text(mc, text, Some(self_node)))?;
Ok(())

index
};

// 13. If (primitiveAssign == true)
if primitive_assign {
let E4XNodeKind::Element { children, .. } = &mut *self_node.kind_mut(activation.gc())
else {
unreachable!("Node should be of Element kind");
};

// 13.a. Delete all the properties of the XML object x[i]
children[index].remove_all_children(activation.gc());

// 13.b. Let s = ToString(c)
let val = value.coerce_to_string(activation)?;

// 13.c. If s is not the empty string, call the [[Replace]] method of x[i] with arguments "0" and s
if !val.is_empty() {
children[index].replace(0, value, activation)?;
}
_ => Err(format!("Can not replace multiple elements yet: {name:?} = {value:?}").into()),
// 14. Else
} else {
// 14.a. Call the [[Replace]] method of x with arguments ToString(i) and c
self_node.replace(index, value, activation)?;
}

// 15. Return
Ok(())
}

fn delete_property_local(
Expand Down
18 changes: 18 additions & 0 deletions tests/tests/swfs/avm2/xml_advanced/Test.as
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package {
import flash.display.Sprite;

public class Test extends Sprite {}
}

var main:XML = <root><hello/></root>;
trace(main);

trace("Set XML as child");
var xmlChild:XML = <item><stuff>Hello</stuff><more><stuff/></more></item>;
main.p = xmlChild;
trace(main);

trace("Set XMLList as child");
var listChild:XMLList = new XMLList("<list><item>A</item><item>B</item><item>C</item></list>");
main.x = listChild;
trace(main);
28 changes: 28 additions & 0 deletions tests/tests/swfs/avm2/xml_advanced/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<root>
<hello/>
</root>
Set XML as child
<root>
<hello/>
<item>
<stuff>Hello</stuff>
<more>
<stuff/>
</more>
</item>
</root>
Set XMLList as child
<root>
<hello/>
<item>
<stuff>Hello</stuff>
<more>
<stuff/>
</more>
</item>
<list>
<item>A</item>
<item>B</item>
<item>C</item>
</list>
</root>
Binary file added tests/tests/swfs/avm2/xml_advanced/test.swf
Binary file not shown.
1 change: 1 addition & 0 deletions tests/tests/swfs/avm2/xml_advanced/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
num_ticks = 1
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
num_ticks = 1
known_failure = true
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
num_ticks = 1
known_failure = true
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
num_ticks = 1
known_failure = true