From b140ce6d97ed1bc79346e38f770e108f95f80ed6 Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Wed, 15 Mar 2023 16:19:30 -0500 Subject: [PATCH] avm2: Implement XML construction from XML and XMLList objects --- core/src/avm2/e4x.rs | 33 +++++++++++------ core/src/avm2/globals/xml.rs | 35 +++++++----------- .../swfs/avm2/xml_ctor_from_tostring/Main.as | 26 +++++++++++++ .../avm2/xml_ctor_from_tostring/output.txt | 7 ++++ .../swfs/avm2/xml_ctor_from_tostring/test.swf | Bin 1064 -> 1084 bytes 5 files changed, 69 insertions(+), 32 deletions(-) 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 bedf393b22e9e32b079739ae526295b30bfb0b57..e7b4f698032ae1b2a255656c6c11c3ad1ddca0bc 100644 GIT binary patch literal 1084 zcmV-C1jGA7S5ptI1^@tfoPAYWQ`<%q-qpn``HHz1Fi=e56flk@pzXAdq2Uq|m@u?M zGGsD%ux!>?scdxoSJUiGP>%%eQwe% z%WS7hb=zJqRSjpIQJ0l8zAI@Q{Q7nAm}72lbua4fv2os9Iey!;PHN0{PU?=`JYlxW z9J6)y=n*F>7;cEmtS-LVe;#i;l(Fu2U}xYG399HY({k0#E~6WcV{~H|j>+iD7dsqr zmA37g%(PoFbBr3D)U-^CUSuHILbV^USb#D`c-0(c{SNVa66C()i<=pw(J%ITI$RWI__fk&v$sxfSKl0&tns2}@_Udd*&QH-i3M^JMzdwN zy9Cx_)Fk)@e!WY5khU`Uh}<+M=8T>dBSrrsJ}HqCok0B23kr z$tUh~Aru^i^p2s@W}9_6Mfg89wb_CB^Ia(Dee`zk6L)$a8yHjae?v`u{4Ruody`Dz zlr$lxBC;qCk7H#dEv84)W9d|SJUx+~Old+)#v!7JYB&;&#S_y6i$stKRtXLhDN1mR z;5fkvHHpZ$>=A!iM=?*7S4wbLfA|2Z5A2COO}=V(1C)z;j&T|IlL$0y2_Ce zFk!wKqxdDdYzkfq?80Mggn@G$tW(@N(F+uA0@D;%#QgLozHEx#6qLZtBA73mk~a-; zGoVX55%i9G&j2vX9XR-l4}n^a?nQx9_U8O(8KM`xdqYbIUFpB+uTkzpPG5pU-G}Ho zDp=SL%$60tug5@{Q3P+EC(7U7ivo4XTL9mzpDqOH{D7SA@W6K=@kKfuh2~JG%)x{a2LKEQF}Lse^$|B*!&AQj@Lu( C{162I literal 1064 zcmV+@1lRjRS5ptR2mk?a$c*-IzMy}9mq{YCmcKRfJle~ie^m*J}aWUAACrve=U447b(b==w9fII*0L1?W zU=xD+>Lvi-kF}K`043MxXh-#iJnUJvr?IEK?JlK`rYM8KAU(*XiF>AGc6WCbHLGN^ zDTYXSBb%aO%J!abmt|j}jy>Hq9cmI==Fg~2`gCu5dm=mRIWt+?OCy7{aat!mWr!Rl zlU5b3!$4Zi#fTF3gb=G-;V8LbA+Ia{;*t*bJSLAUxprKTQ*yg+T85l|l_@DVnR8mW z&i>`Bn$4xuY${tk$rQ9)M#~p}P&1mU&T+@cF|~;)G7v?#$VE+M{Fn3O1?+@Q46`%( zEb=u{E-5#X{Jd-peHN|LcP)Rt3|+w%?qQpHEZ&SCuc2!l;`R_NBge5!9dSWrnDV+r z|9yZiaH_+jE-BZ*FSW#G)@Nyq~MVjQo#5K!wlBEtI zGEjPKG+Q)DHPwO>zm&-K=+9Osm*sumP}PEym}Sa7P%oUSj{j~(vg#bU8O zZV%h%Sf@3@4u8|e?$#pcz^0g)r7Y?(@X0+QPsd~Uf*(=ZK5zIqqIws*VM>mvYuaa9 zbMq$c^YxrZg3&M~T79&g1<=BJxLkS9!5Rd|r1j7=HNN`Dl%aIt2 zJc5$6AxWDOd@QXzk%Xs`@JtfEl7y`QxV{Ac4xtbc{XP^S3_uWIrO1+aslMTZFd7wi zfb2fq!Z8p