From b26f2fd6fbf2655179a612a053d18ca243c678d7 Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Sat, 25 Feb 2023 14:06:36 -0600 Subject: [PATCH] avm2: Initial incomplete implementation of XML (#9647) --- core/src/avm2.rs | 1 + core/src/avm2/activation.rs | 5 +- core/src/avm2/e4x.rs | 395 +++++++++++++++++++++ core/src/avm2/error.rs | 8 + core/src/avm2/globals/XML.as | 42 ++- core/src/avm2/globals/XMLList.as | 14 +- core/src/avm2/globals/flash/utils.as | 13 +- core/src/avm2/globals/flash/utils.rs | 26 ++ core/src/avm2/globals/xml.rs | 80 +++++ core/src/avm2/globals/xml_list.rs | 81 ++++- core/src/avm2/multiname.rs | 23 +- core/src/avm2/object.rs | 16 +- core/src/avm2/object/xml_list_object.rs | 198 +++++++++++ core/src/avm2/object/xml_object.rs | 103 +++++- core/src/avm2/value.rs | 17 + tests/tests/swfs/avm2/typeof/test.swf | Bin 1717 -> 1179 bytes tests/tests/swfs/avm2/xml_basic/Test.as | 62 ++++ tests/tests/swfs/avm2/xml_basic/output.txt | 20 ++ tests/tests/swfs/avm2/xml_basic/test.fla | Bin 0 -> 3813 bytes tests/tests/swfs/avm2/xml_basic/test.swf | Bin 0 -> 1494 bytes tests/tests/swfs/avm2/xml_basic/test.toml | 1 + 21 files changed, 1079 insertions(+), 26 deletions(-) create mode 100644 core/src/avm2/e4x.rs create mode 100644 core/src/avm2/object/xml_list_object.rs create mode 100644 tests/tests/swfs/avm2/xml_basic/Test.as create mode 100644 tests/tests/swfs/avm2/xml_basic/output.txt create mode 100644 tests/tests/swfs/avm2/xml_basic/test.fla create mode 100644 tests/tests/swfs/avm2/xml_basic/test.swf create mode 100644 tests/tests/swfs/avm2/xml_basic/test.toml diff --git a/core/src/avm2.rs b/core/src/avm2.rs index 4c1d886f3..8ec7fe6f3 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -28,6 +28,7 @@ pub mod bytearray; mod call_stack; mod class; mod domain; +mod e4x; pub mod error; mod events; mod function; diff --git a/core/src/avm2/activation.rs b/core/src/avm2/activation.rs index 5008b526e..2b2f4431f 100644 --- a/core/src/avm2/activation.rs +++ b/core/src/avm2/activation.rs @@ -2619,8 +2619,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { fn op_strict_equals(&mut self) -> Result, Error<'gc>> { let value2 = self.pop_stack(); let value1 = self.pop_stack(); - - self.push_stack(value1 == value2); + self.push_stack(value1.strict_eq(&value2)); Ok(FrameControl::Continue) } @@ -2877,7 +2876,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { "object" } } - Object::XmlObject(_) => { + Object::XmlObject(_) | Object::XmlListObject(_) => { if is_not_subclass { "xml" } else { diff --git a/core/src/avm2/e4x.rs b/core/src/avm2/e4x.rs new file mode 100644 index 000000000..ecfe907bb --- /dev/null +++ b/core/src/avm2/e4x.rs @@ -0,0 +1,395 @@ +use std::{ + cell::Ref, + fmt::{self, Debug}, +}; + +use gc_arena::{Collect, GcCell, MutationContext}; +use quick_xml::{ + events::{BytesStart, Event}, + Reader, +}; + +use super::{object::E4XOrXml, string::AvmString, Activation, Error, Multiname, TObject, Value}; + +/// The underlying XML node data, based on E4XNode in avmplus +/// This wrapped by XMLObject when necessary (see `E4XOrXml`) +#[derive(Copy, Clone, Collect, Debug)] +#[collect(no_drop)] +pub struct E4XNode<'gc>(GcCell<'gc, E4XNodeData<'gc>>); + +#[derive(Collect)] +#[collect(no_drop)] +pub struct E4XNodeData<'gc> { + parent: Option>, + local_name: Option>, + kind: E4XNodeKind<'gc>, +} + +impl<'gc> Debug for E4XNodeData<'gc> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("E4XNodeData") + // Don't print the actual parent, to avoid infinite recursion + .field("parent", &self.parent.is_some()) + .field("local_name", &self.local_name) + .field("kind", &self.kind) + .finish() + } +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub enum E4XNodeKind<'gc> { + Text(AvmString<'gc>), + Comment(AvmString<'gc>), + ProcessingInstruction(AvmString<'gc>), + Attribute(AvmString<'gc>), + Element { + attributes: Vec>, + children: Vec>, + }, +} + +impl<'gc> E4XNode<'gc> { + pub fn dummy(mc: MutationContext<'gc, '_>) -> Self { + E4XNode(GcCell::allocate( + mc, + E4XNodeData { + parent: None, + local_name: None, + kind: E4XNodeKind::Element { + attributes: vec![], + children: vec![], + }, + }, + )) + } + + fn append_child( + &self, + gc_context: MutationContext<'gc, '_>, + child: Self, + ) -> Result<(), Error<'gc>> { + let mut this = self.0.write(gc_context); + let mut child_data = match child.0.try_write(gc_context) { + Ok(data) => data, + Err(_) => { + return Err(Error::RustError( + format!( + "Circular write in append_child with self={:?} child={:?}", + self, child + ) + .into(), + )) + } + }; + + child_data.parent = Some(*self); + + match &mut this.kind { + E4XNodeKind::Element { children, .. } => { + children.push(child); + } + _ => { + // FIXME - figure out exactly when appending is allowed in FP, + // and throw the proper AVM error. + return Err(Error::RustError( + format!("Cannot append child {child:?} to node {:?}", this.kind).into(), + )); + } + } + 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.) + pub fn parse( + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + let string = match value { + Value::Bool(_) | Value::Number(_) | Value::Integer(_) => { + value.coerce_to_string(activation)? + } + Value::String(string) => string, + Value::Object(obj) => { + if matches!( + obj.as_primitive().as_deref(), + Some(Value::Bool(_) | Value::Number(_) | Value::String(_)) + ) { + value.coerce_to_string(activation)? + } else if let Some(_xml) = obj.as_xml_object() { + return Err(Error::RustError( + "Deep clone of XML not yet implemented".into(), + )); + } else { + return Err(Error::RustError( + format!("Could not convert value {value:?} to XML").into(), + )); + } + } + // The docs claim that this throws a TypeError, but it actually doesn't + Value::Null | Value::Undefined => AvmString::default(), + }; + + let data_utf8 = string.to_utf8_lossy(); + let mut parser = Reader::from_str(&data_utf8); + let mut buf = Vec::new(); + let mut open_tags: Vec> = vec![]; + + // FIXME - look this up from static property and settings + let ignore_white = true; + + let mut top_level = vec![]; + let mut depth = 0; + + // This can't be a closure that captures these variables, because we need to modify them + // outside of this body. + fn push_childless_node<'gc>( + node: E4XNode<'gc>, + open_tags: &mut [E4XNode<'gc>], + top_level: &mut Vec>, + depth: usize, + activation: &mut Activation<'_, 'gc>, + ) -> Result<(), Error<'gc>> { + if let Some(current_tag) = open_tags.last_mut() { + current_tag.append_child(activation.context.gc_context, node)?; + } + + if depth == 0 { + top_level.push(node); + } + Ok(()) + } + + loop { + let event = parser.read_event(&mut buf).map_err(|error| { + Error::RustError(format!("XML parsing error: {error:?}").into()) + })?; + + match &event { + Event::Start(bs) => { + let child = E4XNode::from_start_event(activation, bs)?; + + if let Some(current_tag) = open_tags.last_mut() { + current_tag.append_child(activation.context.gc_context, child)?; + } + open_tags.push(child); + depth += 1; + } + Event::Empty(bs) => { + let node = E4XNode::from_start_event(activation, bs)?; + push_childless_node(node, &mut open_tags, &mut top_level, depth, activation)?; + } + Event::End(_) => { + depth -= 1; + let node = open_tags.pop().unwrap(); + if depth == 0 { + top_level.push(node); + } + } + Event::Text(bt) => { + let text = bt.unescaped()?; + let is_whitespace_char = |c: &u8| matches!(*c, b'\t' | b'\n' | b'\r' | b' '); + let is_whitespace_text = text.iter().all(is_whitespace_char); + if !(text.is_empty() || ignore_white && is_whitespace_text) { + let text = AvmString::new_utf8_bytes(activation.context.gc_context, &text); + let node = E4XNode(GcCell::allocate( + activation.context.gc_context, + E4XNodeData { + parent: None, + local_name: None, + kind: E4XNodeKind::Text(text), + }, + )); + push_childless_node( + node, + &mut open_tags, + &mut top_level, + depth, + activation, + )?; + } + } + Event::Comment(bt) | Event::PI(bt) => { + let text = bt.unescaped()?; + let text = AvmString::new_utf8_bytes(activation.context.gc_context, &text); + let kind = match event { + Event::Comment(_) => E4XNodeKind::Comment(text), + Event::PI(_) => E4XNodeKind::ProcessingInstruction(text), + _ => unreachable!(), + }; + let node = E4XNode(GcCell::allocate( + activation.context.gc_context, + E4XNodeData { + parent: None, + local_name: None, + kind, + }, + )); + + push_childless_node(node, &mut open_tags, &mut top_level, depth, activation)?; + } + Event::Decl(bd) => { + return Err(Error::RustError( + format!("XML declaration {bd:?} is not yet implemented").into(), + )) + } + Event::DocType(bt) => { + return Err(Error::RustError( + format!("XML doctype {bt:?} is not yet implemented").into(), + )) + } + Event::Eof => break, + _ => {} + } + } + + if top_level.is_empty() { + top_level.push(E4XNode(GcCell::allocate( + activation.context.gc_context, + E4XNodeData { + parent: None, + local_name: None, + kind: E4XNodeKind::Text(AvmString::default()), + }, + ))); + } + Ok(top_level) + } + + /// Construct an XML Element node from a `quick_xml` `BytesStart` event. + /// + /// The returned node will always be an `Element`, and it must only contain + /// valid encoded UTF-8 data. (Other encoding support is planned later.) + pub fn from_start_event( + activation: &mut Activation<'_, 'gc>, + bs: &BytesStart<'_>, + ) -> Result { + // FIXME - handle namespace + let name = AvmString::new_utf8_bytes(activation.context.gc_context, bs.local_name()); + + let mut attribute_nodes = Vec::new(); + + let attributes: Result, _> = bs.attributes().collect(); + for attribute in attributes? { + let key = AvmString::new_utf8_bytes(activation.context.gc_context, attribute.key); + let value_bytes = attribute.unescaped_value()?; + let value = AvmString::new_utf8_bytes(activation.context.gc_context, &value_bytes); + + let attribute_data = E4XNodeData { + parent: None, + local_name: Some(key), + kind: E4XNodeKind::Attribute(value), + }; + let attribute = E4XNode(GcCell::allocate( + activation.context.gc_context, + attribute_data, + )); + attribute_nodes.push(attribute); + } + + let data = E4XNodeData { + parent: None, + local_name: Some(name), + kind: E4XNodeKind::Element { + attributes: attribute_nodes, + children: Vec::new(), + }, + }; + + Ok(E4XNode(GcCell::allocate( + activation.context.gc_context, + data, + ))) + } + + pub fn local_name(&self) -> Option> { + self.0.read().local_name + } + + pub fn matches_name(&self, name: &Multiname<'gc>) -> bool { + // FIXME - we need to handle namespaces heere + if let Some(local_name) = self.local_name() { + Some(local_name) == name.local_name() + } else { + false + } + } + + pub fn has_simple_content(&self) -> bool { + match &self.0.read().kind { + E4XNodeKind::Element { children, .. } => children + .iter() + .all(|child| !matches!(&*child.kind(), E4XNodeKind::Element { .. })), + E4XNodeKind::Text(_) => true, + E4XNodeKind::Attribute(_) => true, + E4XNodeKind::Comment(_) => false, + E4XNodeKind::ProcessingInstruction(_) => false, + } + } + + pub fn xml_to_string( + &self, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + match &self.0.read().kind { + E4XNodeKind::Text(text) => Ok(*text), + E4XNodeKind::Attribute(text) => Ok(*text), + E4XNodeKind::Element { children, .. } => { + if self.has_simple_content() { + return simple_content_to_string( + children.iter().map(|node| E4XOrXml::E4X(*node)), + activation, + ); + } + + Err(format!( + "XML.toString(): Not yet implemented non-simple {:?} children {:?}", + self, children + ) + .into()) + } + other => Err(format!("XML.toString(): Not yet implemented for {other:?}").into()), + } + } + + pub fn xml_to_xml_string( + &self, + _activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + match &self.0.read().kind { + E4XNodeKind::Text(text) => Ok(*text), + E4XNodeKind::Element { .. } => { + Err(format!("XML.toXMLString(): Not yet implemented element {:?}", self).into()) + } + other => Err(format!("XML.toXMLString(): Not yet implemented for {other:?}").into()), + } + } + + pub fn kind(&self) -> Ref<'_, E4XNodeKind<'gc>> { + Ref::map(self.0.read(), |r| &r.kind) + } + + pub fn ptr_eq(first: E4XNode<'gc>, second: E4XNode<'gc>) -> bool { + GcCell::ptr_eq(first.0, second.0) + } +} + +pub fn simple_content_to_string<'gc>( + children: impl Iterator>, + activation: &mut Activation<'_, 'gc>, +) -> Result, Error<'gc>> { + let mut out = AvmString::default(); + for child in children { + if matches!( + &*child.node().kind(), + E4XNodeKind::Comment(_) | E4XNodeKind::ProcessingInstruction(_) + ) { + continue; + } + let child_str = child.node().xml_to_string(activation)?; + out = AvmString::concat(activation.context.gc_context, out, child_str); + } + Ok(out) +} diff --git a/core/src/avm2/error.rs b/core/src/avm2/error.rs index c6192035b..520addf54 100644 --- a/core/src/avm2/error.rs +++ b/core/src/avm2/error.rs @@ -239,3 +239,11 @@ impl<'gc> From for Error<'gc> { Error::RustError(val.into()) } } + +// TODO - Remove this, and convert `quick_xml` errors into AVM errors, +// specific to the XML method that was original invoked. +impl<'gc> From for Error<'gc> { + fn from(val: quick_xml::Error) -> Error<'gc> { + Error::RustError(val.into()) + } +} diff --git a/core/src/avm2/globals/XML.as b/core/src/avm2/globals/XML.as index 560d42e81..2460966b5 100644 --- a/core/src/avm2/globals/XML.as +++ b/core/src/avm2/globals/XML.as @@ -1,4 +1,44 @@ package { [Ruffle(InstanceAllocator)] - public final dynamic class XML {} + public final dynamic class XML { + public function XML(value:* = undefined) { + this.init(value); + } + + private native function init(value:*):void; + + AS3 native function name():Object; + AS3 native function localName():Object; + AS3 native function toXMLString():String; + + AS3 native function toString():String; + + prototype.name = function():Object { + var self:XML = this; + // NOTE - `self.name()` should be sufficient here (and in all of the other methods) + // However, asc.jar doesn't resolve the 'AS3' namespace when I do + // 'self.name()' here, which leads to the prototype method invoking + // itself, instead of the AS3 method. + return self.AS3::name(); + }; + + prototype.localName = function():Object { + var self:XML = this; + return self.AS3::localName(); + }; + + prototype.toXMLString = function():String { + var self:XML = this; + return self.AS3::toXMLString(); + }; + + prototype.toString = function():String { + if (this === prototype) { + return ""; + } + var self:XML = this; + return self.AS3::toString(); + }; + + } } \ No newline at end of file diff --git a/core/src/avm2/globals/XMLList.as b/core/src/avm2/globals/XMLList.as index a3548e5d2..82f253fd0 100644 --- a/core/src/avm2/globals/XMLList.as +++ b/core/src/avm2/globals/XMLList.as @@ -1,4 +1,16 @@ package { [Ruffle(InstanceAllocator)] - public final dynamic class XMLList {} + public final dynamic class XMLList { + + public function XMLList(value:* = undefined) { + this.init(value); + } + + private native function init(value:*): void; + + AS3 native function hasSimpleContent():Boolean; + AS3 native function length():int + + public native function toString():String; + } } \ No newline at end of file diff --git a/core/src/avm2/globals/flash/utils.as b/core/src/avm2/globals/flash/utils.as index ae1827091..1340f5187 100644 --- a/core/src/avm2/globals/flash/utils.as +++ b/core/src/avm2/globals/flash/utils.as @@ -4,18 +4,7 @@ package flash.utils { public native function getQualifiedSuperclassName(value:*):String; public native function getTimer():int; - // note: this is an extremely silly hack, - // made specifically to fool com.adobe.serialization.json.JsonEncoder. - // this relies on the fact that a.@b in Ruffle is unimplemented and behaves like a.b. - // once we get proper XML support, this entire impl is to be trashed. - public function describeType(value:*): XML { - import __ruffle__.stub_method; - stub_method("flash.utils", "describeType"); - - var ret = new XML(); - ret.name = getQualifiedClassName(value); - return ret; - } + public native function describeType(value:*): XML; public native function setInterval(closure:Function, delay:Number, ... arguments):uint; public native function clearInterval(id:uint):void; diff --git a/core/src/avm2/globals/flash/utils.rs b/core/src/avm2/globals/flash/utils.rs index a7a3450a8..04db7ffb1 100644 --- a/core/src/avm2/globals/flash/utils.rs +++ b/core/src/avm2/globals/flash/utils.rs @@ -3,6 +3,7 @@ use crate::avm2::object::TObject; use crate::avm2::QName; use crate::avm2::{Activation, Error, Object, Value}; +use crate::avm2_stub_method; use crate::string::AvmString; use crate::string::WString; use instant::Instant; @@ -260,3 +261,28 @@ pub fn get_definition_by_name<'gc>( let qname = QName::from_qualified_name(name, activation); appdomain.get_defined_value(activation, qname) } + +// Implements `flash.utils.describeType` +pub fn describe_type<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + // This method is very incomplete, and should be fully implemented + // once we have a better way of constructing XML from the Rust side + avm2_stub_method!(activation, "flash.utils", "describeType"); + + let mut xml_string = String::new(); + let qualified_name = + get_qualified_class_name(activation, None, &[args[0]])?.coerce_to_string(activation)?; + + xml_string += &format!(""); + let xml_avm_string = AvmString::new_utf8(activation.context.gc_context, xml_string); + + Ok(activation + .avm2() + .classes() + .xml + .construct(activation, &[xml_avm_string.into()])? + .into()) +} diff --git a/core/src/avm2/globals/xml.rs b/core/src/avm2/globals/xml.rs index e61a9c8de..6a623b4c1 100644 --- a/core/src/avm2/globals/xml.rs +++ b/core/src/avm2/globals/xml.rs @@ -1,3 +1,83 @@ //! XML builtin and prototype +use crate::avm2::e4x::E4XNode; pub use crate::avm2::object::xml_allocator; +use crate::avm2::object::{QNameObject, TObject}; +use crate::avm2::{Activation, Error, Object, QName, Value}; +use crate::avm2_stub_method; + +pub fn init<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.unwrap().as_xml_object().unwrap(); + let value = args[0]; + + match E4XNode::parse(value, activation) { + Ok(nodes) => { + if nodes.len() != 1 { + return Err(Error::RustError( + format!( + "XML constructor must be called with a single node: found {:?}", + nodes + ) + .into(), + )); + } + this.set_node(activation.context.gc_context, nodes[0]) + } + Err(e) => { + return Err(Error::RustError( + format!("Failed to parse XML: {e:?}").into(), + )) + } + } + + Ok(Value::Undefined) +} + +pub fn name<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let node = this.unwrap().as_xml_object().unwrap(); + if let Some(local_name) = node.local_name() { + avm2_stub_method!(activation, "XML", "name", "namespaces"); + // FIXME - use namespace + let namespace = activation.avm2().public_namespace; + Ok(QNameObject::from_qname(activation, QName::new(namespace, local_name))?.into()) + } else { + Ok(Value::Null) + } +} + +pub fn local_name<'gc>( + _activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let node = this.unwrap().as_xml_object().unwrap(); + Ok(node.local_name().map_or(Value::Null, Value::String)) +} + +pub fn to_string<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let xml = this.unwrap().as_xml_object().unwrap(); + let node = xml.node(); + Ok(Value::String(node.xml_to_string(activation)?)) +} + +pub fn to_xml_string<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let xml = this.unwrap().as_xml_object().unwrap(); + let node = xml.node(); + Ok(Value::String(node.xml_to_xml_string(activation)?)) +} diff --git a/core/src/avm2/globals/xml_list.rs b/core/src/avm2/globals/xml_list.rs index ec2d99630..eaba99f98 100644 --- a/core/src/avm2/globals/xml_list.rs +++ b/core/src/avm2/globals/xml_list.rs @@ -1,4 +1,81 @@ //! XMLList builtin and prototype -// XMLList currently uses the same instance allocator as XML -pub use crate::avm2::object::xml_allocator as xml_list_allocator; +pub use crate::avm2::object::xml_list_allocator; +use crate::{ + avm2::{ + e4x::{simple_content_to_string, E4XNode, E4XNodeKind}, + object::E4XOrXml, + Activation, Error, Object, TObject, Value, + }, + avm2_stub_method, +}; + +fn has_simple_content_inner(children: &[E4XOrXml<'_>]) -> bool { + match children { + [] => true, + [child] => child.node().has_simple_content(), + _ => children + .iter() + .all(|child| !matches!(&*child.node().kind(), E4XNodeKind::Element { .. })), + } +} + +pub fn init<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Option>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.unwrap().as_xml_list_object().unwrap(); + let value = args[0]; + + match E4XNode::parse(value, activation) { + Ok(nodes) => { + this.set_children( + activation.context.gc_context, + nodes.into_iter().map(E4XOrXml::E4X).collect(), + ); + } + Err(e) => { + return Err(Error::RustError( + format!("Failed to parse XML: {e:?}").into(), + )) + } + } + + Ok(Value::Undefined) +} + +pub fn has_simple_content<'gc>( + _activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let list = this.unwrap().as_xml_list_object().unwrap(); + let children = list.children(); + Ok(has_simple_content_inner(&children).into()) +} + +pub fn to_string<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let list = this.unwrap().as_xml_list_object().unwrap(); + let children = list.children(); + if has_simple_content_inner(&children) { + Ok(simple_content_to_string(children.iter().cloned(), activation)?.into()) + } else { + avm2_stub_method!(activation, "XMLList", "toString", "non-simple content"); + Err("XMLList.toString() for non-simple content: not yet implemented".into()) + } +} + +pub fn length<'gc>( + _activation: &mut Activation<'_, 'gc>, + this: Option>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let list = this.unwrap().as_xml_list_object().unwrap(); + let children = list.children(); + Ok(children.len().into()) +} diff --git a/core/src/avm2/multiname.rs b/core/src/avm2/multiname.rs index ca6b8b7cc..1168fdbad 100644 --- a/core/src/avm2/multiname.rs +++ b/core/src/avm2/multiname.rs @@ -58,6 +58,8 @@ bitflags! { /// Whether the name needs to be read at runtime before use /// This should only be set when lazy-initialized in Activation. const HAS_LAZY_NAME = 1 << 1; + /// Whether this was a 'MultinameA' - used for XML attribute lookups + const ATTRIBUTE = 1 << 2; } } @@ -101,6 +103,11 @@ impl<'gc> Multiname<'gc> { self.has_lazy_ns() || self.has_lazy_name() } + #[inline(always)] + pub fn is_attribute(&self) -> bool { + self.flags.contains(MultinameFlags::ATTRIBUTE) + } + /// Read a namespace set from the ABC constant pool, and return a list of /// copied namespaces. fn abc_namespace_set( @@ -146,7 +153,7 @@ impl<'gc> Multiname<'gc> { let abc = translation_unit.abc(); let abc_multiname = Self::resolve_multiname_index(&abc, multiname_index)?; - Ok(match abc_multiname { + let mut multiname = match abc_multiname { AbcMultiname::QName { namespace, name } | AbcMultiname::QNameA { namespace, name } => { Self { ns: NamespaceSet::single(translation_unit.pool_namespace(*namespace, mc)?), @@ -212,7 +219,19 @@ impl<'gc> Multiname<'gc> { } base } - }) + }; + + if matches!( + abc_multiname, + AbcMultiname::QNameA { .. } + | AbcMultiname::RTQNameA { .. } + | AbcMultiname::RTQNameLA { .. } + | AbcMultiname::MultinameA { .. } + | AbcMultiname::MultinameLA { .. } + ) { + multiname.flags |= MultinameFlags::ATTRIBUTE; + } + Ok(multiname) } #[inline(never)] diff --git a/core/src/avm2/object.rs b/core/src/avm2/object.rs index 3b799ea1e..22516e82a 100644 --- a/core/src/avm2/object.rs +++ b/core/src/avm2/object.rs @@ -55,6 +55,7 @@ mod stage_object; mod textformat_object; mod vector_object; mod vertex_buffer_3d_object; +mod xml_list_object; mod xml_object; pub use crate::avm2::object::array_object::{array_allocator, ArrayObject}; @@ -87,6 +88,7 @@ pub use crate::avm2::object::stage_object::{stage_allocator, StageObject}; pub use crate::avm2::object::textformat_object::{textformat_allocator, TextFormatObject}; pub use crate::avm2::object::vector_object::{vector_allocator, VectorObject}; pub use crate::avm2::object::vertex_buffer_3d_object::VertexBuffer3DObject; +pub use crate::avm2::object::xml_list_object::{xml_list_allocator, E4XOrXml, XmlListObject}; pub use crate::avm2::object::xml_object::{xml_allocator, XmlObject}; /// Represents an object that can be directly interacted with by the AVM2 @@ -106,6 +108,7 @@ pub use crate::avm2::object::xml_object::{xml_allocator, XmlObject}; EventObject(EventObject<'gc>), DispatchObject(DispatchObject<'gc>), XmlObject(XmlObject<'gc>), + XmlListObject(XmlListObject<'gc>), RegExpObject(RegExpObject<'gc>), ByteArrayObject(ByteArrayObject<'gc>), LoaderInfoObject(LoaderInfoObject<'gc>), @@ -165,6 +168,13 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy self.base().get_slot(slot_id) } Some(Property::Method { disp_id }) => { + // avmplus has a special case for XML and XMLList objects, so we need one as well + // https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/Toplevel.cpp#L629-L634 + if (self.as_xml_object().is_some() || self.as_xml_list_object().is_some()) + && multiname.contains_public_namespace() + { + return self.get_property_local(multiname, activation); + } if let Some(bound_method) = self.get_bound_method(disp_id) { return Ok(bound_method.into()); } @@ -1207,7 +1217,11 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy None } - fn as_xml(&self) -> Option> { + fn as_xml_object(&self) -> Option> { + None + } + + fn as_xml_list_object(&self) -> Option> { None } diff --git a/core/src/avm2/object/xml_list_object.rs b/core/src/avm2/object/xml_list_object.rs new file mode 100644 index 000000000..f371e33d2 --- /dev/null +++ b/core/src/avm2/object/xml_list_object.rs @@ -0,0 +1,198 @@ +use crate::avm2::activation::Activation; +use crate::avm2::e4x::E4XNode; +use crate::avm2::object::script_object::ScriptObjectData; +use crate::avm2::object::{Object, ObjectPtr, TObject}; +use crate::avm2::value::Value; +use crate::avm2::{Error, Multiname}; +use gc_arena::{Collect, GcCell, MutationContext}; +use std::cell::{Ref, RefMut}; +use std::fmt::{self, Debug}; +use std::ops::Deref; + +use super::{ClassObject, XmlObject}; + +/// A class instance allocator that allocates XMLList objects. +pub fn xml_list_allocator<'gc>( + class: ClassObject<'gc>, + activation: &mut Activation<'_, 'gc>, +) -> Result, Error<'gc>> { + let base = ScriptObjectData::new(class); + + Ok(XmlListObject(GcCell::allocate( + activation.context.gc_context, + XmlListObjectData { + base, + children: Vec::new(), + }, + )) + .into()) +} + +#[derive(Clone, Collect, Copy)] +#[collect(no_drop)] +pub struct XmlListObject<'gc>(GcCell<'gc, XmlListObjectData<'gc>>); + +impl<'gc> Debug for XmlListObject<'gc> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("XmlListObject") + .field("ptr", &self.0.as_ptr()) + .finish() + } +} + +impl<'gc> XmlListObject<'gc> { + pub fn new(activation: &mut Activation<'_, 'gc>, children: Vec>) -> Self { + let base = ScriptObjectData::new(activation.context.avm2.classes().xml_list); + XmlListObject(GcCell::allocate( + activation.context.gc_context, + XmlListObjectData { base, children }, + )) + } + + pub fn children(&self) -> Ref<'_, Vec>> { + Ref::map(self.0.read(), |d| &d.children) + } + + pub fn set_children(&self, mc: MutationContext<'gc, '_>, children: Vec>) { + self.0.write(mc).children = children; + } +} + +#[derive(Clone, Collect)] +#[collect(no_drop)] +pub struct XmlListObjectData<'gc> { + /// Base script object + base: ScriptObjectData<'gc>, + + children: Vec>, +} + +/// Holds either an `E4XNode` or an `XmlObject`. This can be converted +/// in-palce to an `XmlObject` via `get_or_create_xml`. +/// This deliberately does not implement `Copy`, since `get_or_create_xml` +/// takes `&mut self` +#[derive(Clone, Collect, Debug)] +#[collect(no_drop)] +pub enum E4XOrXml<'gc> { + E4X(E4XNode<'gc>), + Xml(XmlObject<'gc>), +} + +impl<'gc> E4XOrXml<'gc> { + pub fn get_or_create_xml(&mut self, activation: &mut Activation<'_, 'gc>) -> XmlObject<'gc> { + match self { + E4XOrXml::E4X(node) => { + let xml = XmlObject::new(*node, activation); + *self = E4XOrXml::Xml(xml); + xml + } + E4XOrXml::Xml(xml) => *xml, + } + } + + pub fn node(&self) -> E4XWrapper<'_, 'gc> { + match self { + E4XOrXml::E4X(node) => E4XWrapper::E4X(*node), + E4XOrXml::Xml(xml) => E4XWrapper::XmlRef(xml.node()), + } + } +} + +// Allows using `E4XOrXml` as an `E4XNode` via deref coercions, while +// storing the needed `Ref` wrappers +#[derive(Debug)] +pub enum E4XWrapper<'a, 'gc> { + E4X(E4XNode<'gc>), + XmlRef(Ref<'a, E4XNode<'gc>>), +} + +impl<'a, 'gc> Deref for E4XWrapper<'a, 'gc> { + type Target = E4XNode<'gc>; + + fn deref(&self) -> &Self::Target { + match self { + E4XWrapper::E4X(node) => node, + E4XWrapper::XmlRef(node) => node, + } + } +} + +impl<'gc> TObject<'gc> for XmlListObject<'gc> { + fn base(&self) -> Ref> { + Ref::map(self.0.read(), |read| &read.base) + } + + fn base_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut> { + RefMut::map(self.0.write(mc), |write| &mut write.base) + } + + fn as_ptr(&self) -> *const ObjectPtr { + self.0.as_ptr() as *const ObjectPtr + } + + fn value_of(&self, _mc: MutationContext<'gc, '_>) -> Result, Error<'gc>> { + Ok(Value::Object(Object::from(*self))) + } + + fn as_xml_list_object(&self) -> Option { + Some(*self) + } + + fn get_property_local( + self, + name: &Multiname<'gc>, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + // FIXME - implement everything from E4X spec (XMLListObject::getMultinameProperty in avmplus) + let mut write = self.0.write(activation.context.gc_context); + + if name.contains_public_namespace() { + if let Some(local_name) = name.local_name() { + if let Ok(index) = local_name.parse::() { + if let Some(child) = write.children.get_mut(index) { + return Ok(Value::Object(child.get_or_create_xml(activation).into())); + } else { + return Ok(Value::Undefined); + } + } + + let matched_children = write + .children + .iter_mut() + .flat_map(|child| { + let child_prop = child + .get_or_create_xml(activation) + .get_property_local(name, activation) + .unwrap(); + if let Some(prop_xml) = + child_prop.as_object().and_then(|obj| obj.as_xml_object()) + { + vec![E4XOrXml::Xml(prop_xml)] + } else if let Some(prop_xml_list) = child_prop + .as_object() + .and_then(|obj| obj.as_xml_list_object()) + { + // Flatten children + prop_xml_list.children().clone() + } else { + vec![] + } + }) + .collect(); + + return Ok(XmlListObject::new(activation, matched_children).into()); + } + } + + write.base.get_property_local(name, activation) + } + + fn set_property_local( + self, + _name: &Multiname<'gc>, + _value: Value<'gc>, + _activation: &mut Activation<'_, 'gc>, + ) -> Result<(), Error<'gc>> { + Err("Modifying an XMLList object is not yet implemented".into()) + } +} diff --git a/core/src/avm2/object/xml_object.rs b/core/src/avm2/object/xml_object.rs index 51169344f..d244eb036 100644 --- a/core/src/avm2/object/xml_object.rs +++ b/core/src/avm2/object/xml_object.rs @@ -1,14 +1,18 @@ //! Object representation for XML objects use crate::avm2::activation::Activation; +use crate::avm2::e4x::{E4XNode, E4XNodeKind}; use crate::avm2::object::script_object::ScriptObjectData; -use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; +use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject, XmlListObject}; +use crate::avm2::string::AvmString; use crate::avm2::value::Value; -use crate::avm2::Error; +use crate::avm2::{Error, Multiname}; use core::fmt; use gc_arena::{Collect, GcCell, MutationContext}; use std::cell::{Ref, RefMut}; +use super::xml_list_object::E4XOrXml; + /// A class instance allocator that allocates XML objects. pub fn xml_allocator<'gc>( class: ClassObject<'gc>, @@ -18,7 +22,10 @@ pub fn xml_allocator<'gc>( Ok(XmlObject(GcCell::allocate( activation.context.gc_context, - XmlObjectData { base }, + XmlObjectData { + base, + node: E4XNode::dummy(activation.context.gc_context), + }, )) .into()) } @@ -40,6 +47,35 @@ impl fmt::Debug for XmlObject<'_> { pub struct XmlObjectData<'gc> { /// Base script object base: ScriptObjectData<'gc>, + + node: E4XNode<'gc>, +} + +impl<'gc> XmlObject<'gc> { + pub fn new(node: E4XNode<'gc>, activation: &mut Activation<'_, 'gc>) -> Self { + XmlObject(GcCell::allocate( + activation.context.gc_context, + XmlObjectData { + base: ScriptObjectData::new(activation.context.avm2.classes().xml), + node, + }, + )) + } + pub fn set_node(&self, mc: MutationContext<'gc, '_>, node: E4XNode<'gc>) { + self.0.write(mc).node = node; + } + + pub fn local_name(&self) -> Option> { + self.0.read().node.local_name() + } + + pub fn matches_name(&self, multiname: &Multiname<'gc>) -> bool { + self.0.read().node.matches_name(multiname) + } + + pub fn node(&self) -> Ref<'_, E4XNode<'gc>> { + Ref::map(self.0.read(), |data| &data.node) + } } impl<'gc> TObject<'gc> for XmlObject<'gc> { @@ -59,7 +95,66 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> { Ok(Value::Object(Object::from(*self))) } - fn as_xml(&self) -> Option { + fn as_xml_object(&self) -> Option { Some(*self) } + + fn get_property_local( + self, + name: &Multiname<'gc>, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + // FIXME - implement everything from E4X spec (XMLObject::getMultinameProperty in avmplus) + let read = self.0.read(); + + if name.contains_public_namespace() { + if let Some(local_name) = name.local_name() { + // The only supported numerical index is 0 + if let Ok(index) = local_name.parse::() { + if index == 0 { + return Ok(self.into()); + } else { + return Ok(Value::Undefined); + } + } + + let matched_children = if let E4XNodeKind::Element { + children, + attributes, + } = &*read.node.kind() + { + let search_children = if name.is_attribute() { + attributes + } else { + children + }; + + search_children + .iter() + .filter_map(|child| { + if child.matches_name(name) { + Some(E4XOrXml::E4X(*child)) + } else { + None + } + }) + .collect::>() + } else { + Vec::new() + }; + return Ok(XmlListObject::new(activation, matched_children).into()); + } + } + + read.base.get_property_local(name, activation) + } + + fn set_property_local( + self, + _name: &Multiname<'gc>, + _value: Value<'gc>, + _activation: &mut Activation<'_, 'gc>, + ) -> Result<(), Error<'gc>> { + Err("Modifying an XML object is not yet implemented".into()) + } } diff --git a/core/src/avm2/value.rs b/core/src/avm2/value.rs index 5c9608385..efda5c6e8 100644 --- a/core/src/avm2/value.rs +++ b/core/src/avm2/value.rs @@ -14,6 +14,8 @@ use gc_arena::{Collect, MutationContext}; use std::cell::Ref; use swf::avm2::types::{DefaultValue as AbcDefaultValue, Index}; +use super::e4x::E4XNode; + /// Indicate what kind of primitive coercion would be preferred when coercing /// objects. #[derive(Eq, PartialEq)] @@ -1044,6 +1046,21 @@ impl<'gc> Value<'gc> { } } + /// Implements the strict-equality `===` check for AVM2. + pub fn strict_eq(&self, other: &Value<'gc>) -> bool { + if self == other { + true + } else { + // TODO - this should apply to (Array/Vector).indexOf, and possibility more places as well + if let Some(xml1) = self.as_object().and_then(|obj| obj.as_xml_object()) { + if let Some(xml2) = other.as_object().and_then(|obj| obj.as_xml_object()) { + return E4XNode::ptr_eq(*xml1.node(), *xml2.node()); + } + } + false + } + } + /// Determine if two values are abstractly equal to each other. /// /// This abstract equality algorithm is intended to match ECMA-262 3rd diff --git a/tests/tests/swfs/avm2/typeof/test.swf b/tests/tests/swfs/avm2/typeof/test.swf index dff0aa82bc08611faaf319fb8fabb088e2aeed7d..8322e7ebe2ed229b5d0dd40298cae768ff5b95b3 100644 GIT binary patch literal 1179 zcmV;M1Z4X|S5ptZ2LJ$goQ0HIQzAzc$Gdw5ng(IGUsMDY1-!subT7E6W!Kv-r7*n+f2v%)srFI9z4%sdc zlKw&{iqWpmfe_juU%q@vcyD5b75=To0~Jf4cqgE_b+>|820RZspK@$OW|+ zom929mMkE2AACF?F$y^7C7zcSC zmUwhy2wb?#xUTdvpTgyMudQZL93aj6!nxrD{)_#0>rB!vusA6ie!VRF`$km$t zZcF{djyi*;w$o~MLtpLBZVkaX{s!E~D!#;3ToZW<$No*bsy91rdUxHr=v-d8;Mr!S%+u{;8{V>f77HciHrwTGhO~i)6La z?(4mqs@~}Z^7*7ynY?M&v^os3#-5aC?6z-OE&n*}XXN_R)J#FnZDqY< za+z5#$XRI&s+q!8c50me{>Q|KYMC_us&qCsW*j6l_2D#Sg|nl>$k-juS~3GTOCcP> z+$G08pUTm6nnm2tvTm=VcX&yBv~0P_Wh)N1jz_K4_PIK#_L^ON@qdaZvngwTXalXS z_oY4l*oD2K>eYtUbMIQ)#@b-Mk8NFpMbeuC%_8Av{!XW@L(f{ze$wqVbuIagr9Wq3 zTa<-eVRuxDxNN@g3jt$H*C}QcJE<^5MGv)mDfLqtpfpHnh|(~n5sIUf#weYpbcWJd zO5>EyQJSE5o-xcKgv}5(M_7VToML{$0)zz#3lSD3ECw*grXk7{Z?IXy;-ELF<0Tbe zQSmhui&WfpStkKM0tC24xNUIThfjXPVSyZ$CYU$~2tjn%5W_Z%93uF{?!ltf$gDQY z5#zzhgUgPlkee$H4-!wkT={tL^EAMN6AL27E_e;bp2wuIs|42)$3blD9l$V0v;=gH z(|170h;9L0;Pk}-{}iA$pczP@OFKc8Nvn3l1I9AXC8)KVjt@|p3%@e63?+dlC#1~gu}Y0FRAe}afJIF!ro>bKst#is ztSkpJU^z@HuFRz*l^Jtjt#FV4i<#Cupj9pwl;F?EL@6b)7EztX5?E^-EJL-^w6cKK zxmW?@GSQlnROZX`NOc?Q;O05Yf$KJ1Sy@mP%Zo_$7zJ<(oNa*XG2K@c$!eQ2UMowA zRF;tHHHzSSP5+IuZ23OppyD5uey9X0es~@#!C(DlzcuJ1NCM>m`n(^+fBy6L2iSHP zGpDu44vd{H30CwwSW#TYC~F9SnE*I!uDz0e+%j+qJ=AT-X<`s?ov@GC>VvEyz6COJ tB`a4}{09Jo0WKHEO9B7@ literal 1717 zcmV;m21@xuS5pt)3;+OloTXN6QyR$@?jBy60R;ubx2O}v2#GK=C6d{0z=IVYHDk1tL}f;@AseNFL*!P|3SXoueqlO5Zsu}-Mdpx&pFR?&NJPo z&$L`4*IR@F|3qj8qvGTYLg?2Jb|JKFRI0f*#RK8G*=U(L@SZQ%EUTRp#jC5U6D@OPc}K7niqzDMdfTe&ErI#!x!$p!FE96Hubb^# z*_N49EBZN2YI;+=R@LZCY<=^^c>?)AwyE zzM-bxF*FL{EB);_I?BCjnU>np=&Rxm0Lgm2lH1rS9LVYY&5gax{(fGTw^Cc_{k@HJ z_Gv!*G%Ka-h2Bjas10V$~_B8e{DgZr4H2ezfz_zN(i?bGpy79-O?59wnN(;qhX&p6-}fK z%HC!|m&*1LQPFZ$-Ds-Tj@oWF>YB;~#p{Gw)3u+j)JvMEvg2Y~90uMyC9!|%?FgvA zefsoiX`(Y5m7`WZnl`@gT>N;Ed3RPkdukM$n-@R6{5FX`O!?r*#OOq$Eh@+eeF&Eb z1K^rKNf%JDdnux9ZwF7c=?7}8}%(P5zhdT4Xez53+u@psrA-r zy-6GO78P&Ns-ZTi{NyW9qo|X~x{uEbZMU@DA62O|0T`tl^52Wq1xcEsd3&z&5|t&o6xNY}7#tY$sd4^Be{TpqwRhRw`X8 z{g6LCJNe;F$>>z84O-H4gO-?k1e$jcH6}~RbbMlnoW~Nf z6b}pmEheYqV?+PvUq)tBiLJ3!t;J`D7z2b@wO_Ni2B>CosH*LJC8k z)pbx?JBaadzHR81&O#Ojdzc*E!(6|Q{AvY0VX#i@&t&Bb!K2<-uUY6N`$Pl#>`{cq zQ7x@TjqzfyhkD&M`Ko16*dz;1iXIZV!xwb;K>?D6`=o8uEt>cf8Nm(*2i#7$;YSy8_n+*BV+476HitP62ZqDxaUmYNF!DHo=iGkm>?PUJ zb~xhM;AMl)jmFS8v;1rbFn5AklWYhwH^c@nb~rKi!D}$~e-ye!CA_|L9LC-K7ck8M zx(nzcL-zpX251`4Wrk*6u%!U%0yG8=NIIO38wpGo#!i>_M!I>0fczE^HwZXB01G~+ zOqDsQ%$=cy8fM8#WYf*Fqi^4ZY3A}9|JA0l@hZ_-EL zgbqXKw}UYL$G`ve650-9-rL(`55`_!0Csc(b`&3C6z@6!<{bb>?7dgkKBl`kif*c| z<2A0!0qcc6;-B0|6tNbFN#(dyek_%rNab~@?1aBFywJO9j*r{j8A$miG>3X$&%I|6 L+6Csn1_!EmcyCsi diff --git a/tests/tests/swfs/avm2/xml_basic/Test.as b/tests/tests/swfs/avm2/xml_basic/Test.as new file mode 100644 index 000000000..71a471c6e --- /dev/null +++ b/tests/tests/swfs/avm2/xml_basic/Test.as @@ -0,0 +1,62 @@ +package { + public class Test { + public static function run() { + var soapXML:XML = + + + + + Quito + + + ; + + trace(soapXML.localName()); // Envelope + trace(XML.prototype.localName.call(soapXML)); + + var simpleXML:XML =

Hello world

; + trace("simpleXML.innerElem.p = " + simpleXML.innerElem.p); + + trace("XML.prototype.toString() = " + XML.prototype.toString()); + + var noArgs = new XML(); + trace("noArgs.toString() = " + noArgs.toString()); + trace("XML.prototype.toString.call(noArgs): " + XML.prototype.toString.call(noArgs)); + trace("noArgs.toXMLString() = " + noArgs.toXMLString()); + + var nullArg = new XML(null); + trace("nullArg.toString() = " + nullArg.toString()); + trace("nullArg.toString() = " + nullArg.toXMLString()); + + var undefinedArg = new XML(undefined); + trace("undefinedArg.toString() = " + undefinedArg.toString()); + trace("undefinedArg.toXMLString() = " + undefinedArg.toString()); + + var plainString:XML = new XML("Hello"); + trace("plainString.toString() = " + plainString.toString()); + trace("plainString.toXMLString() = " + plainString.toString()); + + var list = new XMLList("

First

Second

"); + trace("List children: " + list.length()); + + trace("List first child: " + list[0]); + trace("List second child: " + list[1]); + + var a = asdf; + var a1 = a.x; + var a2 = a1[0]; + var b1 = a.x; + var b2 = b1[0]; + trace("XMLList strict equal: " + (a1 === b1)); + trace("XML strict equal: " + (a2 === b2)); + + var weird = My Name; + trace("Get 'name' property': " + weird.name); + trace("Get 'AS#::name' property': " + (typeof a.AS3::name)); + + // FIXME - enable this when Ruffle throws coercion errors + //XML.prototype.name.apply(5); + } + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/xml_basic/output.txt b/tests/tests/swfs/avm2/xml_basic/output.txt new file mode 100644 index 000000000..85c76abfb --- /dev/null +++ b/tests/tests/swfs/avm2/xml_basic/output.txt @@ -0,0 +1,20 @@ +Envelope +Envelope +simpleXML.innerElem.p = Hello world +XML.prototype.toString() = +noArgs.toString() = +XML.prototype.toString.call(noArgs): +noArgs.toXMLString() = +nullArg.toString() = +nullArg.toString() = +undefinedArg.toString() = +undefinedArg.toXMLString() = +plainString.toString() = Hello +plainString.toXMLString() = Hello +List children: 2 +List first child: First +List second child: Second +XMLList strict equal: false +XML strict equal: true +Get 'name' property': My Name +Get 'AS#::name' property': function diff --git a/tests/tests/swfs/avm2/xml_basic/test.fla b/tests/tests/swfs/avm2/xml_basic/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..5588e5539701df9552be4424039fd8b310e11572 GIT binary patch literal 3813 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 LcmC980Kk6$bgc0= literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm2/xml_basic/test.swf b/tests/tests/swfs/avm2/xml_basic/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..2e8edb4585737d5f8e8d84b67c7642d2e2422e70 GIT binary patch literal 1494 zcmV;{1u6PNS5qr43IG6joQ+l8a??f>){!NxWIO-F`3I30NSa{T2@TX310e|z2x%u= zKnD+&&<{ta`^XChLhZEUC_UTsEZ9X}8{^N%K$auc~>e>_2Yg{kb1?K%| z3cbMW>eQlgfnnaCdnbhi9)nCSyzMaJQ|kGOduU0~AiP}i@$#~6s>N;9u$~$R)HE!r zblJMA9?-(#H(b3e7p4_*zzm+t1Yu&oR|wY1=+B=snYLM$gPA#3dxK z?8TppFG7#kzIu(i_d-27SbotwFkfn_XXrO?hSTR%k9fA~JllROdB%ZbQXC1Eeb?Rd zh;?Y1I7lB_HCo3BY6wzTiDB*fu3_zQrs4UZvTA`H26MkCi)$=U9nvYgKsl)|zS>Q`wyW>$_;P)oOKINz>+ zY|{S`9J^=B$)wad;#$`1+Ht#%MzfoUHr4RKbGm(?G^pzDQx{`9fsJOdZo5=hJ--!3T~*o{)o2rDVwBHC1&=`2y1_gz(|l4p8-i&bm3 z7MFT=cO`uARYf~AOy961$F+UiKXxdG;pUx^edtqn!=wl0l3`h$p;LZ9P1BYewrkc( zic>BrJ*x60XuR7GxvnAS$hTyv3&EKR9l(3?8sbL`+OX* zP?X2Ip=j@kPW2F@?LIx7U+Q-4WN-hTHqCq`ot}h`R%)WB<4;FVpH@x>6NHcP`~O;+wOneJ zRj*brDNTHHY8jfD_N20hh3LLae>qf5^dkKgH5a!>xnyCVmA8-Ou-{5bgxXzD&fKTI zd?lo=$oQrl>iWl5iZT~6Q;~Lecdl6cmw{Iu$2^|tY*M-2O{&=5DErk~Z8IG0>aOAV zbN^xPMd^4qq({{9y@8GJwvIca>g)K$_N~X8WW1dYJ+$xvRNXg@Xv}0z>Gy2Q$IR3f zb{*I7>D;&X%NKna7#A{PCOMuS6M2EZI0oYEq>y=#AOu7y$)*4<5D*9ul8^|OB0?X5 zegZQDE=V#En3J*~T>)txr0+n=A(JPtD*XUcAl(9K4M=iLS_esB32c(|L&9wl?h)Z0 z6K-3K|Dsqt6HGh~PYzE$9<#toEE*D=$TI@)fC30Z3E&tu0eEEyBsc((0lX(p^7k=} z>rZgMa-RkQ#4Q+PxE2h-a0`anu@;OZrdx21lUp##XIqdF=36jEaxEAal@?4$7%`bF zv|uW=+=A)!YS33DOXt_%HTg3bsE8jnC1yv2V6Y-qNj+JYK5V8SB! z%YW3<^}e^EGKz2^G;j7Z5iwRFTZDP@cQB61;>Qj>foLg8+W!%gM%*NFS3hnJ#E$6- zk+%rE(Yas-1x4dJvFF>cZImmdIv6?m;37%|jqf;T5iWm$*$P=&LFa3t3&ADSu4zI? zyNvMrFL1sxv~+!I2$Pav0i_!n*&L3gMTA9-WSRO%gqINCL=0`5!^J7BVWb!+npi*g zMwf!TCWZbtdNL@)Tq;=BQZbhfRxm|bleEZ)-^{9m7>jcqdHQ7hFf>uGUu{ zjf5MB?qRyjZxJj;>p7a`cg7&Nc|U9@;P~W8{^TKp4N^uXfmCATB#)4-j91xwm3_v{ z>;p>JSW+@>Djp!2>POc`09Du#n1DrANFbYuvQ;KN-47tkoYeW92{uv*W|kID@{|d) wPUfpq`Ra7OI+L%S&sRD8J%gF}*iU?V5{zTW?UBj$M{wre!Q4URAKL%yXj^vOng9R* literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm2/xml_basic/test.toml b/tests/tests/swfs/avm2/xml_basic/test.toml new file mode 100644 index 000000000..dbee897f5 --- /dev/null +++ b/tests/tests/swfs/avm2/xml_basic/test.toml @@ -0,0 +1 @@ +num_frames = 1