From fbaa02e2958ebaaf4b6e16e2a5f072a784b97824 Mon Sep 17 00:00:00 2001 From: sleepycatcoding <131554884+sleepycatcoding@users.noreply.github.com> Date: Sun, 27 Aug 2023 00:42:43 +0300 Subject: [PATCH] avm2: Implement XML.insertChildAfter and XML.insertChildBefore --- core/src/avm2/e4x.rs | 146 +++++++++++++++++- core/src/avm2/globals/XML.as | 12 ++ core/src/avm2/globals/xml.rs | 98 ++++++++++++ .../Error1118IllegalCyclicalLoop/test.toml | 1 - .../from_avmplus/e4x/XML/e13_4_4_18/test.toml | 1 - .../from_avmplus/e4x/XML/e13_4_4_19/test.toml | 1 - 6 files changed, 255 insertions(+), 4 deletions(-) diff --git a/core/src/avm2/e4x.rs b/core/src/avm2/e4x.rs index 32e737810..bc6d23729 100644 --- a/core/src/avm2/e4x.rs +++ b/core/src/avm2/e4x.rs @@ -13,7 +13,10 @@ use quick_xml::{ use crate::{avm2::TObject, xml::custom_unescape}; use super::{ - error::type_error, object::E4XOrXml, string::AvmString, Activation, Error, Multiname, Value, + error::{make_error_1118, type_error}, + object::E4XOrXml, + string::AvmString, + Activation, Error, Multiname, Value, }; use crate::string::{WStr, WString}; @@ -295,6 +298,147 @@ impl<'gc> E4XNode<'gc> { Ok(()) } + // ECMA-357 9.1.1.4 [[DeleteByIndex]] (P) + pub fn delete_by_index(&self, index: usize, activation: &mut Activation<'_, 'gc>) { + let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else { + return; + }; + + // 2.a. If i is less than x.[[Length]] + if index < children.len() { + // 2.a.i. If x has a property with name P + // 2.a.i.2. Remove the property with the name P from x + let element = children.remove(index); + // 2.a.i.1. Let x[P].[[Parent]] = null + element.set_parent(None, activation.gc()); + } + } + + // ECMA-357 9.1.1.11 [[Insert]] (P, V) + pub fn insert( + &self, + index: usize, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + ) -> Result<(), Error<'gc>> { + // 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return + if !matches!(*self.kind(), E4XNodeKind::Element { .. }) { + return Ok(()); + } + + // 4. If Type(V) is XML and (V is x or an ancestor of x) throw an Error exception + if let Some(xml) = value.as_object().and_then(|x| x.as_xml_object()) { + if self.ancestors().any(|x| E4XNode::ptr_eq(x, *xml.node())) { + return Err(make_error_1118(activation)); + } + } + + // 10. If Type(V) is XMLList + if let Some(list) = value.as_object().and_then(|x| x.as_xml_list_object()) { + let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else { + unreachable!("E4XNode should be of element kind"); + }; + + // 10.a. For j = 0 to V.[[Length-1]] + for (child_index, child) in list.children().iter().enumerate() { + let child = child.node(); + // 10.a.i. V[j].[[Parent]] = x + child.set_parent(Some(*self), activation.gc()); + // 10.a.ii. x[i + j] = V[j] + children.insert(index + child_index, *child); + } + // 11. Else + } else { + // 11.a. Call the [[Replace]] method of x with arguments i and V + if let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) { + // NOTE: Make room for the replace operation. + children.insert(index, E4XNode::dummy(activation.gc())) + } + self.replace(index, value, activation)?; + } + + // 12. Return + Ok(()) + } + + // ECMA-357 9.1.1.12 [[Replace]] (P, V) + pub fn replace( + &self, + index: usize, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + ) -> Result<(), Error<'gc>> { + // 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return + if !matches!(*self.kind(), E4XNodeKind::Element { .. }) { + return Ok(()); + } + + // 5. If Type(V) is XML and V.[[Class]] ∈ {"element", "comment", "processing-instruction", "text"} + if let Some(xml) = value.as_object().and_then(|x| x.as_xml_object()) { + if matches!(*xml.node().kind(), E4XNodeKind::Attribute(_)) { + return Ok(()); + } + + // 5.a. If V.[[Class]] is “element” and (V is x or an ancestor of x) throw an Error exception + if matches!(*xml.node().kind(), E4XNodeKind::Element { .. }) { + if self.ancestors().any(|x| E4XNode::ptr_eq(x, *xml.node())) { + return Err(make_error_1118(activation)); + } + } + + // 5.b. Let V.[[Parent]] = x + xml.node().set_parent(Some(*self), activation.gc()); + + let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else { + unreachable!("E4XNode should be of element kind"); + }; + + // 5.c. If x has a property with name P + if let Some(node) = children.get(index) { + // 5.c.i. Let x[P].[[Parent]] = null + node.set_parent(None, activation.gc()); + } + + // 5.d. Let x[P] = V + if index >= children.len() { + children.push(*xml.node()); + } else { + children[index] = *xml.node(); + } + // 6. Else if Type(V) is XMLList + } else if let Some(_) = value.as_object().and_then(|x| x.as_xml_list_object()) { + // 6.a. Call the [[DeleteByIndex]] method of x with argument P + self.delete_by_index(index, activation); + // 6.b. Call the [[Insert]] method of x with arguments P and V + self.insert(index, value, activation)?; + // 7. Else + } else { + // 7.a. Let s = ToString(V) + let s: AvmString<'_> = value.coerce_to_string(activation)?; + // 7.b. Create a new XML object t with t.[[Class]] = "text", t.[[Parent]] = x and t.[[Value]] = s + let text_node = E4XNode::text(activation.gc(), s, Some(*self)); + + let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else { + unreachable!("E4XNode should be of element kind"); + }; + + // 7.c. If x has a property with name P + if let Some(node) = children.get(index) { + // 7.c.i. Let x[P].[[Parent]] = null + node.set_parent(None, activation.gc()); + } + + // 7.d. Let the value of property P of x be t + if index >= children.len() { + children.push(text_node); + } else { + children[index] = text_node; + } + } + + Ok(()) + } + /// Parses a value provided to `XML`/`XMLList` into a list of nodes. /// The caller is responsible for validating that the number of top-level nodes /// is correct (for XML, there should be exactly one.) diff --git a/core/src/avm2/globals/XML.as b/core/src/avm2/globals/XML.as index ee3993df3..87bfc52d2 100644 --- a/core/src/avm2/globals/XML.as +++ b/core/src/avm2/globals/XML.as @@ -78,6 +78,8 @@ package { AS3 native function length():int; AS3 native function comments():XMLList; AS3 native function processingInstructions(name:String = "*"):XMLList; + AS3 native function insertChildAfter(child1:Object, child2:Object):*; + AS3 native function insertChildBefore(child1:Object, child2:Object):*; AS3 function valueOf():XML { return this; @@ -219,6 +221,16 @@ package { return self.AS3::processingInstructions(name); } + prototype.insertChildAfter = function(child1:Object, child2:Object):* { + var self:XML = this; + return self.AS3::insertChildAfter(child1, child2); + } + + prototype.insertChildBefore = function(child1:Object, child2:Object):* { + var self:XML = this; + return self.AS3::insertChildBefore(child1, child2); + } + public static const length:int = 1; } } diff --git a/core/src/avm2/globals/xml.rs b/core/src/avm2/globals/xml.rs index 6c110eec5..062e2ff9f 100644 --- a/core/src/avm2/globals/xml.rs +++ b/core/src/avm2/globals/xml.rs @@ -485,3 +485,101 @@ pub fn processing_instructions<'gc>( Ok(XmlListObject::new(activation, nodes, Some(xml.into())).into()) } + +// ECMA-357 13.4.4.18 XML.prototype.insertChildAfter (child1, child2) +pub fn insert_child_after<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let xml = this.as_xml_object().unwrap(); + let child1 = args.try_get_object(activation, 0); + let child2 = args.get_object(activation, 1, "child2")?; + + // 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return + if !matches!(*xml.node().kind(), E4XNodeKind::Element { .. }) { + return Ok(Value::Undefined); + } + + // 3. Else if Type(child1) is XML + if let Some(child1) = child1.and_then(|x| x.as_xml_object()) { + // NOTE: We fetch the index separately to avoid borrowing errors. + let index = if let E4XNodeKind::Element { children, .. } = &*xml.node().kind() { + // 3.a. For i = 0 to x.[[Length]]-1 + // 3.a.i. If x[i] is the same object as child1 + children + .iter() + .position(|x| E4XNode::ptr_eq(*x, *child1.node())) + } else { + None + }; + + if let Some(index) = index { + // 3.a.i.1. Call the [[Insert]] method of x with arguments ToString(i + 1) and child2 + xml.node().insert(index + 1, child2.into(), activation)?; + // 3.a.i.2. Return x + return Ok(xml.into()); + } + // 2. If (child1 == null) + } else { + // 2.a. Call the [[Insert]] method of x with arguments "0" and child2 + xml.node().insert(0, child2.into(), activation)?; + // 2.b. Return x + return Ok(xml.into()); + } + + // 4. Return + return Ok(Value::Undefined); +} + +// ECMA-357 13.4.4.19 XML.prototype.insertChildBefore (child1, child2) +pub fn insert_child_before<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let xml = this.as_xml_object().unwrap(); + let child1 = args.try_get_object(activation, 0); + let child2 = args.get_object(activation, 1, "child2")?; + + // 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return + if !matches!(*xml.node().kind(), E4XNodeKind::Element { .. }) { + return Ok(Value::Undefined); + } + + // 3. Else if Type(child1) is XML + if let Some(child1) = child1.and_then(|x| x.as_xml_object()) { + // NOTE: We fetch the index separately to avoid borrowing errors. + let index = if let E4XNodeKind::Element { children, .. } = &*xml.node().kind() { + // 3.a. For i = 0 to x.[[Length]]-1 + // 3.a.i. If x[i] is the same object as child1 + children + .iter() + .position(|x| E4XNode::ptr_eq(*x, *child1.node())) + } else { + None + }; + + if let Some(index) = index { + // 3.a.i.1. Call the [[Insert]] method of x with arguments ToString(i) and child2 + xml.node().insert(index, child2.into(), activation)?; + // 3.a.i.2. Return x + return Ok(xml.into()); + } + // 2. If (child1 == null) + } else { + let length = if let E4XNodeKind::Element { children, .. } = &*xml.node().kind() { + children.len() + } else { + 0 + }; + + // 2.a. Call the [[Insert]] method of x with arguments ToString(x.[[Length]]) and child2 + xml.node().insert(length, child2.into(), activation)?; + // 2.b. Return x + return Ok(xml.into()); + } + + // 4. Return + return Ok(Value::Undefined); +} diff --git a/tests/tests/swfs/from_avmplus/as3/RuntimeErrors/Error1118IllegalCyclicalLoop/test.toml b/tests/tests/swfs/from_avmplus/as3/RuntimeErrors/Error1118IllegalCyclicalLoop/test.toml index 29f3cef79..cf6123969 100644 --- a/tests/tests/swfs/from_avmplus/as3/RuntimeErrors/Error1118IllegalCyclicalLoop/test.toml +++ b/tests/tests/swfs/from_avmplus/as3/RuntimeErrors/Error1118IllegalCyclicalLoop/test.toml @@ -1,2 +1 @@ num_ticks = 1 -known_failure = true diff --git a/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_18/test.toml b/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_18/test.toml index 29f3cef79..cf6123969 100644 --- a/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_18/test.toml +++ b/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_18/test.toml @@ -1,2 +1 @@ num_ticks = 1 -known_failure = true diff --git a/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_19/test.toml b/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_19/test.toml index 29f3cef79..cf6123969 100644 --- a/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_19/test.toml +++ b/tests/tests/swfs/from_avmplus/e4x/XML/e13_4_4_19/test.toml @@ -1,2 +1 @@ num_ticks = 1 -known_failure = true