diff --git a/core/src/avm2/e4x.rs b/core/src/avm2/e4x.rs index 6d7685266..776d1afea 100644 --- a/core/src/avm2/e4x.rs +++ b/core/src/avm2/e4x.rs @@ -9,6 +9,8 @@ use quick_xml::{ Reader, }; +use crate::avm2::{error::type_error, TObject}; + use super::{object::E4XOrXml, string::AvmString, Activation, Error, Multiname, Value}; use crate::string::{WStr, WString}; @@ -117,14 +119,29 @@ impl<'gc> E4XNode<'gc> { /// 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>, + mut value: Value<'gc>, activation: &mut Activation<'_, 'gc>, ) -> Result, Error<'gc>> { - let string = match value { + let string = match &value { // The docs claim that this throws a TypeError, but it actually doesn't Value::Null | Value::Undefined => AvmString::default(), // The docs claim that only String, Number or Boolean are accepted, but that's also a lie - value => value.coerce_to_string(activation)?, + val => { + if let Some(obj) = val.as_object() { + if let Some(xml) = obj.as_xml_object() { + value = xml.call_public_property("toXMLString", &[], activation)?; + } else if let Some(list) = obj.as_xml_list_object() { + if list.length() == 1 { + value = list.children_mut(activation.context.gc_context)[0] + .get_or_create_xml(activation) + .call_public_property("toXMLString", &[], activation)?; + } else { + return Err(Error::AvmError(type_error(activation, "Error #1088: The markup in the document following the root element must be well-formed.", 1088)?)); + } + } + } + value.coerce_to_string(activation)? + } }; let data_utf8 = string.to_utf8_lossy(); @@ -342,15 +359,9 @@ impl<'gc> E4XNode<'gc> { pub fn xml_to_xml_string( &self, - _activation: &mut Activation<'_, 'gc>, + 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()), - } + return to_xml_string(E4XOrXml::E4X(*self), activation); } pub fn kind(&self) -> Ref<'_, E4XNodeKind<'gc>> { diff --git a/core/src/avm2/globals/xml.rs b/core/src/avm2/globals/xml.rs index 65ad05655..3683cd10f 100644 --- a/core/src/avm2/globals/xml.rs +++ b/core/src/avm2/globals/xml.rs @@ -15,30 +15,23 @@ pub fn init<'gc>( let this = this.unwrap().as_xml_object().unwrap(); let value = args[0]; - match E4XNode::parse(value, activation) { - Ok(nodes) => { - let node = match nodes.as_slice() { - // XML defaults to an empty text node when nothing was parsed - [] => E4XNode::text(activation.context.gc_context, AvmString::default()), - [node] => *node, - _ => { - return Err(Error::RustError( - format!( - "XML constructor must be called with a single node: found {:?}", - nodes - ) - .into(), - )) - } - }; - this.set_node(activation.context.gc_context, node); - } - Err(e) => { + let nodes = E4XNode::parse(value, activation)?; + + let node = match nodes.as_slice() { + // XML defaults to an empty text node when nothing was parsed + [] => E4XNode::text(activation.context.gc_context, AvmString::default()), + [node] => *node, + _ => { return Err(Error::RustError( - format!("Failed to parse XML: {e:?}").into(), + format!( + "XML constructor must be called with a single node: found {:?}", + nodes + ) + .into(), )) } - } + }; + this.set_node(activation.context.gc_context, node); Ok(Value::Undefined) } diff --git a/tests/tests/swfs/avm2/xml_ctor_from_tostring/Main.as b/tests/tests/swfs/avm2/xml_ctor_from_tostring/Main.as index f41f1ef58..4b31d47f8 100644 --- a/tests/tests/swfs/avm2/xml_ctor_from_tostring/Main.as +++ b/tests/tests/swfs/avm2/xml_ctor_from_tostring/Main.as @@ -8,6 +8,8 @@ public function Main() { + XML.prettyPrinting = false; + var byteArray: ByteArray = new ByteArray(); byteArray.writeUTFBytes("test"); byteArray.position = 0; @@ -20,6 +22,30 @@ objWithToString.toString = function() { return "test"; }; trace("// new XML(objWithToString).bar"); trace(new XML(objWithToString).bar); + + var xmlObj = ; + var xmlCopy = new XML(xmlObj); + trace("xmlCopy().toXMLString(): " + xmlCopy.toXMLString()); + trace("xmlObj === xmlCopy: " + (xmlObj === xmlCopy)); + + var emptyList = new XMLList(); + try { + new XML(emptyList); + } catch (e) { + trace("Caught error: " + e); + trace(e.errorID); + } + + var singleList = new XMLList("HelloWorld"); + trace("new XML(singleList): " + new XML(singleList)); + + var multiList = new XMLList("HelloWorld"); + try { + new XML(multiList); + } catch (e) { + trace("Caught error: " + e); + trace(e.errorID); + } } } diff --git a/tests/tests/swfs/avm2/xml_ctor_from_tostring/output.txt b/tests/tests/swfs/avm2/xml_ctor_from_tostring/output.txt index ed8b6b949..f70f028f2 100644 --- a/tests/tests/swfs/avm2/xml_ctor_from_tostring/output.txt +++ b/tests/tests/swfs/avm2/xml_ctor_from_tostring/output.txt @@ -3,3 +3,10 @@ test // new XML(objWithToString).bar test +xmlCopy().toXMLString(): +xmlObj === xmlCopy: false +Caught error: TypeError: Error #1088: The markup in the document following the root element must be well-formed. +1088 +new XML(singleList): HelloWorld +Caught error: TypeError: Error #1088: The markup in the document following the root element must be well-formed. +1088 diff --git a/tests/tests/swfs/avm2/xml_ctor_from_tostring/test.swf b/tests/tests/swfs/avm2/xml_ctor_from_tostring/test.swf index bedf393b2..e7b4f6980 100644 Binary files a/tests/tests/swfs/avm2/xml_ctor_from_tostring/test.swf and b/tests/tests/swfs/avm2/xml_ctor_from_tostring/test.swf differ