From 89c9753520bbca60cd13e97365222744f2ade1b7 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 19 Dec 2019 22:19:22 -0500 Subject: [PATCH 01/91] Add rudimentary custom DOM impl on top of `quick_xml`. `quick_xml` was chosen due to it's high performance and support for zero-copy use cases. However, we are not using `minidom`, which is the already-extant DOM impl that uses `quick_xml` as it's parsing provider. This is because `minidom` nodes are not amenable to garbage collection. Specifically: we want to be able to construct a new `Object` variant that holds part of an XML node. However, `minidom::Element` directly owns it's children, meaning that we can't hold references to it from within `Object` while also keeping those objects to the `'gc` lifetime. Hence, we provide a GC-exclusive DOM implementation. I ruled out solutions such as holding an entire XML tree in an `Rc` and having AVM objects that shadow them. This works for `SwfSlice` because indexing an array is cheap; but traversing a tree can get very expensive. XML is used in many places in Flash Player, so it's important that we treat it like a first-class citizen. --- Cargo.lock | 10 +++ core/Cargo.toml | 1 + core/src/lib.rs | 1 + core/src/xml.rs | 8 ++ core/src/xml/document.rs | 62 ++++++++++++++++ core/src/xml/tree.rs | 155 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+) create mode 100644 core/src/xml.rs create mode 100644 core/src/xml/document.rs create mode 100644 core/src/xml/tree.rs diff --git a/Cargo.lock b/Cargo.lock index a743d9aac..5f6159718 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1432,6 +1432,14 @@ name = "quick-error" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "quick-xml" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quote" version = "0.6.13" @@ -1575,6 +1583,7 @@ dependencies = [ "minimp3 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "num_enum 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "puremp3 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "ruffle_macros 0.1.0", "smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2563,6 +2572,7 @@ dependencies = [ "checksum proc-macro2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0319972dcae462681daf4da1adeeaa066e3ebd29c69be96c6abb1259d2ee2bcc" "checksum puremp3 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f2b7efbb39e373af70c139e0611375fa6cad751fb93d528a610b55302710d883" "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" +"checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" diff --git a/core/Cargo.toml b/core/Cargo.toml index e6bbdf08e..8405a263a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -19,6 +19,7 @@ swf = { path = "../swf" } enumset = "0.4.2" smallvec = "1.1.0" num_enum = "0.4.2" +quick-xml = "0.17.2" [dependencies.jpeg-decoder] version = "0.1.18" diff --git a/core/src/lib.rs b/core/src/lib.rs index 54da5e57b..b7822b5de 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,6 +20,7 @@ mod prelude; pub mod shape_utils; pub mod tag_utils; mod transform; +mod xml; pub mod backend; diff --git a/core/src/xml.rs b/core/src/xml.rs new file mode 100644 index 000000000..9693dd312 --- /dev/null +++ b/core/src/xml.rs @@ -0,0 +1,8 @@ +//! Garbage-collectable XML DOM impl + +mod document; +mod tree; + +type Error = Box; + +pub use tree::XMLNode; diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs new file mode 100644 index 000000000..e14271ec3 --- /dev/null +++ b/core/src/xml/document.rs @@ -0,0 +1,62 @@ +//! XML Document + +use crate::xml::Error; +use crate::xml::XMLNode; +use gc_arena::{Collect, MutationContext}; +use quick_xml::events::Event; +use quick_xml::Reader; + +/// The entirety of an XML document. +#[derive(Clone, Collect)] +#[collect(no_drop)] +pub struct XMLDocument<'gc> { + /// The root node(s) of the XML document. + roots: Vec>, +} + +impl<'gc> XMLDocument<'gc> { + pub fn from_str(mc: MutationContext<'gc, '_>, data: &str) -> Result { + let mut parser = Reader::from_str(data); + let mut buf = Vec::new(); + let mut roots = Vec::new(); + let mut open_tags: Vec> = Vec::new(); + + loop { + match parser.read_event(&mut buf)? { + Event::Start(bs) => { + let child = XMLNode::from_start_event(mc, bs)?; + if let Some(node) = open_tags.last_mut() { + node.append_child(mc, child)?; + } else { + roots.push(child); + } + + open_tags.push(child); + } + Event::Empty(bs) => { + let child = XMLNode::from_start_event(mc, bs)?; + if let Some(node) = open_tags.last_mut() { + node.append_child(mc, child)?; + } else { + roots.push(child); + } + } + Event::End(_) => { + open_tags.pop(); + } + Event::Text(bt) => { + let child = XMLNode::text_from_text_event(mc, bt)?; + if let Some(node) = open_tags.last_mut() { + node.append_child(mc, child)?; + } else { + roots.push(child); + } + } + Event::Eof => break, + _ => {} + } + } + + Ok(Self { roots }) + } +} diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs new file mode 100644 index 000000000..371d2dced --- /dev/null +++ b/core/src/xml/tree.rs @@ -0,0 +1,155 @@ +//! XML Tree structure + +use crate::xml::Error; +use gc_arena::{Collect, GcCell, MutationContext}; +use quick_xml::events::attributes::Attribute; +use quick_xml::events::{BytesStart, BytesText}; +use std::borrow::Cow; +use std::collections::BTreeMap; + +/// Represents a scoped name within XML. +/// +/// All names in XML are optionally namespaced. Each namespace is represented +/// as a string; the document contains a mapping of namespaces to URIs. +/// +/// The special namespace `xmlns` is used to map namespace strings to URIs; it +/// should not be used for user-specified namespaces. +#[derive(Clone, Collect, PartialEq, Eq, PartialOrd, Ord)] +#[collect(no_drop)] +pub struct XMLName { + /// The name of the XML namespace this name is scoped to. + /// + /// Names without a namespace use the default namespace. + /// + /// Namespaces may be resolved to a URI by consulting the encapsulating + /// document. + namespace: Option, + name: String, +} + +impl XMLName { + pub fn from_bytes(bytes: &[u8]) -> Result { + Self::from_bytes_cow(Cow::Borrowed(bytes)) + } + + pub fn from_bytes_cow(bytes: Cow<[u8]>) -> Result { + let full_name = match bytes { + Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), + Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), + }; + + if let Some(colon_index) = full_name.find(':') { + Ok(Self { + namespace: Some(full_name[0..colon_index].to_owned()), + name: full_name[colon_index + 1..].to_owned(), + }) + } else { + Ok(Self { + namespace: None, + name: full_name.into_owned(), + }) + } + } +} + +/// Represents a node in the XML tree. +#[derive(Copy, Clone, Collect)] +#[collect(no_drop)] +pub struct XMLNode<'gc>(GcCell<'gc, XMLNodeData<'gc>>); + +#[derive(Clone, Collect)] +#[collect(no_drop)] +pub enum XMLNodeData<'gc> { + /// A text node in the XML tree. + Text { + /// The string representation of the text. + contents: String, + }, + + /// A comment node in the XML tree. + Comment { + /// The string representation of the comment. + contents: String, + }, + + /// An element node in the XML tree. + /// + /// Element nodes are non-leaf nodes: they can store additional data as + /// either attributes (for key/value pairs) or child nodes (for more + /// structured data). + Element { + /// The tag name of this element. + tag_name: XMLName, + + /// Attributes of the element. + attributes: BTreeMap, + + /// Child nodes of this element. + children: Vec>, + }, +} + +impl<'gc> XMLNode<'gc> { + /// Construct an XML 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<'a>( + mc: MutationContext<'gc, '_>, + bs: BytesStart<'a>, + ) -> Result { + let tag_name = XMLName::from_bytes_cow(bs.unescaped()?)?; + let mut attributes = BTreeMap::new(); + + for a in bs.attributes() { + let attribute = a?; + attributes.insert( + XMLName::from_bytes(attribute.key)?, + String::from_utf8(attribute.value.to_owned().to_vec())?, + ); + } + + let children = Vec::new(); + + Ok(XMLNode(GcCell::allocate( + mc, + XMLNodeData::Element { + tag_name, + attributes, + children, + }, + ))) + } + + pub fn text_from_text_event<'a>( + mc: MutationContext<'gc, '_>, + bt: BytesText<'a>, + ) -> Result { + Ok(XMLNode(GcCell::allocate( + mc, + XMLNodeData::Text { + contents: match bt.unescaped()? { + Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), + Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), + } + .to_owned() + .to_string(), + }, + ))) + } + + pub fn append_child( + &mut self, + mc: MutationContext<'gc, '_>, + child: XMLNode<'gc>, + ) -> Result<(), Error> { + match &mut *self.0.write(mc) { + XMLNodeData::Element { + ref mut children, .. + } => children.push(child), + _ => return Err("Not an Element".into()), + }; + + Ok(()) + } +} From 6a65e984aea839297553293949a484a0ee180815 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 00:17:05 -0500 Subject: [PATCH 02/91] Add a new `XMLObject` variant to the AVM1 object ecosystem. This particular variant is actually a two-in-one deal: `XMLObject`s may either refer to a document or a node. --- core/src/avm1.rs | 1 + core/src/avm1/object.rs | 21 ++- core/src/avm1/xml_object.rs | 247 ++++++++++++++++++++++++++++++++++++ core/src/xml.rs | 1 + core/src/xml/document.rs | 14 +- core/src/xml/tree.rs | 11 +- 6 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 core/src/avm1/xml_object.rs diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 082bf8d2c..ed6243de7 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -33,6 +33,7 @@ mod sound_object; mod stage_object; mod super_object; mod value; +mod xml_object; #[cfg(test)] mod tests; diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index 8b8b256f7..b30cced26 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -4,8 +4,10 @@ use crate::avm1::function::Executable; use crate::avm1::property::Attribute; use crate::avm1::return_value::ReturnValue; use crate::avm1::super_object::SuperObject; +use crate::avm1::xml_object::XMLObject; use crate::avm1::{Avm1, Error, ScriptObject, SoundObject, StageObject, UpdateContext, Value}; use crate::display_object::DisplayObject; +use crate::xml::{XMLDocument, XMLNode}; use enumset::EnumSet; use gc_arena::{Collect, MutationContext}; use ruffle_macros::enum_trait_object; @@ -22,6 +24,7 @@ use std::fmt::Debug; SoundObject(SoundObject<'gc>), StageObject(StageObject<'gc>), SuperObject(SuperObject<'gc>), + XMLObject(XMLObject<'gc>), } )] pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy { @@ -258,10 +261,24 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy } /// Get the underlying display node for this object, if it exists. - fn as_display_object(&self) -> Option>; + fn as_display_object(&self) -> Option> { + None + } /// Get the underlying executable for this object, if it exists. - fn as_executable(&self) -> Option>; + fn as_executable(&self) -> Option> { + None + } + + /// Get the underlying XML document for this object, if it exists. + fn as_xml_document(&self) -> Option> { + None + } + + /// Get the underlying XML node for this object, if it exists. + fn as_xml_node(&self) -> Option> { + None + } fn as_ptr(&self) -> *const ObjectPtr; diff --git a/core/src/avm1/xml_object.rs b/core/src/avm1/xml_object.rs new file mode 100644 index 000000000..fa4bfae00 --- /dev/null +++ b/core/src/avm1/xml_object.rs @@ -0,0 +1,247 @@ +//! AVM1 object type to represent XML nodes + +use crate::avm1::function::Executable; +use crate::avm1::object::{ObjectPtr, TObject}; +use crate::avm1::property::Attribute; +use crate::avm1::return_value::ReturnValue; +use crate::avm1::{Avm1, Error, Object, ScriptObject, UpdateContext, Value}; +use crate::xml::{XMLDocument, XMLNode}; +use enumset::EnumSet; +use gc_arena::{Collect, MutationContext}; +use std::collections::HashSet; +use std::fmt; + +/// A ScriptObject that is inherently tied to an XML node. +#[derive(Clone, Copy, Collect)] +#[collect(no_drop)] +pub enum XMLObject<'gc> { + /// An `XMLObject` that references a whole document. + Document(ScriptObject<'gc>, XMLDocument<'gc>), + + /// An `XMLObject` that references a specific node of another document. + Node(ScriptObject<'gc>, XMLNode<'gc>), +} + +impl<'gc> XMLObject<'gc> { + fn empty_document( + gc_context: MutationContext<'gc, '_>, + proto: Option>, + ) -> XMLObject<'gc> { + XMLObject::Document( + ScriptObject::object(gc_context, proto), + XMLDocument::new(gc_context), + ) + } + + fn empty_node( + gc_context: MutationContext<'gc, '_>, + proto: Option>, + ) -> XMLObject<'gc> { + XMLObject::Node( + ScriptObject::object(gc_context, proto), + XMLNode::new_text(gc_context, ""), + ) + } + + fn base(&self) -> ScriptObject<'gc> { + match self { + XMLObject::Document(base, ..) => *base, + XMLObject::Node(base, ..) => *base, + } + } +} + +impl fmt::Debug for XMLObject<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + XMLObject::Document(base, ..) => { + f.debug_tuple("XMLObject::Document") + .field(base) + //.field(document) + .finish() + } + XMLObject::Node(base, ..) => { + f.debug_tuple("XMLObject::Node") + .field(base) + //.field(document) + .finish() + } + } + } +} + +impl<'gc> TObject<'gc> for XMLObject<'gc> { + fn get_local( + &self, + name: &str, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + ) -> Result, Error> { + self.base().get_local(name, avm, context, this) + } + + fn set( + &self, + name: &str, + value: Value<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + self.base().set(name, value, avm, context) + } + + fn call( + &self, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], + ) -> Result, Error> { + self.base().call(avm, context, this, args) + } + + fn new( + &self, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], + ) -> Result, Error> { + match self { + XMLObject::Document(..) => { + Ok(XMLObject::empty_document(context.gc_context, Some(this)).into()) + } + XMLObject::Node(..) => Ok(XMLObject::empty_node(context.gc_context, Some(this)).into()), + } + } + + fn delete(&self, gc_context: MutationContext<'gc, '_>, name: &str) -> bool { + self.base().delete(gc_context, name) + } + + fn add_property( + &self, + gc_context: MutationContext<'gc, '_>, + name: &str, + get: Executable<'gc>, + set: Option>, + attributes: EnumSet, + ) { + self.base() + .add_property(gc_context, name, get, set, attributes) + } + + fn define_value( + &self, + gc_context: MutationContext<'gc, '_>, + name: &str, + value: Value<'gc>, + attributes: EnumSet, + ) { + self.base() + .define_value(gc_context, name, value, attributes) + } + + fn set_attributes( + &mut self, + gc_context: MutationContext<'gc, '_>, + name: Option<&str>, + set_attributes: EnumSet, + clear_attributes: EnumSet, + ) { + self.base() + .set_attributes(gc_context, name, set_attributes, clear_attributes) + } + + fn proto(&self) -> Option> { + self.base().proto() + } + + fn has_property(&self, name: &str) -> bool { + self.base().has_property(name) + } + + fn has_own_property(&self, name: &str) -> bool { + self.base().has_own_property(name) + } + + fn is_property_overwritable(&self, name: &str) -> bool { + self.base().is_property_overwritable(name) + } + + fn is_property_enumerable(&self, name: &str) -> bool { + self.base().is_property_enumerable(name) + } + + fn get_keys(&self) -> HashSet { + self.base().get_keys() + } + + fn as_string(&self) -> String { + self.base().as_string() + } + + fn type_of(&self) -> &'static str { + self.base().type_of() + } + + fn interfaces(&self) -> Vec> { + self.base().interfaces() + } + + fn set_interfaces(&mut self, context: MutationContext<'gc, '_>, iface_list: Vec>) { + self.base().set_interfaces(context, iface_list) + } + + fn as_script_object(&self) -> Option> { + Some(self.base()) + } + + fn as_xml_document(&self) -> Option> { + match self { + XMLObject::Document(_base, document) => Some(*document), + _ => None, + } + } + + fn as_xml_node(&self) -> Option> { + match self { + XMLObject::Node(_base, node) => Some(*node), + _ => None, + } + } + + fn as_ptr(&self) -> *const ObjectPtr { + self.base().as_ptr() as *const ObjectPtr + } + + fn length(&self) -> usize { + self.base().length() + } + + fn array(&self) -> Vec> { + self.base().array() + } + + fn set_length(&self, gc_context: MutationContext<'gc, '_>, length: usize) { + self.base().set_length(gc_context, length) + } + + fn array_element(&self, index: usize) -> Value<'gc> { + self.base().array_element(index) + } + + fn set_array_element( + &self, + index: usize, + value: Value<'gc>, + gc_context: MutationContext<'gc, '_>, + ) -> usize { + self.base().set_array_element(index, value, gc_context) + } + + fn delete_array_element(&self, index: usize, gc_context: MutationContext<'gc, '_>) { + self.base().delete_array_element(index, gc_context) + } +} diff --git a/core/src/xml.rs b/core/src/xml.rs index 9693dd312..5c4981a64 100644 --- a/core/src/xml.rs +++ b/core/src/xml.rs @@ -5,4 +5,5 @@ mod tree; type Error = Box; +pub use document::XMLDocument; pub use tree::XMLNode; diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index e14271ec3..b53cf5652 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -2,19 +2,27 @@ use crate::xml::Error; use crate::xml::XMLNode; -use gc_arena::{Collect, MutationContext}; +use gc_arena::{Collect, GcCell, MutationContext}; use quick_xml::events::Event; use quick_xml::Reader; /// The entirety of an XML document. +#[derive(Copy, Clone, Collect)] +#[collect(no_drop)] +pub struct XMLDocument<'gc>(GcCell<'gc, XMLDocumentData<'gc>>); + #[derive(Clone, Collect)] #[collect(no_drop)] -pub struct XMLDocument<'gc> { +pub struct XMLDocumentData<'gc> { /// The root node(s) of the XML document. roots: Vec>, } impl<'gc> XMLDocument<'gc> { + pub fn new(mc: MutationContext<'gc, '_>) -> Self { + Self(GcCell::allocate(mc, XMLDocumentData { roots: Vec::new() })) + } + pub fn from_str(mc: MutationContext<'gc, '_>, data: &str) -> Result { let mut parser = Reader::from_str(data); let mut buf = Vec::new(); @@ -57,6 +65,6 @@ impl<'gc> XMLDocument<'gc> { } } - Ok(Self { roots }) + Ok(Self(GcCell::allocate(mc, XMLDocumentData { roots }))) } } diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 371d2dced..6792ce931 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -2,7 +2,6 @@ use crate::xml::Error; use gc_arena::{Collect, GcCell, MutationContext}; -use quick_xml::events::attributes::Attribute; use quick_xml::events::{BytesStart, BytesText}; use std::borrow::Cow; use std::collections::BTreeMap; @@ -90,6 +89,16 @@ pub enum XMLNodeData<'gc> { } impl<'gc> XMLNode<'gc> { + /// Construct a new XML text node. + pub fn new_text(mc: MutationContext<'gc, '_>, contents: &str) -> Self { + XMLNode(GcCell::allocate( + mc, + XMLNodeData::Text { + contents: contents.to_string(), + }, + )) + } + /// Construct an XML node from a `quick_xml` `BytesStart` event. /// /// The returned node will always be an `Element`, and it must only contain From 554f0dc1e54ee2c42bdcc05e1a250cc5601f3f39 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 15:28:49 -0500 Subject: [PATCH 03/91] Add XMLNode class and constructor impl --- core/src/avm1/globals.rs | 10 ++++++++ core/src/avm1/globals/xml.rs | 46 ++++++++++++++++++++++++++++++++++++ core/src/avm1/xml_object.rs | 1 + core/src/xml/tree.rs | 20 ++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 core/src/avm1/globals/xml.rs diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index fd3f0a56a..f6ac046b4 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -20,6 +20,7 @@ mod object; mod sound; mod stage; pub(crate) mod text_field; +mod xml; #[allow(non_snake_case, unused_must_use)] //can't use errors yet pub fn getURL<'a, 'gc>( @@ -172,6 +173,8 @@ pub fn create_globals<'gc>( let array_proto: Object<'gc> = array::create_proto(gc_context, object_proto, function_proto); let color_proto: Object<'gc> = color::create_proto(gc_context, object_proto, function_proto); + let xmlnode_proto: Object<'gc> = + xml::create_xmlnode_proto(gc_context, object_proto, function_proto); //TODO: These need to be constructors and should also set `.prototype` on each one let object = ScriptObject::function( @@ -217,6 +220,12 @@ pub fn create_globals<'gc>( Some(function_proto), Some(array_proto), ); + let xmlnode = ScriptObject::function( + gc_context, + Executable::Native(xml::xmlnode_constructor), + Some(function_proto), + Some(xmlnode_proto), + ); let listeners = SystemListeners::new(gc_context, Some(array_proto)); @@ -228,6 +237,7 @@ pub fn create_globals<'gc>( globals.define_value(gc_context, "MovieClip", movie_clip.into(), EnumSet::empty()); globals.define_value(gc_context, "Sound", sound.into(), EnumSet::empty()); globals.define_value(gc_context, "TextField", text_field.into(), EnumSet::empty()); + globals.define_value(gc_context, "XMLNode", xmlnode.into(), EnumSet::empty()); globals.force_set_function( "Number", number, diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs new file mode 100644 index 000000000..4f7a4d6c6 --- /dev/null +++ b/core/src/avm1/globals/xml.rs @@ -0,0 +1,46 @@ +//! XML/XMLNode global classes + +use crate::avm1::return_value::ReturnValue; +use crate::avm1::script_object::ScriptObject; +use crate::avm1::{Avm1, Error, Object, TObject, UpdateContext, Value}; +use crate::xml::XMLNode; +use gc_arena::MutationContext; +use std::mem::swap; + +/// XMLNode constructor +pub fn xmlnode_constructor<'gc>( + avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + match ( + args.get(0).map(|v| v.as_number(avm, ac).map(|v| v as u32)), + args.get(1).map(|v| v.as_string()), + this.as_xml_node(), + ) { + (Some(Ok(1)), Some(Ok(strval)), Some(ref mut this_node)) => { + let mut xmlelement = XMLNode::new_text(ac.gc_context, strval); + swap(&mut xmlelement, this_node); + } + (Some(Ok(3)), Some(Ok(strval)), Some(ref mut this_node)) => { + let mut xmlelement = XMLNode::new_element(ac.gc_context, strval)?; + swap(&mut xmlelement, this_node); + } + //Invalid nodetype ID, string value missing, or not an XMLElement + _ => {} + }; + + Ok(Value::Undefined.into()) +} + +/// Construct the prototype for `XMLNode`. +pub fn create_xmlnode_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + let xmlnode_proto = ScriptObject::object(gc_context, Some(proto)); + + xmlnode_proto.into() +} diff --git a/core/src/avm1/xml_object.rs b/core/src/avm1/xml_object.rs index fa4bfae00..f8f593082 100644 --- a/core/src/avm1/xml_object.rs +++ b/core/src/avm1/xml_object.rs @@ -101,6 +101,7 @@ impl<'gc> TObject<'gc> for XMLObject<'gc> { self.base().call(avm, context, this, args) } + #[allow(clippy::new_ret_no_self)] fn new( &self, _avm: &mut Avm1<'gc>, diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 6792ce931..df0127128 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -31,12 +31,20 @@ impl XMLName { Self::from_bytes_cow(Cow::Borrowed(bytes)) } + pub fn from_str(strval: &str) -> Result { + Self::from_str_cow(Cow::Borrowed(strval)) + } + pub fn from_bytes_cow(bytes: Cow<[u8]>) -> Result { let full_name = match bytes { Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), }; + Self::from_str_cow(full_name) + } + + pub fn from_str_cow(full_name: Cow) -> Result { if let Some(colon_index) = full_name.find(':') { Ok(Self { namespace: Some(full_name[0..colon_index].to_owned()), @@ -99,6 +107,18 @@ impl<'gc> XMLNode<'gc> { )) } + /// Construct a new XML element node. + pub fn new_element(mc: MutationContext<'gc, '_>, element_name: &str) -> Result { + Ok(XMLNode(GcCell::allocate( + mc, + XMLNodeData::Element { + tag_name: XMLName::from_str(element_name)?, + attributes: BTreeMap::new(), + children: Vec::new(), + }, + ))) + } + /// Construct an XML node from a `quick_xml` `BytesStart` event. /// /// The returned node will always be an `Element`, and it must only contain From 4f5ac09b73b1d5bb0c79d12a0e473d6612414ed9 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 15:40:50 -0500 Subject: [PATCH 04/91] Expose XML document constructor, including text parsing ability --- core/src/avm1/globals.rs | 9 +++++++ core/src/avm1/globals/xml.rs | 47 +++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index f6ac046b4..d88ca3b18 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -176,6 +176,8 @@ pub fn create_globals<'gc>( let xmlnode_proto: Object<'gc> = xml::create_xmlnode_proto(gc_context, object_proto, function_proto); + let xml_proto: Object<'gc> = xml::create_xml_proto(gc_context, xmlnode_proto, function_proto); + //TODO: These need to be constructors and should also set `.prototype` on each one let object = ScriptObject::function( gc_context, @@ -226,6 +228,12 @@ pub fn create_globals<'gc>( Some(function_proto), Some(xmlnode_proto), ); + let xml = ScriptObject::function( + gc_context, + Executable::Native(xml::xml_constructor), + Some(function_proto), + Some(xml_proto), + ); let listeners = SystemListeners::new(gc_context, Some(array_proto)); @@ -238,6 +246,7 @@ pub fn create_globals<'gc>( globals.define_value(gc_context, "Sound", sound.into(), EnumSet::empty()); globals.define_value(gc_context, "TextField", text_field.into(), EnumSet::empty()); globals.define_value(gc_context, "XMLNode", xmlnode.into(), EnumSet::empty()); + globals.define_value(gc_context, "XML", xml.into(), EnumSet::empty()); globals.force_set_function( "Number", number, diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 4f7a4d6c6..ee41fead9 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -3,7 +3,7 @@ use crate::avm1::return_value::ReturnValue; use crate::avm1::script_object::ScriptObject; use crate::avm1::{Avm1, Error, Object, TObject, UpdateContext, Value}; -use crate::xml::XMLNode; +use crate::xml::{XMLDocument, XMLNode}; use gc_arena::MutationContext; use std::mem::swap; @@ -16,14 +16,14 @@ pub fn xmlnode_constructor<'gc>( ) -> Result, Error> { match ( args.get(0).map(|v| v.as_number(avm, ac).map(|v| v as u32)), - args.get(1).map(|v| v.as_string()), + args.get(1).map(|v| v.clone().coerce_to_string(avm, ac)), this.as_xml_node(), ) { - (Some(Ok(1)), Some(Ok(strval)), Some(ref mut this_node)) => { + (Some(Ok(1)), Some(Ok(ref strval)), Some(ref mut this_node)) => { let mut xmlelement = XMLNode::new_text(ac.gc_context, strval); swap(&mut xmlelement, this_node); } - (Some(Ok(3)), Some(Ok(strval)), Some(ref mut this_node)) => { + (Some(Ok(3)), Some(Ok(ref strval)), Some(ref mut this_node)) => { let mut xmlelement = XMLNode::new_element(ac.gc_context, strval)?; swap(&mut xmlelement, this_node); } @@ -44,3 +44,42 @@ pub fn create_xmlnode_proto<'gc>( xmlnode_proto.into() } + +/// XML (document) constructor +pub fn xml_constructor<'gc>( + avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + match ( + args.get(0).map(|v| v.clone().coerce_to_string(avm, ac)), + this.as_xml_document(), + ) { + (Some(Ok(ref string)), Some(ref mut this_doc)) => { + let mut xmldoc = XMLDocument::from_str(ac.gc_context, string)?; + + swap(&mut xmldoc, this_doc); + } + (None, Some(ref mut this_doc)) => { + let mut emptydoc = XMLDocument::new(ac.gc_context); + + swap(&mut emptydoc, this_doc); + } + //Non-string argument or not an XML document + _ => {} + }; + + Ok(Value::Undefined.into()) +} + +/// Construct the prototype for `XML`. +pub fn create_xml_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + let xml_proto = ScriptObject::object(gc_context, Some(proto)); + + xml_proto.into() +} From 6ae5ae3038c8f9501813603484dadb8a3e2f0077 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 15:48:24 -0500 Subject: [PATCH 05/91] Add Comment parsing support --- core/src/xml/document.rs | 8 ++++++++ core/src/xml/tree.rs | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index b53cf5652..3ea7298cf 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -60,6 +60,14 @@ impl<'gc> XMLDocument<'gc> { roots.push(child); } } + Event::Comment(bt) => { + let child = XMLNode::comment_from_text_event(mc, bt)?; + if let Some(node) = open_tags.last_mut() { + node.append_child(mc, child)?; + } else { + roots.push(child); + } + } Event::Eof => break, _ => {} } diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index df0127128..37b553c8f 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -167,6 +167,23 @@ impl<'gc> XMLNode<'gc> { ))) } + pub fn comment_from_text_event<'a>( + mc: MutationContext<'gc, '_>, + bt: BytesText<'a>, + ) -> Result { + Ok(XMLNode(GcCell::allocate( + mc, + XMLNodeData::Comment { + contents: match bt.unescaped()? { + Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), + Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), + } + .to_owned() + .to_string(), + }, + ))) + } + pub fn append_child( &mut self, mc: MutationContext<'gc, '_>, From e2eb3d0bdef0a7c7fa98b883f4a357da90936a98 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 21:18:31 -0500 Subject: [PATCH 06/91] Add a test of XMLDocument::from_str. This test technically works, but the results were slightly surprising. If it turns out Flash works differently from `quick_xml`, we'll change this test to match Flash. --- core/src/xml.rs | 8 +++ core/src/xml/document.rs | 42 ++++++++++++++ core/src/xml/tests.rs | 36 ++++++++++++ core/src/xml/tree.rs | 116 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 core/src/xml/tests.rs diff --git a/core/src/xml.rs b/core/src/xml.rs index 5c4981a64..dd510fd72 100644 --- a/core/src/xml.rs +++ b/core/src/xml.rs @@ -3,7 +3,15 @@ mod document; mod tree; +#[cfg(test)] +mod tests; + type Error = Box; pub use document::XMLDocument; +pub use tree::XMLName; pub use tree::XMLNode; + +pub const ELEMENT_NODE: u8 = 1; +pub const TEXT_NODE: u8 = 3; +pub const COMMENT_NODE: u8 = 8; diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index 3ea7298cf..d79ad4b36 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -5,6 +5,7 @@ use crate::xml::XMLNode; use gc_arena::{Collect, GcCell, MutationContext}; use quick_xml::events::Event; use quick_xml::Reader; +use std::fmt; /// The entirety of an XML document. #[derive(Copy, Clone, Collect)] @@ -75,4 +76,45 @@ impl<'gc> XMLDocument<'gc> { Ok(Self(GcCell::allocate(mc, XMLDocumentData { roots }))) } + + /// Returns an iterator that yields the document's root nodes. + pub fn roots(&self) -> impl Iterator> { + struct RootsIter<'gc> { + base: XMLDocument<'gc>, + index: usize, + }; + + impl<'gc> RootsIter<'gc> { + fn for_document(base: XMLDocument<'gc>) -> Self { + Self { base, index: 0 } + } + } + + impl<'gc> Iterator for RootsIter<'gc> { + type Item = XMLNode<'gc>; + + fn next(&mut self) -> Option { + let (len, item) = { + let r = self.base.0.read(); + (r.roots.len(), r.roots.get(self.index).cloned()) + }; + + if self.index < len { + self.index += 1; + } + + item + } + } + + RootsIter::for_document(*self) + } +} + +impl<'gc> fmt::Debug for XMLDocument<'gc> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("XMLDocument") + .field("roots", &self.0.read().roots) + .finish() + } } diff --git a/core/src/xml/tests.rs b/core/src/xml/tests.rs new file mode 100644 index 000000000..ad2ccbcc1 --- /dev/null +++ b/core/src/xml/tests.rs @@ -0,0 +1,36 @@ +//! XML tests + +use crate::xml; +use crate::xml::{XMLDocument, XMLName}; +use gc_arena::rootless_arena; + +#[test] +fn parse_single_element() { + rootless_arena(|mc| { + let xml = XMLDocument::from_str(mc, "").expect("Parsed document"); + dbg!(xml); + let mut roots = xml.roots(); + + let root = roots.next().expect("Parsed document should have a root"); + assert_eq!(root.node_type(), xml::TEXT_NODE); + assert_eq!(root.node_value(), Some("".to_string())); + + let root = roots + .next() + .expect("Parsed document should have a second root"); + assert_eq!(root.node_type(), xml::ELEMENT_NODE); + assert_eq!(root.tag_name(), Some(XMLName::from_str("test").unwrap())); + + let mut root_children = root.children().unwrap(); + + let child_text = root_children + .next() + .expect("Single nodes should have an empty text child"); + assert_eq!(child_text.node_type(), xml::TEXT_NODE); + assert_eq!(child_text.node_value(), Some("".to_string())); + + assert!(root_children.next().is_none()); + + assert!(roots.next().is_none()); + }) +} diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 37b553c8f..43d734a14 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -1,10 +1,12 @@ //! XML Tree structure +use crate::xml; use crate::xml::Error; use gc_arena::{Collect, GcCell, MutationContext}; use quick_xml::events::{BytesStart, BytesText}; use std::borrow::Cow; use std::collections::BTreeMap; +use std::fmt; /// Represents a scoped name within XML. /// @@ -59,6 +61,15 @@ impl XMLName { } } +impl fmt::Debug for XMLName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("XMLName") + .field("namespace", &self.namespace) + .field("name", &self.name) + .finish() + } +} + /// Represents a node in the XML tree. #[derive(Copy, Clone, Collect)] #[collect(no_drop)] @@ -119,7 +130,7 @@ impl<'gc> XMLNode<'gc> { ))) } - /// Construct an XML node from a `quick_xml` `BytesStart` event. + /// 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.) @@ -150,6 +161,10 @@ impl<'gc> XMLNode<'gc> { ))) } + /// Construct an XML Text node from a `quick_xml` `BytesText` event. + /// + /// The returned node will always be `Text`, and it must only contain + /// valid encoded UTF-8 data. (Other encoding support is planned later.) pub fn text_from_text_event<'a>( mc: MutationContext<'gc, '_>, bt: BytesText<'a>, @@ -167,6 +182,10 @@ impl<'gc> XMLNode<'gc> { ))) } + /// Construct an XML Comment node from a `quick_xml` `BytesText` event. + /// + /// The returned node will always be `Comment`, and it must only contain + /// valid encoded UTF-8 data. (Other encoding support is planned later.) pub fn comment_from_text_event<'a>( mc: MutationContext<'gc, '_>, bt: BytesText<'a>, @@ -184,6 +203,10 @@ impl<'gc> XMLNode<'gc> { ))) } + /// Append a child element to an Element node. + /// + /// This function yields an error if appending to a Node that cannot accept + /// children. In that case, no modification will be made to the node. pub fn append_child( &mut self, mc: MutationContext<'gc, '_>, @@ -198,4 +221,95 @@ impl<'gc> XMLNode<'gc> { Ok(()) } + + /// Returns the type of this node as an integer. + /// + /// This is primarily intended to match W3C DOM L1 specifications and + /// should not be used in lieu of a proper `match` statement. + pub fn node_type(&self) -> u8 { + match &*self.0.read() { + XMLNodeData::Element { .. } => xml::ELEMENT_NODE, + XMLNodeData::Text { .. } => xml::TEXT_NODE, + XMLNodeData::Comment { .. } => xml::COMMENT_NODE, + } + } + + /// Returns the tagname, if the element has one. + pub fn tag_name(&self) -> Option { + match &*self.0.read() { + XMLNodeData::Element { ref tag_name, .. } => Some(tag_name.clone()), + _ => None, + } + } + + /// Returns the string contents of the node, if the element has them. + pub fn node_value(&self) -> Option { + match &*self.0.read() { + XMLNodeData::Text { ref contents } => Some(contents.clone()), + XMLNodeData::Comment { ref contents } => Some(contents.clone()), + _ => None, + } + } + + /// Returns an iterator that yields child nodes. + /// + /// Yields None if this node cannot accept children. + pub fn children(&self) -> Option>> { + struct ChildIter<'gc> { + base: XMLNode<'gc>, + index: usize, + }; + + impl<'gc> ChildIter<'gc> { + fn for_node(base: XMLNode<'gc>) -> Self { + Self { base, index: 0 } + } + } + + impl<'gc> Iterator for ChildIter<'gc> { + type Item = XMLNode<'gc>; + + fn next(&mut self) -> Option { + match &*self.base.0.read() { + XMLNodeData::Element { ref children, .. } if self.index < children.len() => { + let item = children.get(self.index).cloned(); + self.index += 1; + + item + } + _ => None, + } + } + } + + match &*self.0.read() { + XMLNodeData::Element { .. } => Some(ChildIter::for_node(*self)), + _ => return None, + } + } +} + +impl<'gc> fmt::Debug for XMLNode<'gc> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &*self.0.read() { + XMLNodeData::Text { ref contents } => f + .debug_struct("XMLNodeData::Text") + .field("contents", contents) + .finish(), + XMLNodeData::Comment { ref contents } => f + .debug_struct("XMLNodeData::Comment") + .field("contents", contents) + .finish(), + XMLNodeData::Element { + ref tag_name, + ref attributes, + ref children, + } => f + .debug_struct("XMLNodeData::Element") + .field("tag_name", tag_name) + .field("attributes", attributes) + .field("children", children) + .finish(), + } + } } From bd1ea56cc3c770eebfcc98f4b6b9221b34e31afe Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 22:37:43 -0500 Subject: [PATCH 07/91] Implement `XMLNode` properties that don't require child or attribute iteration. --- core/src/avm1/globals/xml.rs | 71 +++++++++++++++++++++++++++++++++++- core/src/avm1/xml_object.rs | 4 +- core/src/xml/tree.rs | 21 +++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index ee41fead9..5ff37ade9 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -1,7 +1,10 @@ //! XML/XMLNode global classes +use crate::avm1::function::Executable; +use crate::avm1::property::Attribute::*; use crate::avm1::return_value::ReturnValue; use crate::avm1::script_object::ScriptObject; +use crate::avm1::xml_object::XMLObject; use crate::avm1::{Avm1, Error, Object, TObject, UpdateContext, Value}; use crate::xml::{XMLDocument, XMLNode}; use gc_arena::MutationContext; @@ -40,7 +43,73 @@ pub fn create_xmlnode_proto<'gc>( proto: Object<'gc>, _fn_proto: Object<'gc>, ) -> Object<'gc> { - let xmlnode_proto = ScriptObject::object(gc_context, Some(proto)); + let xmlnode_proto = XMLObject::empty_node(gc_context, Some(proto)); + + xmlnode_proto.add_property( + gc_context, + "localName", + Executable::Native(|_avm, _ac, this: Object<'gc>, _args| { + Ok(this + .as_xml_node() + .and_then(|n| n.tag_name()) + .map(|n| n.local_name().to_string().into()) + .unwrap_or(Value::Undefined.into())) + }), + None, + ReadOnly.into(), + ); + xmlnode_proto.add_property( + gc_context, + "nodeName", + Executable::Native(|_avm, _ac, this: Object<'gc>, _args| { + Ok(this + .as_xml_node() + .and_then(|n| n.tag_name()) + .map(|n| n.node_name().to_string().into()) + .unwrap_or(Value::Undefined.into())) + }), + None, + ReadOnly.into(), + ); + // TODO: AS2 only ever supported `Element` and `Text` nodes + xmlnode_proto.add_property( + gc_context, + "nodeType", + Executable::Native(|_avm, _ac, this: Object<'gc>, _args| { + Ok(this + .as_xml_node() + .map(|n| n.node_type().into()) + .unwrap_or(Value::Undefined.into())) + }), + None, + ReadOnly.into(), + ); + xmlnode_proto.add_property( + gc_context, + "nodeValue", + Executable::Native(|_avm, _ac, this: Object<'gc>, _args| { + Ok(this + .as_xml_node() + .and_then(|n| n.node_value()) + .map(|n| n.into()) + .unwrap_or(Value::Undefined.into())) + }), + None, + ReadOnly.into(), + ); + xmlnode_proto.add_property( + gc_context, + "prefix", + Executable::Native(|_avm, _ac, this: Object<'gc>, _args| { + Ok(this + .as_xml_node() + .and_then(|n| n.tag_name()) + .and_then(|n| n.prefix().map(|n| n.to_string().into())) + .unwrap_or(Value::Undefined.into())) + }), + None, + ReadOnly.into(), + ); xmlnode_proto.into() } diff --git a/core/src/avm1/xml_object.rs b/core/src/avm1/xml_object.rs index f8f593082..18003c305 100644 --- a/core/src/avm1/xml_object.rs +++ b/core/src/avm1/xml_object.rs @@ -23,7 +23,7 @@ pub enum XMLObject<'gc> { } impl<'gc> XMLObject<'gc> { - fn empty_document( + pub fn empty_document( gc_context: MutationContext<'gc, '_>, proto: Option>, ) -> XMLObject<'gc> { @@ -33,7 +33,7 @@ impl<'gc> XMLObject<'gc> { ) } - fn empty_node( + pub fn empty_node( gc_context: MutationContext<'gc, '_>, proto: Option>, ) -> XMLObject<'gc> { diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 43d734a14..2ca8e6aee 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -59,6 +59,27 @@ impl XMLName { }) } } + + /// Retrieve the local part of this name. + pub fn local_name(&self) -> &str { + &self.name + } + + /// Retrieve the prefix part of this name, if available. + pub fn prefix(&self) -> Option<&str> { + self.namespace.as_deref() + } + + /// Return the fully qualified part of the name. + /// + /// This consists of the namespace, if present, plus a colon and local name. + pub fn node_name(&self) -> String { + if let Some(ref ns) = self.namespace { + format!("{}:{}", ns, self.name) + } else { + format!("{}", self.name) + } + } } impl fmt::Debug for XMLName { From 7a9a16e5983ec3fbfbec40ac09cc1fc6380282c7 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 23:47:17 -0500 Subject: [PATCH 08/91] Don't repeat yourself. --- core/src/xml/document.rs | 47 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index d79ad4b36..b3aaf0b8a 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -20,61 +20,62 @@ pub struct XMLDocumentData<'gc> { } impl<'gc> XMLDocument<'gc> { + /// Construct a new, empty XML document. pub fn new(mc: MutationContext<'gc, '_>) -> Self { Self(GcCell::allocate(mc, XMLDocumentData { roots: Vec::new() })) } + /// Ensure that a newly-encountered node is added to an ongoing parsing + /// stack, or to the document itself if the parsing stack is empty. + fn add_child_to_tree( + &mut self, + mc: MutationContext<'gc, '_>, + open_tags: &mut Vec>, + child: XMLNode<'gc>, + ) -> Result<(), Error> { + if let Some(node) = open_tags.last_mut() { + node.append_child(mc, child)?; + } else { + self.0.write(mc).roots.push(child); + } + + Ok(()) + } + pub fn from_str(mc: MutationContext<'gc, '_>, data: &str) -> Result { let mut parser = Reader::from_str(data); let mut buf = Vec::new(); - let mut roots = Vec::new(); + let mut document = Self::new(mc); let mut open_tags: Vec> = Vec::new(); loop { match parser.read_event(&mut buf)? { Event::Start(bs) => { let child = XMLNode::from_start_event(mc, bs)?; - if let Some(node) = open_tags.last_mut() { - node.append_child(mc, child)?; - } else { - roots.push(child); - } - + document.add_child_to_tree(mc, &mut open_tags, child)?; open_tags.push(child); } Event::Empty(bs) => { let child = XMLNode::from_start_event(mc, bs)?; - if let Some(node) = open_tags.last_mut() { - node.append_child(mc, child)?; - } else { - roots.push(child); - } + document.add_child_to_tree(mc, &mut open_tags, child)?; } Event::End(_) => { open_tags.pop(); } Event::Text(bt) => { let child = XMLNode::text_from_text_event(mc, bt)?; - if let Some(node) = open_tags.last_mut() { - node.append_child(mc, child)?; - } else { - roots.push(child); - } + document.add_child_to_tree(mc, &mut open_tags, child)?; } Event::Comment(bt) => { let child = XMLNode::comment_from_text_event(mc, bt)?; - if let Some(node) = open_tags.last_mut() { - node.append_child(mc, child)?; - } else { - roots.push(child); - } + document.add_child_to_tree(mc, &mut open_tags, child)?; } Event::Eof => break, _ => {} } } - Ok(Self(GcCell::allocate(mc, XMLDocumentData { roots }))) + Ok(document) } /// Returns an iterator that yields the document's root nodes. From 3928c7cc5170a529dfb5882b8131457201a41ef2 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 20 Dec 2019 23:54:26 -0500 Subject: [PATCH 09/91] Reject empty text nodes --- core/src/xml/document.rs | 8 ++++++-- core/src/xml/tests.rs | 13 ------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index b3aaf0b8a..e464441f5 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -64,11 +64,15 @@ impl<'gc> XMLDocument<'gc> { } Event::Text(bt) => { let child = XMLNode::text_from_text_event(mc, bt)?; - document.add_child_to_tree(mc, &mut open_tags, child)?; + if child.node_value().as_deref() != Some("") { + document.add_child_to_tree(mc, &mut open_tags, child)?; + } } Event::Comment(bt) => { let child = XMLNode::comment_from_text_event(mc, bt)?; - document.add_child_to_tree(mc, &mut open_tags, child)?; + if child.node_value().as_deref() != Some("") { + document.add_child_to_tree(mc, &mut open_tags, child)?; + } } Event::Eof => break, _ => {} diff --git a/core/src/xml/tests.rs b/core/src/xml/tests.rs index ad2ccbcc1..6c5eda15c 100644 --- a/core/src/xml/tests.rs +++ b/core/src/xml/tests.rs @@ -12,23 +12,10 @@ fn parse_single_element() { let mut roots = xml.roots(); let root = roots.next().expect("Parsed document should have a root"); - assert_eq!(root.node_type(), xml::TEXT_NODE); - assert_eq!(root.node_value(), Some("".to_string())); - - let root = roots - .next() - .expect("Parsed document should have a second root"); assert_eq!(root.node_type(), xml::ELEMENT_NODE); assert_eq!(root.tag_name(), Some(XMLName::from_str("test").unwrap())); let mut root_children = root.children().unwrap(); - - let child_text = root_children - .next() - .expect("Single nodes should have an empty text child"); - assert_eq!(child_text.node_type(), xml::TEXT_NODE); - assert_eq!(child_text.node_value(), Some("".to_string())); - assert!(root_children.next().is_none()); assert!(roots.next().is_none()); From 0fe0e4fe902c35fb5e14c7cb5b1a060202c9e4da Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 15:52:34 -0500 Subject: [PATCH 10/91] Separate `XMLName` into another module --- core/src/xml.rs | 3 +- core/src/xml/namespace.rs | 89 +++++++++++++++++++++++++++++++++++++++ core/src/xml/tree.rs | 85 +------------------------------------ 3 files changed, 92 insertions(+), 85 deletions(-) create mode 100644 core/src/xml/namespace.rs diff --git a/core/src/xml.rs b/core/src/xml.rs index dd510fd72..39b3619df 100644 --- a/core/src/xml.rs +++ b/core/src/xml.rs @@ -1,6 +1,7 @@ //! Garbage-collectable XML DOM impl mod document; +mod namespace; mod tree; #[cfg(test)] @@ -9,7 +10,7 @@ mod tests; type Error = Box; pub use document::XMLDocument; -pub use tree::XMLName; +pub use namespace::XMLName; pub use tree::XMLNode; pub const ELEMENT_NODE: u8 = 1; diff --git a/core/src/xml/namespace.rs b/core/src/xml/namespace.rs new file mode 100644 index 000000000..a0ff1f8ea --- /dev/null +++ b/core/src/xml/namespace.rs @@ -0,0 +1,89 @@ +//! XML namespacing support + +use crate::xml::Error; +use gc_arena::Collect; +use std::borrow::Cow; +use std::fmt; + +/// Represents a scoped name within XML. +/// +/// All names in XML are optionally namespaced. Each namespace is represented +/// as a string; the document contains a mapping of namespaces to URIs. +/// +/// The special namespace `xmlns` is used to map namespace strings to URIs; it +/// should not be used for user-specified namespaces. +#[derive(Clone, Collect, PartialEq, Eq, PartialOrd, Ord)] +#[collect(no_drop)] +pub struct XMLName { + /// The name of the XML namespace this name is scoped to. + /// + /// Names without a namespace use the default namespace. + /// + /// Namespaces may be resolved to a URI by consulting the encapsulating + /// document. + namespace: Option, + name: String, +} + +impl XMLName { + pub fn from_bytes(bytes: &[u8]) -> Result { + Self::from_bytes_cow(Cow::Borrowed(bytes)) + } + + pub fn from_str(strval: &str) -> Result { + Self::from_str_cow(Cow::Borrowed(strval)) + } + + pub fn from_bytes_cow(bytes: Cow<[u8]>) -> Result { + let full_name = match bytes { + Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), + Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), + }; + + Self::from_str_cow(full_name) + } + + pub fn from_str_cow(full_name: Cow) -> Result { + if let Some(colon_index) = full_name.find(':') { + Ok(Self { + namespace: Some(full_name[0..colon_index].to_owned()), + name: full_name[colon_index + 1..].to_owned(), + }) + } else { + Ok(Self { + namespace: None, + name: full_name.into_owned(), + }) + } + } + + /// Retrieve the local part of this name. + pub fn local_name(&self) -> &str { + &self.name + } + + /// Retrieve the prefix part of this name, if available. + pub fn prefix(&self) -> Option<&str> { + self.namespace.as_deref() + } + + /// Return the fully qualified part of the name. + /// + /// This consists of the namespace, if present, plus a colon and local name. + pub fn node_name(&self) -> String { + if let Some(ref ns) = self.namespace { + format!("{}:{}", ns, self.name) + } else { + format!("{}", self.name) + } + } +} + +impl fmt::Debug for XMLName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("XMLName") + .field("namespace", &self.namespace) + .field("name", &self.name) + .finish() + } +} diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 2ca8e6aee..e435fbf7d 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -1,96 +1,13 @@ //! XML Tree structure use crate::xml; -use crate::xml::Error; +use crate::xml::{Error, XMLName}; use gc_arena::{Collect, GcCell, MutationContext}; use quick_xml::events::{BytesStart, BytesText}; use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt; -/// Represents a scoped name within XML. -/// -/// All names in XML are optionally namespaced. Each namespace is represented -/// as a string; the document contains a mapping of namespaces to URIs. -/// -/// The special namespace `xmlns` is used to map namespace strings to URIs; it -/// should not be used for user-specified namespaces. -#[derive(Clone, Collect, PartialEq, Eq, PartialOrd, Ord)] -#[collect(no_drop)] -pub struct XMLName { - /// The name of the XML namespace this name is scoped to. - /// - /// Names without a namespace use the default namespace. - /// - /// Namespaces may be resolved to a URI by consulting the encapsulating - /// document. - namespace: Option, - name: String, -} - -impl XMLName { - pub fn from_bytes(bytes: &[u8]) -> Result { - Self::from_bytes_cow(Cow::Borrowed(bytes)) - } - - pub fn from_str(strval: &str) -> Result { - Self::from_str_cow(Cow::Borrowed(strval)) - } - - pub fn from_bytes_cow(bytes: Cow<[u8]>) -> Result { - let full_name = match bytes { - Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), - Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), - }; - - Self::from_str_cow(full_name) - } - - pub fn from_str_cow(full_name: Cow) -> Result { - if let Some(colon_index) = full_name.find(':') { - Ok(Self { - namespace: Some(full_name[0..colon_index].to_owned()), - name: full_name[colon_index + 1..].to_owned(), - }) - } else { - Ok(Self { - namespace: None, - name: full_name.into_owned(), - }) - } - } - - /// Retrieve the local part of this name. - pub fn local_name(&self) -> &str { - &self.name - } - - /// Retrieve the prefix part of this name, if available. - pub fn prefix(&self) -> Option<&str> { - self.namespace.as_deref() - } - - /// Return the fully qualified part of the name. - /// - /// This consists of the namespace, if present, plus a colon and local name. - pub fn node_name(&self) -> String { - if let Some(ref ns) = self.namespace { - format!("{}:{}", ns, self.name) - } else { - format!("{}", self.name) - } - } -} - -impl fmt::Debug for XMLName { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("XMLName") - .field("namespace", &self.namespace) - .field("name", &self.name) - .finish() - } -} - /// Represents a node in the XML tree. #[derive(Copy, Clone, Collect)] #[collect(no_drop)] From b06dd5d15ec01132793fd8e5264a9384c7dbd8d1 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 17:07:38 -0500 Subject: [PATCH 11/91] Add a special node to represent the document root in the node tree, and get rid of the explicit document reference type in `XMLObject`. --- core/src/avm1/globals/xml.rs | 20 ++-- core/src/avm1/object.rs | 7 +- core/src/avm1/xml_object.rs | 57 ++---------- core/src/xml/document.rs | 61 +++++------- core/src/xml/tree.rs | 175 +++++++++++++++++++++++++++++++---- 5 files changed, 204 insertions(+), 116 deletions(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 5ff37ade9..ad7e33ded 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -17,17 +17,19 @@ pub fn xmlnode_constructor<'gc>( this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error> { + let blank_document = XMLDocument::new(ac.gc_context); + match ( args.get(0).map(|v| v.as_number(avm, ac).map(|v| v as u32)), args.get(1).map(|v| v.clone().coerce_to_string(avm, ac)), this.as_xml_node(), ) { (Some(Ok(1)), Some(Ok(ref strval)), Some(ref mut this_node)) => { - let mut xmlelement = XMLNode::new_text(ac.gc_context, strval); + let mut xmlelement = XMLNode::new_text(ac.gc_context, strval, blank_document); swap(&mut xmlelement, this_node); } (Some(Ok(3)), Some(Ok(ref strval)), Some(ref mut this_node)) => { - let mut xmlelement = XMLNode::new_element(ac.gc_context, strval)?; + let mut xmlelement = XMLNode::new_element(ac.gc_context, strval, blank_document)?; swap(&mut xmlelement, this_node); } //Invalid nodetype ID, string value missing, or not an XMLElement @@ -123,17 +125,17 @@ pub fn xml_constructor<'gc>( ) -> Result, Error> { match ( args.get(0).map(|v| v.clone().coerce_to_string(avm, ac)), - this.as_xml_document(), + this.as_xml_node(), ) { - (Some(Ok(ref string)), Some(ref mut this_doc)) => { - let mut xmldoc = XMLDocument::from_str(ac.gc_context, string)?; + (Some(Ok(ref string)), Some(ref mut this_node)) => { + let xmldoc = XMLDocument::from_str(ac.gc_context, string)?; - swap(&mut xmldoc, this_doc); + swap(&mut xmldoc.as_node(), this_node); } - (None, Some(ref mut this_doc)) => { - let mut emptydoc = XMLDocument::new(ac.gc_context); + (None, Some(ref mut this_node)) => { + let xmldoc = XMLDocument::new(ac.gc_context); - swap(&mut emptydoc, this_doc); + swap(&mut xmldoc.as_node(), this_node); } //Non-string argument or not an XML document _ => {} diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index b30cced26..3da09c63a 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -7,7 +7,7 @@ use crate::avm1::super_object::SuperObject; use crate::avm1::xml_object::XMLObject; use crate::avm1::{Avm1, Error, ScriptObject, SoundObject, StageObject, UpdateContext, Value}; use crate::display_object::DisplayObject; -use crate::xml::{XMLDocument, XMLNode}; +use crate::xml::XMLNode; use enumset::EnumSet; use gc_arena::{Collect, MutationContext}; use ruffle_macros::enum_trait_object; @@ -270,11 +270,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy None } - /// Get the underlying XML document for this object, if it exists. - fn as_xml_document(&self) -> Option> { - None - } - /// Get the underlying XML node for this object, if it exists. fn as_xml_node(&self) -> Option> { None diff --git a/core/src/avm1/xml_object.rs b/core/src/avm1/xml_object.rs index 18003c305..726224d23 100644 --- a/core/src/avm1/xml_object.rs +++ b/core/src/avm1/xml_object.rs @@ -14,39 +14,24 @@ use std::fmt; /// A ScriptObject that is inherently tied to an XML node. #[derive(Clone, Copy, Collect)] #[collect(no_drop)] -pub enum XMLObject<'gc> { - /// An `XMLObject` that references a whole document. - Document(ScriptObject<'gc>, XMLDocument<'gc>), - - /// An `XMLObject` that references a specific node of another document. - Node(ScriptObject<'gc>, XMLNode<'gc>), -} +pub struct XMLObject<'gc>(ScriptObject<'gc>, XMLNode<'gc>); impl<'gc> XMLObject<'gc> { - pub fn empty_document( - gc_context: MutationContext<'gc, '_>, - proto: Option>, - ) -> XMLObject<'gc> { - XMLObject::Document( - ScriptObject::object(gc_context, proto), - XMLDocument::new(gc_context), - ) - } - pub fn empty_node( gc_context: MutationContext<'gc, '_>, proto: Option>, ) -> XMLObject<'gc> { - XMLObject::Node( + let empty_document = XMLDocument::new(gc_context); + + XMLObject( ScriptObject::object(gc_context, proto), - XMLNode::new_text(gc_context, ""), + XMLNode::new_text(gc_context, "", empty_document), ) } fn base(&self) -> ScriptObject<'gc> { match self { - XMLObject::Document(base, ..) => *base, - XMLObject::Node(base, ..) => *base, + XMLObject(base, ..) => *base, } } } @@ -54,18 +39,7 @@ impl<'gc> XMLObject<'gc> { impl fmt::Debug for XMLObject<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - XMLObject::Document(base, ..) => { - f.debug_tuple("XMLObject::Document") - .field(base) - //.field(document) - .finish() - } - XMLObject::Node(base, ..) => { - f.debug_tuple("XMLObject::Node") - .field(base) - //.field(document) - .finish() - } + XMLObject(base, node) => f.debug_tuple("XMLObject").field(base).field(node).finish(), } } } @@ -109,12 +83,7 @@ impl<'gc> TObject<'gc> for XMLObject<'gc> { this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { - match self { - XMLObject::Document(..) => { - Ok(XMLObject::empty_document(context.gc_context, Some(this)).into()) - } - XMLObject::Node(..) => Ok(XMLObject::empty_node(context.gc_context, Some(this)).into()), - } + Ok(XMLObject::empty_node(context.gc_context, Some(this)).into()) } fn delete(&self, gc_context: MutationContext<'gc, '_>, name: &str) -> bool { @@ -199,17 +168,9 @@ impl<'gc> TObject<'gc> for XMLObject<'gc> { Some(self.base()) } - fn as_xml_document(&self) -> Option> { - match self { - XMLObject::Document(_base, document) => Some(*document), - _ => None, - } - } - fn as_xml_node(&self) -> Option> { match self { - XMLObject::Node(_base, node) => Some(*node), - _ => None, + XMLObject(_base, node) => Some(*node), } } diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index e464441f5..e4e20fcc6 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -15,14 +15,19 @@ pub struct XMLDocument<'gc>(GcCell<'gc, XMLDocumentData<'gc>>); #[derive(Clone, Collect)] #[collect(no_drop)] pub struct XMLDocumentData<'gc> { - /// The root node(s) of the XML document. - roots: Vec>, + /// The root node of the XML document. + root: Option>, } impl<'gc> XMLDocument<'gc> { /// Construct a new, empty XML document. pub fn new(mc: MutationContext<'gc, '_>) -> Self { - Self(GcCell::allocate(mc, XMLDocumentData { roots: Vec::new() })) + let document = Self(GcCell::allocate(mc, XMLDocumentData { root: None })); + let root = XMLNode::new_document_root(mc, document); + + document.0.write(mc).root = Some(root); + + document } /// Ensure that a newly-encountered node is added to an ongoing parsing @@ -36,7 +41,7 @@ impl<'gc> XMLDocument<'gc> { if let Some(node) = open_tags.last_mut() { node.append_child(mc, child)?; } else { - self.0.write(mc).roots.push(child); + self.as_node().append_child(mc, child)?; } Ok(()) @@ -51,25 +56,25 @@ impl<'gc> XMLDocument<'gc> { loop { match parser.read_event(&mut buf)? { Event::Start(bs) => { - let child = XMLNode::from_start_event(mc, bs)?; + let child = XMLNode::from_start_event(mc, bs, document)?; document.add_child_to_tree(mc, &mut open_tags, child)?; open_tags.push(child); } Event::Empty(bs) => { - let child = XMLNode::from_start_event(mc, bs)?; + let child = XMLNode::from_start_event(mc, bs, document)?; document.add_child_to_tree(mc, &mut open_tags, child)?; } Event::End(_) => { open_tags.pop(); } Event::Text(bt) => { - let child = XMLNode::text_from_text_event(mc, bt)?; + let child = XMLNode::text_from_text_event(mc, bt, document)?; if child.node_value().as_deref() != Some("") { document.add_child_to_tree(mc, &mut open_tags, child)?; } } Event::Comment(bt) => { - let child = XMLNode::comment_from_text_event(mc, bt)?; + let child = XMLNode::comment_from_text_event(mc, bt, document)?; if child.node_value().as_deref() != Some("") { document.add_child_to_tree(mc, &mut open_tags, child)?; } @@ -84,42 +89,24 @@ impl<'gc> XMLDocument<'gc> { /// Returns an iterator that yields the document's root nodes. pub fn roots(&self) -> impl Iterator> { - struct RootsIter<'gc> { - base: XMLDocument<'gc>, - index: usize, - }; + self.as_node() + .children() + .expect("Document root node must always be capable of holding children") + } - impl<'gc> RootsIter<'gc> { - fn for_document(base: XMLDocument<'gc>) -> Self { - Self { base, index: 0 } - } - } - - impl<'gc> Iterator for RootsIter<'gc> { - type Item = XMLNode<'gc>; - - fn next(&mut self) -> Option { - let (len, item) = { - let r = self.base.0.read(); - (r.roots.len(), r.roots.get(self.index).cloned()) - }; - - if self.index < len { - self.index += 1; - } - - item - } - } - - RootsIter::for_document(*self) + /// Yield the document in node form. + pub fn as_node(&self) -> XMLNode<'gc> { + self.0 + .read() + .root + .expect("Document must always have a root node") } } impl<'gc> fmt::Debug for XMLDocument<'gc> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("XMLDocument") - .field("roots", &self.0.read().roots) + .field("root", &self.0.read().root) .finish() } } diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index e435fbf7d..d799bf3c1 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -1,7 +1,7 @@ //! XML Tree structure use crate::xml; -use crate::xml::{Error, XMLName}; +use crate::xml::{Error, XMLDocument, XMLName}; use gc_arena::{Collect, GcCell, MutationContext}; use quick_xml::events::{BytesStart, BytesText}; use std::borrow::Cow; @@ -18,12 +18,24 @@ pub struct XMLNode<'gc>(GcCell<'gc, XMLNodeData<'gc>>); pub enum XMLNodeData<'gc> { /// A text node in the XML tree. Text { + /// The document that this tree node currently belongs to. + document: XMLDocument<'gc>, + + /// The parent node of this one. + parent: Option>, + /// The string representation of the text. contents: String, }, /// A comment node in the XML tree. Comment { + /// The document that this tree node currently belongs to. + document: XMLDocument<'gc>, + + /// The parent node of this one. + parent: Option>, + /// The string representation of the comment. contents: String, }, @@ -34,6 +46,12 @@ pub enum XMLNodeData<'gc> { /// either attributes (for key/value pairs) or child nodes (for more /// structured data). Element { + /// The document that this tree node currently belongs to. + document: XMLDocument<'gc>, + + /// The parent node of this one. + parent: Option>, + /// The tag name of this element. tag_name: XMLName, @@ -43,24 +61,45 @@ pub enum XMLNodeData<'gc> { /// Child nodes of this element. children: Vec>, }, + + /// The root level of an XML document. Has no parent. + DocumentRoot { + /// The document that this is the root of. + document: XMLDocument<'gc>, + + /// Child nodes of this element. + children: Vec>, + }, } impl<'gc> XMLNode<'gc> { /// Construct a new XML text node. - pub fn new_text(mc: MutationContext<'gc, '_>, contents: &str) -> Self { + pub fn new_text( + mc: MutationContext<'gc, '_>, + contents: &str, + document: XMLDocument<'gc>, + ) -> Self { XMLNode(GcCell::allocate( mc, XMLNodeData::Text { + document, + parent: None, contents: contents.to_string(), }, )) } /// Construct a new XML element node. - pub fn new_element(mc: MutationContext<'gc, '_>, element_name: &str) -> Result { + pub fn new_element( + mc: MutationContext<'gc, '_>, + element_name: &str, + document: XMLDocument<'gc>, + ) -> Result { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Element { + document, + parent: None, tag_name: XMLName::from_str(element_name)?, attributes: BTreeMap::new(), children: Vec::new(), @@ -68,6 +107,17 @@ impl<'gc> XMLNode<'gc> { ))) } + /// Construct a new XML root node. + pub fn new_document_root(mc: MutationContext<'gc, '_>, document: XMLDocument<'gc>) -> Self { + XMLNode(GcCell::allocate( + mc, + XMLNodeData::DocumentRoot { + document, + children: Vec::new(), + }, + )) + } + /// Construct an XML Element node from a `quick_xml` `BytesStart` event. /// /// The returned node will always be an `Element`, and it must only contain @@ -75,6 +125,7 @@ impl<'gc> XMLNode<'gc> { pub fn from_start_event<'a>( mc: MutationContext<'gc, '_>, bs: BytesStart<'a>, + document: XMLDocument<'gc>, ) -> Result { let tag_name = XMLName::from_bytes_cow(bs.unescaped()?)?; let mut attributes = BTreeMap::new(); @@ -92,6 +143,8 @@ impl<'gc> XMLNode<'gc> { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Element { + document, + parent: None, tag_name, attributes, children, @@ -106,10 +159,13 @@ impl<'gc> XMLNode<'gc> { pub fn text_from_text_event<'a>( mc: MutationContext<'gc, '_>, bt: BytesText<'a>, + document: XMLDocument<'gc>, ) -> Result { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Text { + document, + parent: None, contents: match bt.unescaped()? { Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), @@ -127,10 +183,13 @@ impl<'gc> XMLNode<'gc> { pub fn comment_from_text_event<'a>( mc: MutationContext<'gc, '_>, bt: BytesText<'a>, + document: XMLDocument<'gc>, ) -> Result { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Comment { + document, + parent: None, contents: match bt.unescaped()? { Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), @@ -141,8 +200,56 @@ impl<'gc> XMLNode<'gc> { ))) } + /// Return the XML document that this tree node belongs to. + /// + /// Every XML node belongs to a document object (see `XMLDocument`) which + /// stores global information about the document, such as namespace URIs. + pub fn document(&self) -> XMLDocument<'gc> { + match &*self.0.read() { + XMLNodeData::Text { document, .. } => *document, + XMLNodeData::Comment { document, .. } => *document, + XMLNodeData::Element { document, .. } => *document, + XMLNodeData::DocumentRoot { document, .. } => *document, + } + } + + /// Adopt a child element into the current node. + /// + /// This does not add the node to any internal lists; it merely updates the + /// child to ensure that it considers this node it's parent. This function + /// should always be called after a child node is added to this one. + pub fn adopt( + &mut self, + mc: MutationContext<'gc, '_>, + child: XMLNode<'gc>, + ) -> Result<(), Error> { + let mut write = child.0.write(mc); + let (child_document, child_parent) = match &mut *write { + XMLNodeData::Element { + document, parent, .. + } => Ok((document, parent)), + XMLNodeData::Text { + document, parent, .. + } => Ok((document, parent)), + XMLNodeData::Comment { + document, parent, .. + } => Ok((document, parent)), + XMLNodeData::DocumentRoot { .. } => Err("Cannot adopt other document roots"), + }?; + + *child_document = self.document(); + *child_parent = Some(*self); + + Ok(()) + } + /// Append a child element to an Element node. /// + /// The child will be adopted into the current tree: all child references + /// to other nodes or documents will be adjusted to reflect it's new + /// position in the tree. This may remove it from any existing trees or + /// documents. + /// /// This function yields an error if appending to a Node that cannot accept /// children. In that case, no modification will be made to the node. pub fn append_child( @@ -153,10 +260,14 @@ impl<'gc> XMLNode<'gc> { match &mut *self.0.write(mc) { XMLNodeData::Element { ref mut children, .. - } => children.push(child), + } => { + children.push(child); + } _ => return Err("Not an Element".into()), }; + self.adopt(mc, child)?; + Ok(()) } @@ -167,6 +278,7 @@ impl<'gc> XMLNode<'gc> { pub fn node_type(&self) -> u8 { match &*self.0.read() { XMLNodeData::Element { .. } => xml::ELEMENT_NODE, + XMLNodeData::DocumentRoot { .. } => xml::ELEMENT_NODE, XMLNodeData::Text { .. } => xml::TEXT_NODE, XMLNodeData::Comment { .. } => xml::COMMENT_NODE, } @@ -183,8 +295,8 @@ impl<'gc> XMLNode<'gc> { /// Returns the string contents of the node, if the element has them. pub fn node_value(&self) -> Option { match &*self.0.read() { - XMLNodeData::Text { ref contents } => Some(contents.clone()), - XMLNodeData::Comment { ref contents } => Some(contents.clone()), + XMLNodeData::Text { ref contents, .. } => Some(contents.clone()), + XMLNodeData::Comment { ref contents, .. } => Some(contents.clone()), _ => None, } } @@ -208,20 +320,30 @@ impl<'gc> XMLNode<'gc> { type Item = XMLNode<'gc>; fn next(&mut self) -> Option { - match &*self.base.0.read() { - XMLNodeData::Element { ref children, .. } if self.index < children.len() => { + let read = self.base.0.read(); + let children = match &*read { + XMLNodeData::Element { children, .. } + | XMLNodeData::DocumentRoot { children, .. } => Some(children), + _ => None, + }; + + if let Some(children) = children { + if self.index < children.len() { let item = children.get(self.index).cloned(); self.index += 1; - item + return item; } - _ => None, } + + None } } match &*self.0.read() { - XMLNodeData::Element { .. } => Some(ChildIter::for_node(*self)), + XMLNodeData::Element { .. } | XMLNodeData::DocumentRoot { .. } => { + Some(ChildIter::for_node(*self)) + } _ => return None, } } @@ -230,24 +352,45 @@ impl<'gc> XMLNode<'gc> { impl<'gc> fmt::Debug for XMLNode<'gc> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &*self.0.read() { - XMLNodeData::Text { ref contents } => f + XMLNodeData::Text { + document, + parent, + contents, + } => f .debug_struct("XMLNodeData::Text") + .field("document", document) + .field("parent", parent) .field("contents", contents) .finish(), - XMLNodeData::Comment { ref contents } => f + XMLNodeData::Comment { + document, + parent, + contents, + } => f .debug_struct("XMLNodeData::Comment") + .field("document", document) + .field("parent", parent) .field("contents", contents) .finish(), XMLNodeData::Element { - ref tag_name, - ref attributes, - ref children, + document, + parent, + tag_name, + attributes, + children, } => f .debug_struct("XMLNodeData::Element") + .field("document", document) + .field("parent", parent) .field("tag_name", tag_name) .field("attributes", attributes) .field("children", children) .finish(), + XMLNodeData::DocumentRoot { document, children } => f + .debug_struct("XMLNodeData::DocumentRoot") + .field("document", document) + .field("children", children) + .finish(), } } } From e7768d0802050f7eae0c85ec1aad51fc05d4df4f Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 22:24:27 -0500 Subject: [PATCH 12/91] Add methods to allow storing XML objects on the accompanying tree nodes, so that expando properties on child nodes will work. --- core/src/avm1.rs | 2 +- core/src/avm1/xml_object.rs | 22 ++++++++--- core/src/xml/tree.rs | 77 ++++++++++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index ed6243de7..3ee1837e5 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -33,7 +33,7 @@ mod sound_object; mod stage_object; mod super_object; mod value; -mod xml_object; +pub mod xml_object; #[cfg(test)] mod tests; diff --git a/core/src/avm1/xml_object.rs b/core/src/avm1/xml_object.rs index 726224d23..ab859e346 100644 --- a/core/src/avm1/xml_object.rs +++ b/core/src/avm1/xml_object.rs @@ -17,16 +17,28 @@ use std::fmt; pub struct XMLObject<'gc>(ScriptObject<'gc>, XMLNode<'gc>); impl<'gc> XMLObject<'gc> { + /// Construct a new XML node and object pair. pub fn empty_node( gc_context: MutationContext<'gc, '_>, proto: Option>, - ) -> XMLObject<'gc> { + ) -> Object<'gc> { let empty_document = XMLDocument::new(gc_context); + let mut xml_node = XMLNode::new_text(gc_context, "", empty_document); + let base_object = ScriptObject::object(gc_context, proto); + let object = XMLObject(base_object, xml_node).into(); - XMLObject( - ScriptObject::object(gc_context, proto), - XMLNode::new_text(gc_context, "", empty_document), - ) + xml_node.introduce_script_object(gc_context, object); + + object + } + + /// Construct an XMLObject for an already existing node. + pub fn from_xml_node( + gc_context: MutationContext<'gc, '_>, + xml_node: XMLNode<'gc>, + proto: Option>, + ) -> Object<'gc> { + XMLObject(ScriptObject::object(gc_context, proto), xml_node).into() } fn base(&self) -> ScriptObject<'gc> { diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index d799bf3c1..3d782b3c7 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -1,5 +1,7 @@ //! XML Tree structure +use crate::avm1::xml_object::XMLObject; +use crate::avm1::Object; use crate::xml; use crate::xml::{Error, XMLDocument, XMLName}; use gc_arena::{Collect, GcCell, MutationContext}; @@ -18,6 +20,9 @@ pub struct XMLNode<'gc>(GcCell<'gc, XMLNodeData<'gc>>); pub enum XMLNodeData<'gc> { /// A text node in the XML tree. Text { + /// The script object associated with this XML node, if any. + script_object: Option>, + /// The document that this tree node currently belongs to. document: XMLDocument<'gc>, @@ -30,6 +35,9 @@ pub enum XMLNodeData<'gc> { /// A comment node in the XML tree. Comment { + /// The script object associated with this XML node, if any. + script_object: Option>, + /// The document that this tree node currently belongs to. document: XMLDocument<'gc>, @@ -46,6 +54,9 @@ pub enum XMLNodeData<'gc> { /// either attributes (for key/value pairs) or child nodes (for more /// structured data). Element { + /// The script object associated with this XML node, if any. + script_object: Option>, + /// The document that this tree node currently belongs to. document: XMLDocument<'gc>, @@ -64,6 +75,9 @@ pub enum XMLNodeData<'gc> { /// The root level of an XML document. Has no parent. DocumentRoot { + /// The script object associated with this XML node, if any. + script_object: Option>, + /// The document that this is the root of. document: XMLDocument<'gc>, @@ -82,6 +96,7 @@ impl<'gc> XMLNode<'gc> { XMLNode(GcCell::allocate( mc, XMLNodeData::Text { + script_object: None, document, parent: None, contents: contents.to_string(), @@ -98,6 +113,7 @@ impl<'gc> XMLNode<'gc> { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Element { + script_object: None, document, parent: None, tag_name: XMLName::from_str(element_name)?, @@ -112,6 +128,7 @@ impl<'gc> XMLNode<'gc> { XMLNode(GcCell::allocate( mc, XMLNodeData::DocumentRoot { + script_object: None, document, children: Vec::new(), }, @@ -143,6 +160,7 @@ impl<'gc> XMLNode<'gc> { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Element { + script_object: None, document, parent: None, tag_name, @@ -164,6 +182,7 @@ impl<'gc> XMLNode<'gc> { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Text { + script_object: None, document, parent: None, contents: match bt.unescaped()? { @@ -188,6 +207,7 @@ impl<'gc> XMLNode<'gc> { Ok(XMLNode(GcCell::allocate( mc, XMLNodeData::Comment { + script_object: None, document, parent: None, contents: match bt.unescaped()? { @@ -347,32 +367,81 @@ impl<'gc> XMLNode<'gc> { _ => return None, } } + + /// Get the already-instantiated script object from the current node. + fn get_script_object(&self) -> Option> { + match &*self.0.read() { + XMLNodeData::Element { script_object, .. } => *script_object, + XMLNodeData::Text { script_object, .. } => *script_object, + XMLNodeData::Comment { script_object, .. } => *script_object, + XMLNodeData::DocumentRoot { script_object, .. } => *script_object, + } + } + + /// Introduce this node to a new script object. + /// + /// This internal function *will* overwrite already extant objects, so only + /// call this if you need to instantiate the script object for the first + /// time. + pub fn introduce_script_object( + &mut self, + gc_context: MutationContext<'gc, '_>, + new_object: Object<'gc>, + ) { + match &mut *self.0.write(gc_context) { + XMLNodeData::Element { script_object, .. } => *script_object = Some(new_object), + XMLNodeData::Text { script_object, .. } => *script_object = Some(new_object), + XMLNodeData::Comment { script_object, .. } => *script_object = Some(new_object), + XMLNodeData::DocumentRoot { script_object, .. } => *script_object = Some(new_object), + } + } + + /// Obtain the script object for a given XML tree node, constructing a new + /// script object if one does not exist. + pub fn script_object( + &mut self, + gc_context: MutationContext<'gc, '_>, + prototype: Option>, + ) -> Object<'gc> { + let mut object = self.get_script_object(); + if object.is_none() { + object = Some(XMLObject::from_xml_node(gc_context, *self, prototype)); + self.introduce_script_object(gc_context, object.unwrap()); + } + + object.unwrap() + } } impl<'gc> fmt::Debug for XMLNode<'gc> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &*self.0.read() { XMLNodeData::Text { + script_object, document, parent, contents, } => f .debug_struct("XMLNodeData::Text") + .field("script_object", script_object) .field("document", document) .field("parent", parent) .field("contents", contents) .finish(), XMLNodeData::Comment { + script_object, document, parent, contents, } => f .debug_struct("XMLNodeData::Comment") + .field("script_object", script_object) .field("document", document) .field("parent", parent) .field("contents", contents) .finish(), XMLNodeData::Element { + script_object, document, parent, tag_name, @@ -380,14 +449,20 @@ impl<'gc> fmt::Debug for XMLNode<'gc> { children, } => f .debug_struct("XMLNodeData::Element") + .field("script_object", script_object) .field("document", document) .field("parent", parent) .field("tag_name", tag_name) .field("attributes", attributes) .field("children", children) .finish(), - XMLNodeData::DocumentRoot { document, children } => f + XMLNodeData::DocumentRoot { + script_object, + document, + children, + } => f .debug_struct("XMLNodeData::DocumentRoot") + .field("script_object", script_object) .field("document", document) .field("children", children) .finish(), From 0af248d81feb457515bdd90ec78f280375b2218f Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 22:50:03 -0500 Subject: [PATCH 13/91] Expose `childNodes` to ActionScript --- core/src/avm1/globals.rs | 3 +++ core/src/avm1/globals/xml.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index d88ca3b18..951ef2d8f 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -139,6 +139,7 @@ pub struct SystemPrototypes<'gc> { pub sound: Object<'gc>, pub text_field: Object<'gc>, pub array: Object<'gc>, + pub xml_node: Object<'gc>, } unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> { @@ -150,6 +151,7 @@ unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> { self.sound.trace(cc); self.text_field.trace(cc); self.array.trace(cc); + self.xml_node.trace(cc); } } @@ -364,6 +366,7 @@ pub fn create_globals<'gc>( sound: sound_proto, text_field: text_field_proto, array: array_proto, + xml_node: xmlnode_proto, }, globals.into(), listeners, diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index ad7e33ded..ba8f39663 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -112,6 +112,32 @@ pub fn create_xmlnode_proto<'gc>( None, ReadOnly.into(), ); + xmlnode_proto.add_property( + gc_context, + "childNodes", + Executable::Native(|avm, ac, this: Object<'gc>, _args| { + if let Some(node) = this.as_xml_node() { + let array = ScriptObject::array(ac.gc_context, Some(avm.prototypes.array)); + if let Some(children) = node.children() { + for (i, mut child) in children.enumerate() { + array.set_array_element( + i as usize, + child + .script_object(ac.gc_context, Some(avm.prototypes.xml_node)) + .into(), + ac.gc_context, + ); + } + + return Ok(array.into()); + } + } + + Ok(Value::Undefined.into()) + }), + None, + ReadOnly.into(), + ); xmlnode_proto.into() } From 571c4bbd5218d928050930a516d43841057810d0 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 22:54:59 -0500 Subject: [PATCH 14/91] Cargo fmt compliance --- core/src/avm1/globals/xml.rs | 14 +++++++------- core/src/avm1/xml_object.rs | 2 +- core/src/xml/document.rs | 4 ++-- core/src/xml/namespace.rs | 2 +- core/src/xml/tree.rs | 16 ++++++++-------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index ba8f39663..740e937ab 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -55,7 +55,7 @@ pub fn create_xmlnode_proto<'gc>( .as_xml_node() .and_then(|n| n.tag_name()) .map(|n| n.local_name().to_string().into()) - .unwrap_or(Value::Undefined.into())) + .unwrap_or_else(|| Value::Undefined.into())) }), None, ReadOnly.into(), @@ -67,8 +67,8 @@ pub fn create_xmlnode_proto<'gc>( Ok(this .as_xml_node() .and_then(|n| n.tag_name()) - .map(|n| n.node_name().to_string().into()) - .unwrap_or(Value::Undefined.into())) + .map(|n| n.node_name().into()) + .unwrap_or_else(|| Value::Undefined.into())) }), None, ReadOnly.into(), @@ -81,7 +81,7 @@ pub fn create_xmlnode_proto<'gc>( Ok(this .as_xml_node() .map(|n| n.node_type().into()) - .unwrap_or(Value::Undefined.into())) + .unwrap_or_else(|| Value::Undefined.into())) }), None, ReadOnly.into(), @@ -94,7 +94,7 @@ pub fn create_xmlnode_proto<'gc>( .as_xml_node() .and_then(|n| n.node_value()) .map(|n| n.into()) - .unwrap_or(Value::Undefined.into())) + .unwrap_or_else(|| Value::Undefined.into())) }), None, ReadOnly.into(), @@ -107,7 +107,7 @@ pub fn create_xmlnode_proto<'gc>( .as_xml_node() .and_then(|n| n.tag_name()) .and_then(|n| n.prefix().map(|n| n.to_string().into())) - .unwrap_or(Value::Undefined.into())) + .unwrap_or_else(|| Value::Undefined.into())) }), None, ReadOnly.into(), @@ -139,7 +139,7 @@ pub fn create_xmlnode_proto<'gc>( ReadOnly.into(), ); - xmlnode_proto.into() + xmlnode_proto } /// XML (document) constructor diff --git a/core/src/avm1/xml_object.rs b/core/src/avm1/xml_object.rs index ab859e346..8cfee1ca4 100644 --- a/core/src/avm1/xml_object.rs +++ b/core/src/avm1/xml_object.rs @@ -95,7 +95,7 @@ impl<'gc> TObject<'gc> for XMLObject<'gc> { this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { - Ok(XMLObject::empty_node(context.gc_context, Some(this)).into()) + Ok(XMLObject::empty_node(context.gc_context, Some(this))) } fn delete(&self, gc_context: MutationContext<'gc, '_>, name: &str) -> bool { diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index e4e20fcc6..57c780ed7 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -88,14 +88,14 @@ impl<'gc> XMLDocument<'gc> { } /// Returns an iterator that yields the document's root nodes. - pub fn roots(&self) -> impl Iterator> { + pub fn roots(self) -> impl Iterator> { self.as_node() .children() .expect("Document root node must always be capable of holding children") } /// Yield the document in node form. - pub fn as_node(&self) -> XMLNode<'gc> { + pub fn as_node(self) -> XMLNode<'gc> { self.0 .read() .root diff --git a/core/src/xml/namespace.rs b/core/src/xml/namespace.rs index a0ff1f8ea..573dd0a80 100644 --- a/core/src/xml/namespace.rs +++ b/core/src/xml/namespace.rs @@ -74,7 +74,7 @@ impl XMLName { if let Some(ref ns) = self.namespace { format!("{}:{}", ns, self.name) } else { - format!("{}", self.name) + self.name.to_string() } } } diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 3d782b3c7..3356a1f2c 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -224,7 +224,7 @@ impl<'gc> XMLNode<'gc> { /// /// Every XML node belongs to a document object (see `XMLDocument`) which /// stores global information about the document, such as namespace URIs. - pub fn document(&self) -> XMLDocument<'gc> { + pub fn document(self) -> XMLDocument<'gc> { match &*self.0.read() { XMLNodeData::Text { document, .. } => *document, XMLNodeData::Comment { document, .. } => *document, @@ -295,7 +295,7 @@ impl<'gc> XMLNode<'gc> { /// /// This is primarily intended to match W3C DOM L1 specifications and /// should not be used in lieu of a proper `match` statement. - pub fn node_type(&self) -> u8 { + pub fn node_type(self) -> u8 { match &*self.0.read() { XMLNodeData::Element { .. } => xml::ELEMENT_NODE, XMLNodeData::DocumentRoot { .. } => xml::ELEMENT_NODE, @@ -305,7 +305,7 @@ impl<'gc> XMLNode<'gc> { } /// Returns the tagname, if the element has one. - pub fn tag_name(&self) -> Option { + pub fn tag_name(self) -> Option { match &*self.0.read() { XMLNodeData::Element { ref tag_name, .. } => Some(tag_name.clone()), _ => None, @@ -313,7 +313,7 @@ impl<'gc> XMLNode<'gc> { } /// Returns the string contents of the node, if the element has them. - pub fn node_value(&self) -> Option { + pub fn node_value(self) -> Option { match &*self.0.read() { XMLNodeData::Text { ref contents, .. } => Some(contents.clone()), XMLNodeData::Comment { ref contents, .. } => Some(contents.clone()), @@ -324,7 +324,7 @@ impl<'gc> XMLNode<'gc> { /// Returns an iterator that yields child nodes. /// /// Yields None if this node cannot accept children. - pub fn children(&self) -> Option>> { + pub fn children(self) -> Option>> { struct ChildIter<'gc> { base: XMLNode<'gc>, index: usize, @@ -362,14 +362,14 @@ impl<'gc> XMLNode<'gc> { match &*self.0.read() { XMLNodeData::Element { .. } | XMLNodeData::DocumentRoot { .. } => { - Some(ChildIter::for_node(*self)) + Some(ChildIter::for_node(self)) } - _ => return None, + _ => None, } } /// Get the already-instantiated script object from the current node. - fn get_script_object(&self) -> Option> { + fn get_script_object(self) -> Option> { match &*self.0.read() { XMLNodeData::Element { script_object, .. } => *script_object, XMLNodeData::Text { script_object, .. } => *script_object, From d058c83ac0c42521f843a83a3dba7b348c7903ff Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 23:04:53 -0500 Subject: [PATCH 15/91] Document roots should accept children. --- core/src/xml/tree.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 3356a1f2c..e083306ac 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -280,6 +280,9 @@ impl<'gc> XMLNode<'gc> { match &mut *self.0.write(mc) { XMLNodeData::Element { ref mut children, .. + } + | XMLNodeData::DocumentRoot { + ref mut children, .. } => { children.push(child); } From bec60acc1e52ac52e0b1582bbc0c9cce5843374a Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 23:24:54 -0500 Subject: [PATCH 16/91] `XML.prototype` should be an `XMLObject` so that instances of `XML` can hold a node --- core/src/avm1/globals/xml.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 740e937ab..f9fb91ab4 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -176,7 +176,7 @@ pub fn create_xml_proto<'gc>( proto: Object<'gc>, _fn_proto: Object<'gc>, ) -> Object<'gc> { - let xml_proto = ScriptObject::object(gc_context, Some(proto)); + let xml_proto = XMLObject::empty_node(gc_context, Some(proto)); xml_proto.into() } From 960e4dad90a6422265161021ea2b3f7621aeba0b Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 23:33:00 -0500 Subject: [PATCH 17/91] Don't cause stack overflow when debug-printing XML nodes. --- core/src/xml/tree.rs | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index e083306ac..1c90eccf9 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -419,42 +419,30 @@ impl<'gc> XMLNode<'gc> { impl<'gc> fmt::Debug for XMLNode<'gc> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &*self.0.read() { - XMLNodeData::Text { - script_object, - document, - parent, - contents, - } => f + XMLNodeData::Text { contents, .. } => f .debug_struct("XMLNodeData::Text") - .field("script_object", script_object) - .field("document", document) - .field("parent", parent) + .field("script_object", &"".to_string()) + .field("document", &"".to_string()) + .field("parent", &"".to_string()) .field("contents", contents) .finish(), - XMLNodeData::Comment { - script_object, - document, - parent, - contents, - } => f + XMLNodeData::Comment { contents, .. } => f .debug_struct("XMLNodeData::Comment") - .field("script_object", script_object) - .field("document", document) - .field("parent", parent) + .field("script_object", &"".to_string()) + .field("document", &"".to_string()) + .field("parent", &"".to_string()) .field("contents", contents) .finish(), XMLNodeData::Element { - script_object, - document, - parent, tag_name, attributes, children, + .. } => f .debug_struct("XMLNodeData::Element") - .field("script_object", script_object) - .field("document", document) - .field("parent", parent) + .field("script_object", &"".to_string()) + .field("document", &"".to_string()) + .field("parent", &"".to_string()) .field("tag_name", tag_name) .field("attributes", attributes) .field("children", children) From e47a1d1e3850f1475077ae99821f9b00c0e46873 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 21 Dec 2019 23:57:51 -0500 Subject: [PATCH 18/91] Fix newly constructed XML trees not actually containing the XML they just parsed. --- core/src/avm1/globals/xml.rs | 19 +++++++------------ core/src/xml/tree.rs | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index f9fb91ab4..9bc7f2763 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -8,7 +8,6 @@ use crate::avm1::xml_object::XMLObject; use crate::avm1::{Avm1, Error, Object, TObject, UpdateContext, Value}; use crate::xml::{XMLDocument, XMLNode}; use gc_arena::MutationContext; -use std::mem::swap; /// XMLNode constructor pub fn xmlnode_constructor<'gc>( @@ -25,12 +24,12 @@ pub fn xmlnode_constructor<'gc>( this.as_xml_node(), ) { (Some(Ok(1)), Some(Ok(ref strval)), Some(ref mut this_node)) => { - let mut xmlelement = XMLNode::new_text(ac.gc_context, strval, blank_document); - swap(&mut xmlelement, this_node); + let xmlelement = XMLNode::new_text(ac.gc_context, strval, blank_document); + this_node.swap(ac.gc_context, xmlelement); } (Some(Ok(3)), Some(Ok(ref strval)), Some(ref mut this_node)) => { - let mut xmlelement = XMLNode::new_element(ac.gc_context, strval, blank_document)?; - swap(&mut xmlelement, this_node); + let xmlelement = XMLNode::new_element(ac.gc_context, strval, blank_document)?; + this_node.swap(ac.gc_context, xmlelement); } //Invalid nodetype ID, string value missing, or not an XMLElement _ => {} @@ -155,13 +154,11 @@ pub fn xml_constructor<'gc>( ) { (Some(Ok(ref string)), Some(ref mut this_node)) => { let xmldoc = XMLDocument::from_str(ac.gc_context, string)?; - - swap(&mut xmldoc.as_node(), this_node); + this_node.swap(ac.gc_context, xmldoc.as_node()); } (None, Some(ref mut this_node)) => { let xmldoc = XMLDocument::new(ac.gc_context); - - swap(&mut xmldoc.as_node(), this_node); + this_node.swap(ac.gc_context, xmldoc.as_node()); } //Non-string argument or not an XML document _ => {} @@ -176,7 +173,5 @@ pub fn create_xml_proto<'gc>( proto: Object<'gc>, _fn_proto: Object<'gc>, ) -> Object<'gc> { - let xml_proto = XMLObject::empty_node(gc_context, Some(proto)); - - xml_proto.into() + XMLObject::empty_node(gc_context, Some(proto)) } diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 1c90eccf9..1815a8dfe 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -9,6 +9,7 @@ use quick_xml::events::{BytesStart, BytesText}; use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt; +use std::mem::swap; /// Represents a node in the XML tree. #[derive(Copy, Clone, Collect)] @@ -414,6 +415,20 @@ impl<'gc> XMLNode<'gc> { object.unwrap() } + + /// Swap the contents of this node with another one. + /// + /// After this function completes, the current `XMLNode` will contain all + /// data present in the `other` node, and vice versa. References to the node + /// within the tree will *not* be updated. + pub fn swap(&mut self, gc_context: MutationContext<'gc, '_>, other: Self) { + if !GcCell::ptr_eq(self.0, other.0) { + swap( + &mut *self.0.write(gc_context), + &mut *other.0.write(gc_context), + ); + } + } } impl<'gc> fmt::Debug for XMLNode<'gc> { From b491dd034e9abf8b5d2cea4d3f518ef3a6193edf Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 22 Dec 2019 00:01:42 -0500 Subject: [PATCH 19/91] Don't overflow stack when debug-printing an entire document, either. --- core/src/xml/tree.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 1815a8dfe..acaadb9a6 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -462,14 +462,10 @@ impl<'gc> fmt::Debug for XMLNode<'gc> { .field("attributes", attributes) .field("children", children) .finish(), - XMLNodeData::DocumentRoot { - script_object, - document, - children, - } => f + XMLNodeData::DocumentRoot { children, .. } => f .debug_struct("XMLNodeData::DocumentRoot") - .field("script_object", script_object) - .field("document", document) + .field("script_object", &"".to_string()) + .field("document", &"".to_string()) .field("children", children) .finish(), } From cfacd397cf146d2b7fa36e927f945acaa1c046e5 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 22 Dec 2019 00:21:02 -0500 Subject: [PATCH 20/91] Most XML properties return `null`, not `undefined`. Furthermore, `prefix` does not distinguish between `` and `<:test>` - they both have a `prefix` of `""`. --- core/src/avm1/globals/xml.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 9bc7f2763..fc5eb03e4 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -54,7 +54,7 @@ pub fn create_xmlnode_proto<'gc>( .as_xml_node() .and_then(|n| n.tag_name()) .map(|n| n.local_name().to_string().into()) - .unwrap_or_else(|| Value::Undefined.into())) + .unwrap_or_else(|| Value::Null.into())) }), None, ReadOnly.into(), @@ -67,7 +67,7 @@ pub fn create_xmlnode_proto<'gc>( .as_xml_node() .and_then(|n| n.tag_name()) .map(|n| n.node_name().into()) - .unwrap_or_else(|| Value::Undefined.into())) + .unwrap_or_else(|| Value::Null.into())) }), None, ReadOnly.into(), @@ -93,7 +93,7 @@ pub fn create_xmlnode_proto<'gc>( .as_xml_node() .and_then(|n| n.node_value()) .map(|n| n.into()) - .unwrap_or_else(|| Value::Undefined.into())) + .unwrap_or_else(|| Value::Null.into())) }), None, ReadOnly.into(), @@ -105,8 +105,12 @@ pub fn create_xmlnode_proto<'gc>( Ok(this .as_xml_node() .and_then(|n| n.tag_name()) - .and_then(|n| n.prefix().map(|n| n.to_string().into())) - .unwrap_or_else(|| Value::Undefined.into())) + .map(|n| { + n.prefix() + .map(|n| n.to_string().into()) + .unwrap_or("".to_string().into()) + }) + .unwrap_or_else(|| Value::Null.into())) }), None, ReadOnly.into(), From f8f569440fa3b2fd950b34d5b0e384c0f8442b53 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 22 Dec 2019 00:21:41 -0500 Subject: [PATCH 21/91] Add very basic XML test --- core/tests/regression_tests.rs | 1 + core/tests/swfs/avm1/xml/output.txt | 15 +++++++++++++++ core/tests/swfs/avm1/xml/test.fla | Bin 0 -> 41472 bytes core/tests/swfs/avm1/xml/test.swf | Bin 0 -> 328 bytes 4 files changed, 16 insertions(+) create mode 100644 core/tests/swfs/avm1/xml/output.txt create mode 100644 core/tests/swfs/avm1/xml/test.fla create mode 100644 core/tests/swfs/avm1/xml/test.swf diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index c796b6f8b..c95e7d824 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -105,6 +105,7 @@ swf_tests! { (strictequals_swf6, "avm1/strictequals_swf6", 1), (global_is_bare, "avm1/global_is_bare", 1), (as2_oop, "avm1/as2_oop", 1), + (xml, "avm1/xml", 1), } #[test] diff --git a/core/tests/swfs/avm1/xml/output.txt b/core/tests/swfs/avm1/xml/output.txt new file mode 100644 index 000000000..609023a0f --- /dev/null +++ b/core/tests/swfs/avm1/xml/output.txt @@ -0,0 +1,15 @@ +1 +null +null +null +null +Children: +Child 0 +1 +test +null +test + +Children: +End children. +End children. diff --git a/core/tests/swfs/avm1/xml/test.fla b/core/tests/swfs/avm1/xml/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..d1abdfdd557419b4996cf7efcd5cbcfd9ae86a85 GIT binary patch literal 41472 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/xml/test.swf b/core/tests/swfs/avm1/xml/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..762049a2b36123142dada31a2ad07e75f335ef2e GIT binary patch literal 328 zcmV-O0k{4`S5pZ00RRAaoMn(tO9DY0hToYTcgNH$AtdPFX%9km4=q7W1DhZsx{0~t zA6QrM4{V>HP97H$g06j-KGQa{>q6EXW}bQHH_!YSAE_>o{{*rGjYFtWr(qag8Q%nW z+u#7+)R7M_GZ}&CO=5WLxhn~G+V8nhHk-@A*hhafb{(P47pQBTOYZ`%^oGk(9F*Y5 z=hd*Q9b4>g3nA`G_4ZAtao-eZdQK@hID?PMQZAO4mGfx9(`^Uqqu(k5S_biPihzL; z4q?YY5}*kf2{2)=kN-A7z{)UuY$B1tQ{q**iva~U?Z5z|#-LF6pqL;?!^C!|B2o-Y zrDc>`qOB{gwSsd-TG4})5fNCh)W?FStHpoWp2{p`h<2^W_)hT^s>y_IAY Date: Sun, 22 Dec 2019 00:26:33 -0500 Subject: [PATCH 22/91] `XMLDocument.roots` is not necessary since you can just get the children of it's node form anyway... --- core/src/xml/document.rs | 7 ------- core/src/xml/tests.rs | 5 ++++- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index 57c780ed7..f47a88c82 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -87,13 +87,6 @@ impl<'gc> XMLDocument<'gc> { Ok(document) } - /// Returns an iterator that yields the document's root nodes. - pub fn roots(self) -> impl Iterator> { - self.as_node() - .children() - .expect("Document root node must always be capable of holding children") - } - /// Yield the document in node form. pub fn as_node(self) -> XMLNode<'gc> { self.0 diff --git a/core/src/xml/tests.rs b/core/src/xml/tests.rs index 6c5eda15c..3d39592ff 100644 --- a/core/src/xml/tests.rs +++ b/core/src/xml/tests.rs @@ -9,7 +9,10 @@ fn parse_single_element() { rootless_arena(|mc| { let xml = XMLDocument::from_str(mc, "").expect("Parsed document"); dbg!(xml); - let mut roots = xml.roots(); + let mut roots = xml + .as_node() + .children() + .expect("Parsed document should be capable of having child nodes"); let root = roots.next().expect("Parsed document should have a root"); assert_eq!(root.node_type(), xml::ELEMENT_NODE); From 7e45dee8cfecbaaba50ae126c79115186f436d94 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 22 Dec 2019 00:27:07 -0500 Subject: [PATCH 23/91] Clippy said so. --- core/src/avm1/globals/xml.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index fc5eb03e4..6c39da260 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -108,7 +108,7 @@ pub fn create_xmlnode_proto<'gc>( .map(|n| { n.prefix() .map(|n| n.to_string().into()) - .unwrap_or("".to_string().into()) + .unwrap_or_else(|| "".to_string().into()) }) .unwrap_or_else(|| Value::Null.into())) }), From 08c0f273a6e71ca29edeff1d3e5c2e0d854d83f3 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 22 Dec 2019 21:00:05 -0700 Subject: [PATCH 24/91] Add sibling links to nodes. --- core/src/xml/tree.rs | 174 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 19 deletions(-) diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index acaadb9a6..543b45571 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -30,6 +30,12 @@ pub enum XMLNodeData<'gc> { /// The parent node of this one. parent: Option>, + /// The previous sibling node to this one. + prev_sibling: Option>, + + /// The next sibling node to this one. + next_sibling: Option>, + /// The string representation of the text. contents: String, }, @@ -45,6 +51,12 @@ pub enum XMLNodeData<'gc> { /// The parent node of this one. parent: Option>, + /// The previous sibling node to this one. + prev_sibling: Option>, + + /// The next sibling node to this one. + next_sibling: Option>, + /// The string representation of the comment. contents: String, }, @@ -64,6 +76,12 @@ pub enum XMLNodeData<'gc> { /// The parent node of this one. parent: Option>, + /// The previous sibling node to this one. + prev_sibling: Option>, + + /// The next sibling node to this one. + next_sibling: Option>, + /// The tag name of this element. tag_name: XMLName, @@ -100,6 +118,8 @@ impl<'gc> XMLNode<'gc> { script_object: None, document, parent: None, + prev_sibling: None, + next_sibling: None, contents: contents.to_string(), }, )) @@ -117,6 +137,8 @@ impl<'gc> XMLNode<'gc> { script_object: None, document, parent: None, + prev_sibling: None, + next_sibling: None, tag_name: XMLName::from_str(element_name)?, attributes: BTreeMap::new(), children: Vec::new(), @@ -164,6 +186,8 @@ impl<'gc> XMLNode<'gc> { script_object: None, document, parent: None, + prev_sibling: None, + next_sibling: None, tag_name, attributes, children, @@ -186,6 +210,8 @@ impl<'gc> XMLNode<'gc> { script_object: None, document, parent: None, + prev_sibling: None, + next_sibling: None, contents: match bt.unescaped()? { Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), @@ -211,6 +237,8 @@ impl<'gc> XMLNode<'gc> { script_object: None, document, parent: None, + prev_sibling: None, + next_sibling: None, contents: match bt.unescaped()? { Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?), Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?), @@ -234,32 +262,140 @@ impl<'gc> XMLNode<'gc> { } } - /// Adopt a child element into the current node. + /// Adopt a new child node into the current node. /// /// This does not add the node to any internal lists; it merely updates the /// child to ensure that it considers this node it's parent. This function - /// should always be called after a child node is added to this one. - pub fn adopt( + /// should always be called after a child node is added to this one. If + /// you adopt a node that is NOT already added to the children list, bad + /// things may happen. + pub fn adopt_child( + &mut self, + mc: MutationContext<'gc, '_>, + mut child: XMLNode<'gc>, + ) -> Result<(), Error> { + { + let mut write = child.0.write(mc); + let (child_document, child_parent) = match &mut *write { + XMLNodeData::Element { + document, parent, .. + } => Ok((document, parent)), + XMLNodeData::Text { + document, parent, .. + } => Ok((document, parent)), + XMLNodeData::Comment { + document, parent, .. + } => Ok((document, parent)), + XMLNodeData::DocumentRoot { .. } => Err("Cannot adopt other document roots"), + }?; + + if let Some(parent) = child_parent { + parent.orphan_child(mc, child)?; + } + + *child_document = self.document(); + *child_parent = Some(*self); + } + child.disown_siblings(mc)?; + Ok(()) + } + + /// Get the previous sibling + pub fn prev_sibling(self) -> Result>, Error> { + match *self.0.read() { + XMLNodeData::Element { prev_sibling, .. } => Ok(prev_sibling), + XMLNodeData::Text { prev_sibling, .. } => Ok(prev_sibling), + XMLNodeData::Comment { prev_sibling, .. } => Ok(prev_sibling), + XMLNodeData::DocumentRoot { .. } => Err("Document roots cannot have siblings".into()), + } + } + + /// Establish a new, previous sibling node. + fn establish_prev_sibling( + &mut self, + mc: MutationContext<'gc, '_>, + new_prev: Option>, + ) -> Result<(), Error> { + match &mut *self.0.write(mc) { + XMLNodeData::Element { prev_sibling, .. } => *prev_sibling = new_prev, + XMLNodeData::Text { prev_sibling, .. } => *prev_sibling = new_prev, + XMLNodeData::Comment { prev_sibling, .. } => *prev_sibling = new_prev, + XMLNodeData::DocumentRoot { .. } => { + return Err("Document roots cannot have siblings".into()) + } + }; + + Ok(()) + } + + /// Get the next sibling + pub fn next_sibling(self) -> Result>, Error> { + match *self.0.read() { + XMLNodeData::Element { next_sibling, .. } => Ok(next_sibling), + XMLNodeData::Text { next_sibling, .. } => Ok(next_sibling), + XMLNodeData::Comment { next_sibling, .. } => Ok(next_sibling), + XMLNodeData::DocumentRoot { .. } => Err("Document roots cannot have siblings".into()), + } + } + + /// Establish a new, next sibling node. + fn establish_next_sibling( + &mut self, + mc: MutationContext<'gc, '_>, + new_next: Option>, + ) -> Result<(), Error> { + match &mut *self.0.write(mc) { + XMLNodeData::Element { next_sibling, .. } => *next_sibling = new_next, + XMLNodeData::Text { next_sibling, .. } => *next_sibling = new_next, + XMLNodeData::Comment { next_sibling, .. } => *next_sibling = new_next, + XMLNodeData::DocumentRoot { .. } => { + return Err("Document roots cannot have siblings".into()) + } + }; + + Ok(()) + } + + /// Remove node from it's current siblings list. + fn disown_siblings(&mut self, mc: MutationContext<'gc, '_>) -> Result<(), Error> { + let old_prev = self.prev_sibling()?; + let old_next = self.next_sibling()?; + + if let Some(mut prev) = old_prev { + prev.establish_next_sibling(mc, old_next)?; + } + + if let Some(mut next) = old_next { + next.establish_prev_sibling(mc, old_prev)?; + } + + Ok(()) + } + + /// Remove node from this node's child list. + fn orphan_child( &mut self, mc: MutationContext<'gc, '_>, child: XMLNode<'gc>, ) -> Result<(), Error> { - let mut write = child.0.write(mc); - let (child_document, child_parent) = match &mut *write { - XMLNodeData::Element { - document, parent, .. - } => Ok((document, parent)), - XMLNodeData::Text { - document, parent, .. - } => Ok((document, parent)), - XMLNodeData::Comment { - document, parent, .. - } => Ok((document, parent)), - XMLNodeData::DocumentRoot { .. } => Err("Cannot adopt other document roots"), - }?; + for (i, other_child) in self + .children() + .ok_or("Cannot orphan child if I have no children")? + .enumerate() + { + if GcCell::ptr_eq(child.0, other_child.0) { + match &mut *self.0.write(mc) { + XMLNodeData::Element { children, .. } => children.remove(i), + XMLNodeData::DocumentRoot { children, .. } => children.remove(i), + XMLNodeData::Text { .. } => return Err("Text node has no child nodes!".into()), + XMLNodeData::Comment { .. } => { + return Err("Comment node has no child nodes!".into()) + } + }; - *child_document = self.document(); - *child_parent = Some(*self); + break; + } + } Ok(()) } @@ -290,7 +426,7 @@ impl<'gc> XMLNode<'gc> { _ => return Err("Not an Element".into()), }; - self.adopt(mc, child)?; + self.adopt_child(mc, child)?; Ok(()) } From 37f6efb753178240da28116e273935a6da1f1613 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Mon, 23 Dec 2019 00:23:10 -0500 Subject: [PATCH 25/91] Expose `appendChild` to ActionScript --- core/src/avm1/globals/xml.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 6c39da260..143de6f02 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -7,6 +7,7 @@ use crate::avm1::script_object::ScriptObject; use crate::avm1::xml_object::XMLObject; use crate::avm1::{Avm1, Error, Object, TObject, UpdateContext, Value}; use crate::xml::{XMLDocument, XMLNode}; +use enumset::EnumSet; use gc_arena::MutationContext; /// XMLNode constructor @@ -38,11 +39,27 @@ pub fn xmlnode_constructor<'gc>( Ok(Value::Undefined.into()) } +pub fn xmlnode_append_child<'gc>( + _avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(mut xmlnode), Some(Ok(Some(child_xmlnode)))) = ( + this.as_xml_node(), + args.get(0).map(|n| n.as_object().map(|n| n.as_xml_node())), + ) { + xmlnode.append_child(ac.gc_context, child_xmlnode)?; + } + + Ok(Value::Undefined.into()) +} + /// Construct the prototype for `XMLNode`. pub fn create_xmlnode_proto<'gc>( gc_context: MutationContext<'gc, '_>, proto: Object<'gc>, - _fn_proto: Object<'gc>, + fn_proto: Object<'gc>, ) -> Object<'gc> { let xmlnode_proto = XMLObject::empty_node(gc_context, Some(proto)); @@ -141,6 +158,16 @@ pub fn create_xmlnode_proto<'gc>( None, ReadOnly.into(), ); + xmlnode_proto + .as_script_object() + .unwrap() + .force_set_function( + "appendChild", + xmlnode_append_child, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); xmlnode_proto } From 55fa6ef09b0967c0ff97101b064731994838253c Mon Sep 17 00:00:00 2001 From: David Wendt Date: Tue, 24 Dec 2019 23:33:53 -0500 Subject: [PATCH 26/91] Add node cloning support --- core/src/avm1/globals/xml.rs | 31 ++++++++++++++++ core/src/xml/document.rs | 37 +++++++++++++++++++ core/src/xml/tree.rs | 70 ++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 143de6f02..04494259d 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -55,6 +55,27 @@ pub fn xmlnode_append_child<'gc>( Ok(Value::Undefined.into()) } +pub fn xmlnode_clone_node<'gc>( + avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(xmlnode), Some(deep)) = ( + this.as_xml_node(), + args.get(0).map(|v| v.as_bool(avm.current_swf_version())), + ) { + let mut clone_node = xmlnode.duplicate(ac.gc_context, deep); + + return Ok(Value::Object( + clone_node.script_object(ac.gc_context, Some(avm.prototypes.xml_node)), + ) + .into()); + } + + Ok(Value::Undefined.into()) +} + /// Construct the prototype for `XMLNode`. pub fn create_xmlnode_proto<'gc>( gc_context: MutationContext<'gc, '_>, @@ -168,6 +189,16 @@ pub fn create_xmlnode_proto<'gc>( EnumSet::empty(), Some(fn_proto), ); + xmlnode_proto + .as_script_object() + .unwrap() + .force_set_function( + "cloneNode", + xmlnode_clone_node, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); xmlnode_proto } diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index f47a88c82..174a64436 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -88,12 +88,49 @@ impl<'gc> XMLDocument<'gc> { } /// Yield the document in node form. + /// + /// If the document does not have a node, then this function will panic. pub fn as_node(self) -> XMLNode<'gc> { self.0 .read() .root .expect("Document must always have a root node") } + + /// Create a duplicate copy of this document. + /// + /// The contents of the document will not be duplicated. This results in a + /// rootless document that is not safe to use without first linking another + /// root node into it. (See `link_root_node`.) + pub fn duplicate(self, gc_context: MutationContext<'gc, '_>) -> Self { + Self(GcCell::allocate(gc_context, XMLDocumentData { root: None })) + } + + /// Set the root node of the document, if possible. + /// + /// If the proposed root is not an `XMLNode::DocumentRoot`, then a fresh + /// document root node will be created and the root will be adopted into + /// it. + /// + /// If the document already has a root node, nothing happens. + pub fn link_root_node( + &mut self, + gc_context: MutationContext<'gc, '_>, + proposed_root: XMLNode<'gc>, + ) { + match ( + &mut *self.0.write(gc_context), + proposed_root.is_document_root(), + ) { + (XMLDocumentData { root }, true) if root.is_none() => { + *root = Some(proposed_root); + } + (XMLDocumentData { root }, false) if root.is_none() => { + *root = Some(XMLNode::new_document_root(gc_context, *self)); + } + _ => {} + } + } } impl<'gc> fmt::Debug for XMLDocument<'gc> { diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 543b45571..d1a229a04 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -565,6 +565,76 @@ impl<'gc> XMLNode<'gc> { ); } } + + /// Check if this XML node constitutes the root of a whole document. + pub fn is_document_root(self) -> bool { + match &*self.0.read() { + XMLNodeData::DocumentRoot { .. } => true, + _ => false, + } + } + + /// Create a duplicate copy of this node. + /// + /// If the `deep` flag is set true, then the entire node tree will be + /// cloned. + pub fn duplicate(self, gc_context: MutationContext<'gc, '_>, deep: bool) -> XMLNode<'gc> { + let mut document = self.document().duplicate(gc_context); + let mut clone = XMLNode(GcCell::allocate( + gc_context, + match &*self.0.read() { + XMLNodeData::Text { contents, .. } => XMLNodeData::Text { + script_object: None, + document, + parent: None, + prev_sibling: None, + next_sibling: None, + contents: contents.to_string(), + }, + XMLNodeData::Comment { contents, .. } => XMLNodeData::Comment { + script_object: None, + document, + parent: None, + prev_sibling: None, + next_sibling: None, + contents: contents.to_string(), + }, + XMLNodeData::Element { + tag_name, + attributes, + .. + } => XMLNodeData::Element { + script_object: None, + document, + parent: None, + prev_sibling: None, + next_sibling: None, + tag_name: tag_name.clone(), + attributes: attributes.clone(), + children: Vec::new(), + }, + XMLNodeData::DocumentRoot { .. } => XMLNodeData::DocumentRoot { + script_object: None, + document, + children: Vec::new(), + }, + }, + )); + + document.link_root_node(gc_context, clone); + + if deep { + if let Some(children) = self.children() { + for child in children { + clone + .append_child(gc_context, child.duplicate(gc_context, deep)) + .expect("If I can see my children then my clone should accept children"); + } + } + } + + clone + } } impl<'gc> fmt::Debug for XMLNode<'gc> { From fe7d2b5173ceaeac21a1cbfd362285a5c6ff3ec1 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 25 Dec 2019 00:27:51 -0500 Subject: [PATCH 27/91] Extremely WIP impl of `lookup_uri_for_namespace` --- core/src/xml/document.rs | 3 +-- core/src/xml/namespace.rs | 8 ++++++++ core/src/xml/tree.rs | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/src/xml/document.rs b/core/src/xml/document.rs index 174a64436..87730fbf4 100644 --- a/core/src/xml/document.rs +++ b/core/src/xml/document.rs @@ -1,7 +1,6 @@ //! XML Document -use crate::xml::Error; -use crate::xml::XMLNode; +use crate::xml::{Error, XMLNode}; use gc_arena::{Collect, GcCell, MutationContext}; use quick_xml::events::Event; use quick_xml::Reader; diff --git a/core/src/xml/namespace.rs b/core/src/xml/namespace.rs index 573dd0a80..162753e9d 100644 --- a/core/src/xml/namespace.rs +++ b/core/src/xml/namespace.rs @@ -26,6 +26,14 @@ pub struct XMLName { } impl XMLName { + /// Construct an XML name from it's parts (name and namespace). + pub fn from_parts(namespace: Option<&str>, name: &str) -> Self { + XMLName { + namespace: namespace.map(|s| s.to_string()), + name: name.to_string(), + } + } + pub fn from_bytes(bytes: &[u8]) -> Result { Self::from_bytes_cow(Cow::Borrowed(bytes)) } diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index d1a229a04..69ce1cb9f 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -635,6 +635,25 @@ impl<'gc> XMLNode<'gc> { clone } + + /// Retrieve the value of a single attribute on this node. + /// + /// If the node does not contain attributes, then this function always + /// yields None. + pub fn attribute_value(self, name: &XMLName) -> Option { + match &*self.0.read() { + XMLNodeData::Element { attributes, .. } => attributes.get(name).cloned(), + _ => None, + } + } + + /// Look up the URI for the namespace in a given `XMLName`. + /// + /// XML namespaces are determined by `xmlns:` namespace attributes on the + /// current node, or it's parent. + pub fn lookup_uri_for_namespace(self, namespace: &str) -> Option { + self.attribute_value(&XMLName::from_parts(Some("xmlns"), namespace)) + } } impl<'gc> fmt::Debug for XMLNode<'gc> { From 69a1ab16498fd9c7fe6b09b67286a66365dc7779 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 25 Dec 2019 17:39:20 -0500 Subject: [PATCH 28/91] Expose namespace prefix and URI lookups to ActionScript. Also, fix the previous commit's half-assed impl. --- core/src/avm1/globals/xml.rs | 60 +++++++++++++++++++++++++++++ core/src/xml/tree.rs | 75 ++++++++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 04494259d..5ea7fe376 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -76,6 +76,46 @@ pub fn xmlnode_clone_node<'gc>( Ok(Value::Undefined.into()) } +pub fn xmlnode_get_namespace_for_prefix<'gc>( + avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(xmlnode), Some(prefix_string)) = ( + this.as_xml_node(), + args.get(0).map(|v| v.clone().coerce_to_string(avm, ac)), + ) { + if let Some(uri) = xmlnode.lookup_uri_for_namespace(&prefix_string?) { + Ok(uri.into()) + } else { + Ok(Value::Null.into()) + } + } else { + Ok(Value::Undefined.into()) + } +} + +pub fn xmlnode_get_prefix_for_namespace<'gc>( + avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(xmlnode), Some(uri_string)) = ( + this.as_xml_node(), + args.get(0).map(|v| v.clone().coerce_to_string(avm, ac)), + ) { + if let Some(prefix) = xmlnode.lookup_namespace_for_uri(&uri_string?) { + Ok(prefix.into()) + } else { + Ok(Value::Null.into()) + } + } else { + Ok(Value::Undefined.into()) + } +} + /// Construct the prototype for `XMLNode`. pub fn create_xmlnode_proto<'gc>( gc_context: MutationContext<'gc, '_>, @@ -199,6 +239,26 @@ pub fn create_xmlnode_proto<'gc>( EnumSet::empty(), Some(fn_proto), ); + xmlnode_proto + .as_script_object() + .unwrap() + .force_set_function( + "getNamespaceForPrefix", + xmlnode_get_namespace_for_prefix, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + xmlnode_proto + .as_script_object() + .unwrap() + .force_set_function( + "getPrefixForNamespace", + xmlnode_get_prefix_for_namespace, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); xmlnode_proto } diff --git a/core/src/xml/tree.rs b/core/src/xml/tree.rs index 69ce1cb9f..a32325725 100644 --- a/core/src/xml/tree.rs +++ b/core/src/xml/tree.rs @@ -300,7 +300,21 @@ impl<'gc> XMLNode<'gc> { Ok(()) } - /// Get the previous sibling + /// Get the parent, if this node has one. + /// + /// If the node cannot have a parent, then this function yields Err. + pub fn parent(self) -> Result>, Error> { + match *self.0.read() { + XMLNodeData::Element { parent, .. } => Ok(parent), + XMLNodeData::Text { parent, .. } => Ok(parent), + XMLNodeData::Comment { parent, .. } => Ok(parent), + XMLNodeData::DocumentRoot { .. } => Err("Document roots cannot have parents".into()), + } + } + + /// Get the previous sibling, if this node has one. + /// + /// If the node cannot have siblings, then this function yields Err. pub fn prev_sibling(self) -> Result>, Error> { match *self.0.read() { XMLNodeData::Element { prev_sibling, .. } => Ok(prev_sibling), @@ -328,7 +342,9 @@ impl<'gc> XMLNode<'gc> { Ok(()) } - /// Get the next sibling + /// Get the next sibling, if this node has one. + /// + /// If the node cannot have siblings, then this function yields Err. pub fn next_sibling(self) -> Result>, Error> { match *self.0.read() { XMLNodeData::Element { next_sibling, .. } => Ok(next_sibling), @@ -647,12 +663,63 @@ impl<'gc> XMLNode<'gc> { } } - /// Look up the URI for the namespace in a given `XMLName`. + /// Look up the URI for the given namespace. /// /// XML namespaces are determined by `xmlns:` namespace attributes on the /// current node, or it's parent. pub fn lookup_uri_for_namespace(self, namespace: &str) -> Option { - self.attribute_value(&XMLName::from_parts(Some("xmlns"), namespace)) + if let Some(url) = self.attribute_value(&XMLName::from_parts(Some("xmlns"), namespace)) { + Some(url) + } else if let Ok(Some(parent)) = self.parent() { + parent.lookup_uri_for_namespace(namespace) + } else { + None + } + } + + /// Retrieve the first attribute key set to a given value, if any. + /// + /// If the node does not contain attributes, then this function always + /// yields None. + /// + /// You may restrict your value search to specific namespaces by setting + /// `within_namespace`. If it is set to `None`, then any namespace's + /// attributes may satisfy the search. It is it set to `""`, then + /// the default namespace will be searched. + pub fn value_attribute(self, value: &str, within_namespace: Option<&str>) -> Option { + match &*self.0.read() { + XMLNodeData::Element { attributes, .. } => { + for (attr, attr_value) in attributes.iter() { + if let Some(namespace) = within_namespace { + if attr.prefix().unwrap_or("") == namespace && value == attr_value { + return Some(attr.clone()); + } + } else if value == attr_value { + return Some(attr.clone()); + } + } + + None + } + _ => None, + } + } + + /// Look up the namespace for the given URI. + /// + /// XML namespaces are determined by `xmlns:` namespace attributes on the + /// current node, or it's parent. + /// + /// If there are multiple namespaces that match the URI, the first + /// mentioned on the closest node will be returned. + pub fn lookup_namespace_for_uri(self, uri: &str) -> Option { + if let Some(xname) = self.value_attribute(uri, Some("xmlns")) { + Some(xname.local_name().to_string()) + } else if let Ok(Some(parent)) = self.parent() { + parent.lookup_namespace_for_uri(uri) + } else { + None + } } } From 8e33566d07d87a41da9ab71727ca07a2944ae7bc Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 25 Dec 2019 17:42:38 -0500 Subject: [PATCH 29/91] Add namespacing test --- core/tests/regression_tests.rs | 1 + core/tests/swfs/avm1/xml_namespaces/output.txt | 12 ++++++++++++ core/tests/swfs/avm1/xml_namespaces/test.fla | Bin 0 -> 42496 bytes core/tests/swfs/avm1/xml_namespaces/test.swf | Bin 0 -> 267 bytes 4 files changed, 13 insertions(+) create mode 100644 core/tests/swfs/avm1/xml_namespaces/output.txt create mode 100644 core/tests/swfs/avm1/xml_namespaces/test.fla create mode 100644 core/tests/swfs/avm1/xml_namespaces/test.swf diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index c95e7d824..1d3d83dfd 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -106,6 +106,7 @@ swf_tests! { (global_is_bare, "avm1/global_is_bare", 1), (as2_oop, "avm1/as2_oop", 1), (xml, "avm1/xml", 1), + (xml_namespaces, "avm1/xml_namespaces", 1), } #[test] diff --git a/core/tests/swfs/avm1/xml_namespaces/output.txt b/core/tests/swfs/avm1/xml_namespaces/output.txt new file mode 100644 index 000000000..39167fa44 --- /dev/null +++ b/core/tests/swfs/avm1/xml_namespaces/output.txt @@ -0,0 +1,12 @@ +null +null +null +null +https://example.com +test +null +null +https://example.com/2 +test +https://example.com/2 +test diff --git a/core/tests/swfs/avm1/xml_namespaces/test.fla b/core/tests/swfs/avm1/xml_namespaces/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..925adc1639121d850443104ddd547af402178999 GIT binary patch literal 42496 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