avm1: Merge #205, XML support

Support for AVM1 XML and XMLNode objects.
This commit is contained in:
Mike Welsh 2020-01-05 12:57:38 -08:00 committed by GitHub
commit bcc51f2dd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 3911 additions and 2 deletions

10
Cargo.lock generated
View File

@ -1432,6 +1432,14 @@ name = "quick-error"
version = "1.2.2" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "quote" name = "quote"
version = "0.6.13" version = "0.6.13"
@ -1575,6 +1583,7 @@ dependencies = [
"minimp3 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ruffle_macros 0.1.0", "ruffle_macros 0.1.0",
"smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "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 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 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-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 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 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" "checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412"

View File

@ -19,6 +19,7 @@ swf = { path = "../swf" }
enumset = "0.4.2" enumset = "0.4.2"
smallvec = "1.1.0" smallvec = "1.1.0"
num_enum = "0.4.2" num_enum = "0.4.2"
quick-xml = "0.17.2"
[dependencies.jpeg-decoder] [dependencies.jpeg-decoder]
version = "0.1.18" version = "0.1.18"

View File

@ -33,6 +33,9 @@ mod sound_object;
mod stage_object; mod stage_object;
mod super_object; mod super_object;
mod value; mod value;
pub mod xml_attributes_object;
pub mod xml_idmap_object;
pub mod xml_object;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View File

@ -20,6 +20,7 @@ mod object;
mod sound; mod sound;
mod stage; mod stage;
pub(crate) mod text_field; pub(crate) mod text_field;
mod xml;
#[allow(non_snake_case, unused_must_use)] //can't use errors yet #[allow(non_snake_case, unused_must_use)] //can't use errors yet
pub fn getURL<'a, 'gc>( pub fn getURL<'a, 'gc>(
@ -138,6 +139,7 @@ pub struct SystemPrototypes<'gc> {
pub sound: Object<'gc>, pub sound: Object<'gc>,
pub text_field: Object<'gc>, pub text_field: Object<'gc>,
pub array: Object<'gc>, pub array: Object<'gc>,
pub xml_node: Object<'gc>,
} }
unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> { unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> {
@ -149,6 +151,7 @@ unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> {
self.sound.trace(cc); self.sound.trace(cc);
self.text_field.trace(cc); self.text_field.trace(cc);
self.array.trace(cc); self.array.trace(cc);
self.xml_node.trace(cc);
} }
} }
@ -172,6 +175,10 @@ pub fn create_globals<'gc>(
let array_proto: Object<'gc> = array::create_proto(gc_context, object_proto, function_proto); 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 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);
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 //TODO: These need to be constructors and should also set `.prototype` on each one
let object = ScriptObject::function( let object = ScriptObject::function(
@ -217,6 +224,18 @@ pub fn create_globals<'gc>(
Some(function_proto), Some(function_proto),
Some(array_proto), Some(array_proto),
); );
let xmlnode = ScriptObject::function(
gc_context,
Executable::Native(xml::xmlnode_constructor),
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)); let listeners = SystemListeners::new(gc_context, Some(array_proto));
@ -228,6 +247,8 @@ pub fn create_globals<'gc>(
globals.define_value(gc_context, "MovieClip", movie_clip.into(), EnumSet::empty()); 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, "Sound", sound.into(), EnumSet::empty());
globals.define_value(gc_context, "TextField", text_field.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( globals.force_set_function(
"Number", "Number",
number, number,
@ -345,6 +366,7 @@ pub fn create_globals<'gc>(
sound: sound_proto, sound: sound_proto,
text_field: text_field_proto, text_field: text_field_proto,
array: array_proto, array: array_proto,
xml_node: xmlnode_proto,
}, },
globals.into(), globals.into(),
listeners, listeners,

View File

@ -0,0 +1,932 @@
//! 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;
use crate::xml::{XMLDocument, XMLNode};
use enumset::EnumSet;
use gc_arena::MutationContext;
use quick_xml::Error as ParseError;
pub const XML_NO_ERROR: f64 = 0.0;
#[allow(dead_code)]
pub const XML_CDATA_NOT_TERMINATED: f64 = -2.0;
pub const XML_DECL_NOT_TERMINATED: f64 = -3.0;
#[allow(dead_code)]
pub const XML_DOCTYPE_NOT_TERMINATED: f64 = -4.0;
#[allow(dead_code)]
pub const XML_COMMENT_NOT_TERMINATED: f64 = -5.0;
pub const XML_ELEMENT_MALFORMED: f64 = -6.0;
pub const XML_OUT_OF_MEMORY: f64 = -7.0;
pub const XML_ATTRIBUTE_NOT_TERMINATED: f64 = -8.0;
#[allow(dead_code)]
pub const XML_MISMATCHED_START: f64 = -9.0;
pub const XML_MISMATCHED_END: f64 = -10.0;
/// Returns true if a particular node can or cannot be exposed to AVM1.
///
/// Our internal XML tree representation supports node types that AVM1 XML did
/// not. Those nodes are filtered from all attributes that return XML nodes to
/// act as if those nodes did not exist. For example, `prevSibling` skips
/// past incompatible nodes, etc.
fn is_as2_compatible(node: XMLNode<'_>) -> bool {
node.is_document_root() || node.is_element() || node.is_text()
}
/// XMLNode constructor
pub fn xmlnode_constructor<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, 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_element(ac.gc_context, strval, blank_document)?;
xmlelement.introduce_script_object(ac.gc_context, this);
this_node.swap(ac.gc_context, xmlelement);
}
(Some(Ok(3)), Some(Ok(ref strval)), Some(ref mut this_node)) => {
let mut xmlelement = XMLNode::new_text(ac.gc_context, strval, blank_document);
xmlelement.introduce_script_object(ac.gc_context, this);
this_node.swap(ac.gc_context, xmlelement);
}
//Invalid nodetype ID, string value missing, or not an XMLElement
_ => {}
};
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<ReturnValue<'gc>, 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())),
) {
if let Ok(None) = child_xmlnode.parent() {
let position = xmlnode.children_len();
xmlnode.insert_child(ac.gc_context, position, child_xmlnode)?;
}
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_insert_before<'gc>(
_avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let (Some(mut xmlnode), Some(Ok(Some(child_xmlnode))), Some(Ok(Some(insertpoint_xmlnode)))) = (
this.as_xml_node(),
args.get(0).map(|n| n.as_object().map(|n| n.as_xml_node())),
args.get(1).map(|n| n.as_object().map(|n| n.as_xml_node())),
) {
if let Ok(None) = child_xmlnode.parent() {
if let Some(position) = xmlnode.child_position(insertpoint_xmlnode) {
xmlnode.insert_child(ac.gc_context, position, child_xmlnode)?;
}
}
}
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<ReturnValue<'gc>, Error> {
if let (Some(xmlnode), deep) = (
this.as_xml_node(),
args.get(0)
.map(|v| v.as_bool(avm.current_swf_version()))
.unwrap_or(false),
) {
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())
}
pub fn xmlnode_get_namespace_for_prefix<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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())
}
}
pub fn xmlnode_has_child_nodes<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(xmlnode) = this.as_xml_node() {
Ok((xmlnode.children_len() > 0).into())
} else {
Ok(Value::Undefined.into())
}
}
pub fn xmlnode_remove_node<'gc>(
_avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
if let Ok(Some(mut parent)) = node.parent() {
if let Err(e) = parent.remove_child(ac.gc_context, node) {
log::warn!("Error in XML.removeNode: {}", e);
}
}
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_to_string<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
let result = node.into_string(&mut is_as2_compatible);
return Ok(result
.unwrap_or_else(|e| {
log::warn!("XMLNode toString failed: {}", e);
"".to_string()
})
.into());
}
Ok("".to_string().into())
}
pub fn xmlnode_local_name<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(this
.as_xml_node()
.and_then(|n| n.tag_name())
.map(|n| n.local_name().to_string().into())
.unwrap_or_else(|| Value::Null.into()))
}
pub fn xmlnode_node_name<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(this
.as_xml_node()
.and_then(|n| n.tag_name())
.map(|n| n.node_name().into())
.unwrap_or_else(|| Value::Null.into()))
}
pub fn xmlnode_node_type<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(this
.as_xml_node()
.map(|n| {
match n.node_type() {
xml::DOCUMENT_NODE => xml::ELEMENT_NODE,
xml::DOCUMENT_TYPE_NODE => xml::TEXT_NODE,
xml::COMMENT_NODE => xml::TEXT_NODE,
n => n,
}
.into()
})
.unwrap_or_else(|| Value::Undefined.into()))
}
pub fn xmlnode_node_value<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(this
.as_xml_node()
.and_then(|n| n.node_value())
.map(|n| n.into())
.unwrap_or_else(|| Value::Null.into()))
}
pub fn xmlnode_prefix<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
Ok(this
.as_xml_node()
.and_then(|n| n.tag_name())
.map(|n| {
n.prefix()
.map(|n| n.to_string().into())
.unwrap_or_else(|| "".to_string().into())
})
.unwrap_or_else(|| Value::Null.into()))
}
pub fn xmlnode_child_nodes<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
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() {
let mut compatible_nodes = 0;
for mut child in children {
if !is_as2_compatible(child) {
continue;
}
array.set_array_element(
compatible_nodes as usize,
child
.script_object(ac.gc_context, Some(avm.prototypes.xml_node))
.into(),
ac.gc_context,
);
compatible_nodes += 1;
}
}
return Ok(array.into());
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_first_child<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
if let Some(mut children) = node.children() {
return Ok(children
.next()
.map(|mut child| {
child
.script_object(ac.gc_context, Some(avm.prototypes.xml_node))
.into()
})
.unwrap_or_else(|| Value::Null.into()));
}
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_last_child<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
if let Some(mut children) = node.children() {
return Ok(children
.next_back()
.map(|mut child| {
child
.script_object(ac.gc_context, Some(avm.prototypes.xml_node))
.into()
})
.unwrap_or_else(|| Value::Null.into()));
}
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_parent_node<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
return Ok(node
.parent()
.unwrap_or(None)
.map(|mut parent| {
parent
.script_object(ac.gc_context, Some(avm.prototypes.xml_node))
.into()
})
.unwrap_or_else(|| Value::Null.into()));
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_previous_sibling<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
let mut prev = node.prev_sibling().unwrap_or(None);
while let Some(my_prev) = prev {
if is_as2_compatible(my_prev) {
break;
}
prev = my_prev.prev_sibling().unwrap_or(None);
}
return Ok(prev
.map(|mut prev| {
prev.script_object(ac.gc_context, Some(avm.prototypes.xml_node))
.into()
})
.unwrap_or_else(|| Value::Null.into()));
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_next_sibling<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
let mut next = node.next_sibling().unwrap_or(None);
while let Some(my_next) = next {
if is_as2_compatible(my_next) {
break;
}
next = my_next.next_sibling().unwrap_or(None);
}
return Ok(next
.map(|mut next| {
next.script_object(ac.gc_context, Some(avm.prototypes.xml_node))
.into()
})
.unwrap_or_else(|| Value::Null.into()));
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_attributes<'gc>(
_avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(mut node) = this.as_xml_node() {
return Ok(node
.attribute_script_object(ac.gc_context)
.map(|o| o.into())
.unwrap_or_else(|| Value::Undefined.into()));
}
Ok(Value::Undefined.into())
}
pub fn xmlnode_namespace_uri<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
if let Some(name) = node.tag_name() {
return Ok(node
.lookup_uri_for_namespace(name.prefix().unwrap_or(""))
.map(|s| s.into())
.unwrap_or_else(|| "".into()));
}
return Ok(Value::Null.into());
}
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 = XMLObject::empty_node(gc_context, Some(proto));
xmlnode_proto.add_property(
gc_context,
"localName",
Executable::Native(xmlnode_local_name),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"nodeName",
Executable::Native(xmlnode_node_name),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"nodeType",
Executable::Native(xmlnode_node_type),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"nodeValue",
Executable::Native(xmlnode_node_value),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"prefix",
Executable::Native(xmlnode_prefix),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"childNodes",
Executable::Native(xmlnode_child_nodes),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"firstChild",
Executable::Native(xmlnode_first_child),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"lastChild",
Executable::Native(xmlnode_last_child),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"parentNode",
Executable::Native(xmlnode_parent_node),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"previousSibling",
Executable::Native(xmlnode_previous_sibling),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"nextSibling",
Executable::Native(xmlnode_next_sibling),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"attributes",
Executable::Native(xmlnode_attributes),
None,
ReadOnly.into(),
);
xmlnode_proto.add_property(
gc_context,
"namespaceURI",
Executable::Native(xmlnode_namespace_uri),
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
.as_script_object()
.unwrap()
.force_set_function(
"insertBefore",
xmlnode_insert_before,
gc_context,
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
.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
.as_script_object()
.unwrap()
.force_set_function(
"hasChildNodes",
xmlnode_has_child_nodes,
gc_context,
EnumSet::empty(),
Some(fn_proto),
);
xmlnode_proto
.as_script_object()
.unwrap()
.force_set_function(
"removeNode",
xmlnode_remove_node,
gc_context,
EnumSet::empty(),
Some(fn_proto),
);
xmlnode_proto
.as_script_object()
.unwrap()
.force_set_function(
"toString",
xmlnode_to_string,
gc_context,
EnumSet::empty(),
Some(fn_proto),
);
xmlnode_proto
}
/// XML (document) constructor
pub fn xml_constructor<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
match (
args.get(0).map(|v| v.clone().coerce_to_string(avm, ac)),
this.as_xml_node(),
) {
(Some(Ok(ref string)), Some(ref mut this_node)) => {
let xmldoc = XMLDocument::new(ac.gc_context);
let mut xmlnode = xmldoc.as_node();
xmlnode.introduce_script_object(ac.gc_context, this);
this_node.swap(ac.gc_context, xmlnode);
this_node.replace_with_str(ac.gc_context, string)?;
}
(None, Some(ref mut this_node)) => {
let xmldoc = XMLDocument::new(ac.gc_context);
let mut xmlnode = xmldoc.as_node();
xmlnode.introduce_script_object(ac.gc_context, this);
this_node.swap(ac.gc_context, xmlnode);
}
//Non-string argument or not an XML document
_ => {}
};
Ok(Value::Undefined.into())
}
pub fn xml_create_element<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
let document = if let Some(node) = this.as_xml_node() {
node.document()
} else {
XMLDocument::new(ac.gc_context)
};
let nodename = args
.get(0)
.map(|v| v.clone().coerce_to_string(avm, ac).unwrap_or_default())
.unwrap_or_default();
let mut xml_node = XMLNode::new_element(ac.gc_context, &nodename, document)?;
let object = XMLObject::from_xml_node(ac.gc_context, xml_node, Some(avm.prototypes().xml_node));
xml_node.introduce_script_object(ac.gc_context, object);
Ok(object.into())
}
pub fn xml_create_text_node<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
let document = if let Some(node) = this.as_xml_node() {
node.document()
} else {
XMLDocument::new(ac.gc_context)
};
let text_node = args
.get(0)
.map(|v| v.clone().coerce_to_string(avm, ac).unwrap_or_default())
.unwrap_or_default();
let mut xml_node = XMLNode::new_text(ac.gc_context, &text_node, document);
let object = XMLObject::from_xml_node(ac.gc_context, xml_node, Some(avm.prototypes().xml_node));
xml_node.introduce_script_object(ac.gc_context, object);
Ok(object.into())
}
pub fn xml_parse_xml<'gc>(
avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(mut node) = this.as_xml_node() {
let xmlstring =
if let Some(Ok(xmlstring)) = args.get(0).map(|s| s.clone().coerce_to_string(avm, ac)) {
xmlstring
} else {
"".to_string()
};
if let Some(children) = node.children() {
for child in children.rev() {
let result = node.remove_child(ac.gc_context, child);
if let Err(e) = result {
log::warn!("XML.parseXML: Error removing node contents: {}", e);
return Ok(Value::Undefined.into());
}
}
}
let result = node.replace_with_str(ac.gc_context, &xmlstring);
if let Err(e) = result {
log::warn!("XML parsing error: {}", e);
}
}
Ok(Value::Undefined.into())
}
pub fn xml_doc_type_decl<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
if let Some(doctype) = node.document().doctype() {
let result = doctype.into_string(&mut |_| true);
return Ok(result
.unwrap_or_else(|e| {
log::warn!("Error occured when serializing DOCTYPE: {}", e);
"".to_string()
})
.into());
}
}
Ok(Value::Undefined.into())
}
pub fn xml_xml_decl<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
let result = node.document().xmldecl_string();
if let Err(e) = result {
log::warn!("Could not generate XML declaration for document: {}", e);
} else if let Ok(Some(result_str)) = result {
return Ok(result_str.into());
}
}
Ok(Value::Undefined.into())
}
pub fn xml_id_map<'gc>(
_avm: &mut Avm1<'gc>,
ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
return Ok(node.document().idmap_script_object(ac.gc_context).into());
}
Ok(Value::Undefined.into())
}
pub fn xml_status<'gc>(
_avm: &mut Avm1<'gc>,
_ac: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> {
if let Some(node) = this.as_xml_node() {
return match node.document().last_parse_error() {
None => Ok(XML_NO_ERROR.into()),
Some(err) => match err.ref_error() {
ParseError::UnexpectedEof(_) => Ok(Value::Number(XML_ELEMENT_MALFORMED).into()),
ParseError::EndEventMismatch { .. } => Ok(Value::Number(XML_MISMATCHED_END).into()),
ParseError::XmlDeclWithoutVersion(_) => {
Ok(Value::Number(XML_DECL_NOT_TERMINATED).into())
}
ParseError::NameWithQuote(_) => Ok(Value::Number(XML_ELEMENT_MALFORMED).into()),
ParseError::NoEqAfterName(_) => Ok(Value::Number(XML_ELEMENT_MALFORMED).into()),
ParseError::UnquotedValue(_) => {
Ok(Value::Number(XML_ATTRIBUTE_NOT_TERMINATED).into())
}
ParseError::DuplicatedAttribute(_, _) => {
Ok(Value::Number(XML_ELEMENT_MALFORMED).into())
}
_ => Ok(Value::Number(XML_OUT_OF_MEMORY).into()), //Not accounted for:
//ParseError::UnexpectedToken(_)
//ParseError::UnexpectedBang
//ParseError::TextNotFound
//ParseError::EscapeError(_)
},
};
}
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 = XMLObject::empty_node(gc_context, Some(proto));
xml_proto.add_property(
gc_context,
"docTypeDecl",
Executable::Native(xml_doc_type_decl),
None,
ReadOnly.into(),
);
xml_proto.add_property(
gc_context,
"xmlDecl",
Executable::Native(xml_xml_decl),
None,
ReadOnly.into(),
);
xml_proto.add_property(
gc_context,
"idMap",
Executable::Native(xml_id_map),
None,
ReadOnly.into(),
);
xml_proto.add_property(
gc_context,
"status",
Executable::Native(xml_status),
None,
ReadOnly.into(),
);
xml_proto.as_script_object().unwrap().force_set_function(
"createElement",
xml_create_element,
gc_context,
EnumSet::empty(),
Some(fn_proto),
);
xml_proto.as_script_object().unwrap().force_set_function(
"createTextNode",
xml_create_text_node,
gc_context,
EnumSet::empty(),
Some(fn_proto),
);
xml_proto.as_script_object().unwrap().force_set_function(
"parseXML",
xml_parse_xml,
gc_context,
EnumSet::empty(),
Some(fn_proto),
);
xml_proto
}

View File

@ -4,8 +4,12 @@ use crate::avm1::function::Executable;
use crate::avm1::property::Attribute; use crate::avm1::property::Attribute;
use crate::avm1::return_value::ReturnValue; use crate::avm1::return_value::ReturnValue;
use crate::avm1::super_object::SuperObject; use crate::avm1::super_object::SuperObject;
use crate::avm1::xml_attributes_object::XMLAttributesObject;
use crate::avm1::xml_idmap_object::XMLIDMapObject;
use crate::avm1::xml_object::XMLObject;
use crate::avm1::{Avm1, Error, ScriptObject, SoundObject, StageObject, UpdateContext, Value}; use crate::avm1::{Avm1, Error, ScriptObject, SoundObject, StageObject, UpdateContext, Value};
use crate::display_object::DisplayObject; use crate::display_object::DisplayObject;
use crate::xml::XMLNode;
use enumset::EnumSet; use enumset::EnumSet;
use gc_arena::{Collect, MutationContext}; use gc_arena::{Collect, MutationContext};
use ruffle_macros::enum_trait_object; use ruffle_macros::enum_trait_object;
@ -22,6 +26,9 @@ use std::fmt::Debug;
SoundObject(SoundObject<'gc>), SoundObject(SoundObject<'gc>),
StageObject(StageObject<'gc>), StageObject(StageObject<'gc>),
SuperObject(SuperObject<'gc>), SuperObject(SuperObject<'gc>),
XMLObject(XMLObject<'gc>),
XMLAttributesObject(XMLAttributesObject<'gc>),
XMLIDMapObject(XMLIDMapObject<'gc>),
} }
)] )]
pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy { pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy {
@ -258,10 +265,19 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
} }
/// Get the underlying display node for this object, if it exists. /// Get the underlying display node for this object, if it exists.
fn as_display_object(&self) -> Option<DisplayObject<'gc>>; fn as_display_object(&self) -> Option<DisplayObject<'gc>> {
None
}
/// Get the underlying executable for this object, if it exists. /// Get the underlying executable for this object, if it exists.
fn as_executable(&self) -> Option<Executable<'gc>>; fn as_executable(&self) -> Option<Executable<'gc>> {
None
}
/// Get the underlying XML node for this object, if it exists.
fn as_xml_node(&self) -> Option<XMLNode<'gc>> {
None
}
fn as_ptr(&self) -> *const ObjectPtr; fn as_ptr(&self) -> *const ObjectPtr;

View File

@ -0,0 +1,233 @@
//! AVM1 object type to represent the attributes of 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::{XMLName, 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's attributes.
///
/// Note that this is *not* the same as the XMLNode object itself; for example,
/// `XMLNode`s must store both their base object and attributes object
/// separately.
#[derive(Clone, Copy, Collect)]
#[collect(no_drop)]
pub struct XMLAttributesObject<'gc>(ScriptObject<'gc>, XMLNode<'gc>);
impl<'gc> XMLAttributesObject<'gc> {
/// Construct an XMLAttributesObject for an already existing node's
/// attributes.
pub fn from_xml_node(
gc_context: MutationContext<'gc, '_>,
xml_node: XMLNode<'gc>,
) -> Object<'gc> {
XMLAttributesObject(ScriptObject::object(gc_context, None), xml_node).into()
}
fn base(&self) -> ScriptObject<'gc> {
match self {
XMLAttributesObject(base, ..) => *base,
}
}
fn node(&self) -> XMLNode<'gc> {
match self {
XMLAttributesObject(_, node) => *node,
}
}
}
impl fmt::Debug for XMLAttributesObject<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
XMLAttributesObject(base, node) => f
.debug_tuple("XMLAttributesObject")
.field(base)
.field(node)
.finish(),
}
}
}
impl<'gc> TObject<'gc> for XMLAttributesObject<'gc> {
fn get_local(
&self,
name: &str,
_avm: &mut Avm1<'gc>,
_context: &mut UpdateContext<'_, 'gc, '_>,
_this: Object<'gc>,
) -> Result<ReturnValue<'gc>, Error> {
Ok(self
.node()
.attribute_value(&XMLName::from_str(name))
.map(|s| s.into())
.unwrap_or_else(|| Value::Undefined)
.into())
}
fn set(
&self,
name: &str,
value: Value<'gc>,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
self.node().set_attribute_value(
context.gc_context,
&XMLName::from_str(name),
&value.clone().coerce_to_string(avm, context)?,
);
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<ReturnValue<'gc>, Error> {
self.base().call(avm, context, this, args)
}
#[allow(clippy::new_ret_no_self)]
fn new(
&self,
_avm: &mut Avm1<'gc>,
_context: &mut UpdateContext<'_, 'gc, '_>,
_this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Object<'gc>, Error> {
//TODO: `new xmlnode.attributes()` returns undefined, not an object
Err("Cannot create new XML Attributes object".into())
}
fn delete(&self, gc_context: MutationContext<'gc, '_>, name: &str) -> bool {
self.node()
.delete_attribute(gc_context, &XMLName::from_str(name));
self.base().delete(gc_context, name)
}
fn add_property(
&self,
gc_context: MutationContext<'gc, '_>,
name: &str,
get: Executable<'gc>,
set: Option<Executable<'gc>>,
attributes: EnumSet<Attribute>,
) {
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<Attribute>,
) {
self.base()
.define_value(gc_context, name, value, attributes)
}
fn set_attributes(
&mut self,
gc_context: MutationContext<'gc, '_>,
name: Option<&str>,
set_attributes: EnumSet<Attribute>,
clear_attributes: EnumSet<Attribute>,
) {
self.base()
.set_attributes(gc_context, name, set_attributes, clear_attributes)
}
fn proto(&self) -> Option<Object<'gc>> {
self.base().proto()
}
fn has_property(&self, name: &str) -> bool {
self.base().has_property(name)
}
fn has_own_property(&self, name: &str) -> bool {
self.node()
.attribute_value(&XMLName::from_str(name))
.is_some()
}
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<String> {
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<Object<'gc>> {
self.base().interfaces()
}
fn set_interfaces(&mut self, context: MutationContext<'gc, '_>, iface_list: Vec<Object<'gc>>) {
self.base().set_interfaces(context, iface_list)
}
fn as_script_object(&self) -> Option<ScriptObject<'gc>> {
Some(self.base())
}
fn as_xml_node(&self) -> Option<XMLNode<'gc>> {
Some(self.node())
}
fn as_ptr(&self) -> *const ObjectPtr {
self.base().as_ptr() as *const ObjectPtr
}
fn length(&self) -> usize {
self.base().length()
}
fn array(&self) -> Vec<Value<'gc>> {
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)
}
}

View File

@ -0,0 +1,228 @@
//! AVM1 object type to represent the attributes of 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;
/// An Object that is inherently tied to an XML document's ID map.
///
/// Note that this is *not* the same as the XML root object itself; and
/// furthermore this object is linked to the document, not the root node of the
/// document.
#[derive(Clone, Copy, Collect)]
#[collect(no_drop)]
pub struct XMLIDMapObject<'gc>(ScriptObject<'gc>, XMLDocument<'gc>);
impl<'gc> XMLIDMapObject<'gc> {
/// Construct an XMLIDMapObject for an already existing node's
/// attributes.
pub fn from_xml_document(
gc_context: MutationContext<'gc, '_>,
xml_doc: XMLDocument<'gc>,
) -> Object<'gc> {
XMLIDMapObject(ScriptObject::object(gc_context, None), xml_doc).into()
}
fn base(&self) -> ScriptObject<'gc> {
match self {
XMLIDMapObject(base, ..) => *base,
}
}
fn document(&self) -> XMLDocument<'gc> {
match self {
XMLIDMapObject(_, document) => *document,
}
}
}
impl fmt::Debug for XMLIDMapObject<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
XMLIDMapObject(base, document) => f
.debug_tuple("XMLIDMapObject")
.field(base)
.field(document)
.finish(),
}
}
}
impl<'gc> TObject<'gc> for XMLIDMapObject<'gc> {
fn get_local(
&self,
name: &str,
avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
) -> Result<ReturnValue<'gc>, Error> {
if let Some(mut node) = self.document().get_node_by_id(name) {
Ok(node
.script_object(context.gc_context, Some(avm.prototypes().xml_node))
.into())
} else {
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<ReturnValue<'gc>, Error> {
self.base().call(avm, context, this, args)
}
#[allow(clippy::new_ret_no_self)]
fn new(
&self,
_avm: &mut Avm1<'gc>,
_context: &mut UpdateContext<'_, 'gc, '_>,
_this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Object<'gc>, Error> {
//TODO: `new xmlnode.attributes()` returns undefined, not an object
Err("Cannot create new XML Attributes object".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<Executable<'gc>>,
attributes: EnumSet<Attribute>,
) {
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<Attribute>,
) {
self.base()
.define_value(gc_context, name, value, attributes)
}
fn set_attributes(
&mut self,
gc_context: MutationContext<'gc, '_>,
name: Option<&str>,
set_attributes: EnumSet<Attribute>,
clear_attributes: EnumSet<Attribute>,
) {
self.base()
.set_attributes(gc_context, name, set_attributes, clear_attributes)
}
fn proto(&self) -> Option<Object<'gc>> {
self.base().proto()
}
fn has_property(&self, name: &str) -> bool {
self.base().has_property(name)
}
fn has_own_property(&self, name: &str) -> bool {
self.document().get_node_by_id(name).is_some() || 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<String> {
let keys = self.base().get_keys();
keys.union(&self.document().get_node_ids())
.map(|s| s.to_string())
.collect()
}
fn as_string(&self) -> String {
self.base().as_string()
}
fn type_of(&self) -> &'static str {
self.base().type_of()
}
fn interfaces(&self) -> Vec<Object<'gc>> {
self.base().interfaces()
}
fn set_interfaces(&mut self, context: MutationContext<'gc, '_>, iface_list: Vec<Object<'gc>>) {
self.base().set_interfaces(context, iface_list)
}
fn as_script_object(&self) -> Option<ScriptObject<'gc>> {
Some(self.base())
}
fn as_xml_node(&self) -> Option<XMLNode<'gc>> {
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<Value<'gc>> {
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)
}
}

221
core/src/avm1/xml_object.rs Normal file
View File

@ -0,0 +1,221 @@
//! 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 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<Object<'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();
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>>,
) -> Object<'gc> {
XMLObject(ScriptObject::object(gc_context, proto), xml_node).into()
}
fn base(&self) -> ScriptObject<'gc> {
match self {
XMLObject(base, ..) => *base,
}
}
}
impl fmt::Debug for XMLObject<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
XMLObject(base, node) => f.debug_tuple("XMLObject").field(base).field(node).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<ReturnValue<'gc>, 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<ReturnValue<'gc>, Error> {
self.base().call(avm, context, this, args)
}
#[allow(clippy::new_ret_no_self)]
fn new(
&self,
_avm: &mut Avm1<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Object<'gc>, Error> {
Ok(XMLObject::empty_node(context.gc_context, Some(this)))
}
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<Executable<'gc>>,
attributes: EnumSet<Attribute>,
) {
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<Attribute>,
) {
self.base()
.define_value(gc_context, name, value, attributes)
}
fn set_attributes(
&mut self,
gc_context: MutationContext<'gc, '_>,
name: Option<&str>,
set_attributes: EnumSet<Attribute>,
clear_attributes: EnumSet<Attribute>,
) {
self.base()
.set_attributes(gc_context, name, set_attributes, clear_attributes)
}
fn proto(&self) -> Option<Object<'gc>> {
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<String> {
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<Object<'gc>> {
self.base().interfaces()
}
fn set_interfaces(&mut self, context: MutationContext<'gc, '_>, iface_list: Vec<Object<'gc>>) {
self.base().set_interfaces(context, iface_list)
}
fn as_script_object(&self) -> Option<ScriptObject<'gc>> {
Some(self.base())
}
fn as_xml_node(&self) -> Option<XMLNode<'gc>> {
match self {
XMLObject(_base, node) => Some(*node),
}
}
fn as_ptr(&self) -> *const ObjectPtr {
self.base().as_ptr() as *const ObjectPtr
}
fn length(&self) -> usize {
self.base().length()
}
fn array(&self) -> Vec<Value<'gc>> {
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)
}
}

View File

@ -20,6 +20,7 @@ mod prelude;
pub mod shape_utils; pub mod shape_utils;
pub mod tag_utils; pub mod tag_utils;
mod transform; mod transform;
mod xml;
pub mod backend; pub mod backend;

21
core/src/xml.rs Normal file
View File

@ -0,0 +1,21 @@
//! Garbage-collectable XML DOM impl
mod document;
mod error;
mod namespace;
mod tree;
#[cfg(test)]
mod tests;
pub use document::XMLDocument;
pub use error::Error;
pub use error::ParseError;
pub use namespace::XMLName;
pub use tree::XMLNode;
pub const ELEMENT_NODE: u8 = 1;
pub const TEXT_NODE: u8 = 3;
pub const COMMENT_NODE: u8 = 8;
pub const DOCUMENT_NODE: u8 = 9;
pub const DOCUMENT_TYPE_NODE: u8 = 10;

279
core/src/xml/document.rs Normal file
View File

@ -0,0 +1,279 @@
//! XML Document
use crate::avm1::xml_idmap_object::XMLIDMapObject;
use crate::avm1::Object;
use crate::xml::{Error, ParseError, XMLName, XMLNode};
use gc_arena::{Collect, GcCell, MutationContext};
use quick_xml::events::{BytesDecl, Event};
use quick_xml::{Error as QXError, Writer};
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use std::io::Cursor;
/// 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 XMLDocumentData<'gc> {
/// The root node of the XML document.
root: Option<XMLNode<'gc>>,
/// Whether or not the document has a document declaration.
has_xmldecl: bool,
/// The XML version string, if set.
version: String,
/// The XML document encoding, if set.
encoding: Option<String>,
/// The XML standalone flag, if set.
standalone: Option<String>,
/// The XML doctype, if set.
doctype: Option<XMLNode<'gc>>,
/// The document's ID map.
///
/// When nodes are parsed into the document by way of `parseXML` or the
/// document constructor, they get put into this list here, which is used
/// to populate the document's `idMap`.
idmap: BTreeMap<String, XMLNode<'gc>>,
/// The script object associated with this XML node, if any.
idmap_script_object: Option<Object<'gc>>,
/// The last parse error encountered, if any.
last_parse_error: Option<ParseError>,
}
impl<'gc> XMLDocument<'gc> {
/// Construct a new, empty XML document.
pub fn new(mc: MutationContext<'gc, '_>) -> Self {
let document = Self(GcCell::allocate(
mc,
XMLDocumentData {
root: None,
has_xmldecl: false,
version: "1.0".to_string(),
encoding: None,
standalone: None,
doctype: None,
idmap: BTreeMap::new(),
idmap_script_object: None,
last_parse_error: None,
},
));
let root = XMLNode::new_document_root(mc, document);
document.0.write(mc).root = Some(root);
document
}
/// 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 {
let self_read = self.0.read();
Self(GcCell::allocate(
gc_context,
XMLDocumentData {
root: None,
has_xmldecl: self_read.has_xmldecl,
version: self_read.version.clone(),
encoding: self_read.encoding.clone(),
standalone: self_read.standalone.clone(),
doctype: None,
idmap: BTreeMap::new(),
idmap_script_object: None,
last_parse_error: 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));
}
_ => {}
}
}
/// Set the DOCTYPE of the document, if possible.
///
/// If the proposed doctype is not an `XMLNode::DocType`, or the document
/// already has a doctype, nothing happens.
pub fn link_doctype(
&mut self,
gc_context: MutationContext<'gc, '_>,
proposed_doctype: XMLNode<'gc>,
) {
let mut self_write = self.0.write(gc_context);
if self_write.doctype.is_none() && proposed_doctype.is_doctype() {
self_write.doctype = Some(proposed_doctype);
}
}
/// Retrieve the first DocType node in the document.
pub fn doctype(self) -> Option<XMLNode<'gc>> {
self.0.read().doctype
}
/// Process events being passed into some node of the document.
///
/// There are certain nodes which have document-wide implications if parsed
/// into any node within the document. These are processed here.
pub fn process_event(self, mc: MutationContext<'gc, '_>, event: &Event) -> Result<(), Error> {
if let Event::Decl(bd) = event {
let mut self_write = self.0.write(mc);
self_write.has_xmldecl = true;
self_write.version = String::from_utf8(bd.version()?.into_owned())?;
self_write.encoding = if let Some(encoding) = bd.encoding() {
Some(String::from_utf8(encoding?.into_owned())?)
} else {
None
};
self_write.standalone = if let Some(standalone) = bd.standalone() {
Some(String::from_utf8(standalone?.into_owned())?)
} else {
None
};
}
Ok(())
}
/// Generate a string matching the XML document declaration, if there is
/// one.
pub fn xmldecl_string(self) -> Result<Option<String>, Error> {
let self_read = self.0.read();
if self_read.has_xmldecl {
let mut result = Vec::new();
let mut writer = Writer::new(Cursor::new(&mut result));
let bd = BytesDecl::new(
&self_read.version.as_bytes(),
self_read.encoding.as_ref().map(|s| s.as_bytes()),
self_read.standalone.as_ref().map(|s| s.as_bytes()),
);
writer.write_event(Event::Decl(bd))?;
Ok(Some(String::from_utf8(result)?))
} else {
Ok(None)
}
}
/// Obtain the script object for the document's `idMap` property, or create
/// one if it doesn't exist
pub fn idmap_script_object(&mut self, gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
let mut object = self.0.read().idmap_script_object;
if object.is_none() {
object = Some(XMLIDMapObject::from_xml_document(gc_context, *self));
self.0.write(gc_context).idmap_script_object = object;
}
object.unwrap()
}
/// Update the idmap object with a given new node.
pub fn update_idmap(&mut self, mc: MutationContext<'gc, '_>, node: XMLNode<'gc>) {
if let Some(id) = node.attribute_value(&XMLName::from_str("id")) {
self.0.write(mc).idmap.insert(id, node);
}
}
/// Retrieve a node from the idmap.
///
/// This only retrieves nodes that had this `id` *at the time of string
/// parsing*. Nodes which obtained the `id` after the fact, or nodes with
/// the `id` that were added to the document after the fact, will not be
/// returned by this function.
pub fn get_node_by_id(self, id: &str) -> Option<XMLNode<'gc>> {
self.0.read().idmap.get(id).copied()
}
/// Retrieve all IDs currently present in the idmap.
pub fn get_node_ids(self) -> HashSet<String> {
let mut result = HashSet::new();
for key in self.0.read().idmap.keys() {
result.insert(key.to_string());
}
result
}
/// Log the result of an XML parse, saving the error for later inspection
/// if necessary.
pub fn log_parse_result<O>(
self,
gc_context: MutationContext<'gc, '_>,
maybe_error: Result<O, QXError>,
) -> Result<O, ParseError> {
match maybe_error {
Ok(v) => Ok(v),
Err(e) => {
let new_error = ParseError::from_quickxml_error(e);
self.0.write(gc_context).last_parse_error = Some(new_error.clone());
Err(new_error)
}
}
}
/// Get the last parse error within this document, if any.
pub fn last_parse_error(self) -> Option<ParseError> {
self.0.read().last_parse_error.clone()
}
/// Clear the previous parse error.
pub fn clear_parse_error(self, gc_context: MutationContext<'gc, '_>) {
self.0.write(gc_context).last_parse_error = None;
}
}
impl<'gc> fmt::Debug for XMLDocument<'gc> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("XMLDocument")
.field("root", &self.0.read().root)
.finish()
}
}

52
core/src/xml/error.rs Normal file
View File

@ -0,0 +1,52 @@
//! Error types used in XML handling
use gc_arena::{Collect, CollectionContext};
use quick_xml::Error as QXError;
use std::error::Error as StdError;
use std::fmt::Error as FmtError;
use std::fmt::{Display, Formatter};
use std::rc::Rc;
/// Boxed error type.
pub type Error = Box<dyn std::error::Error>;
/// Boxed `quick_xml` error
///
/// We can't clone `quick_xml` errors, nor can we clone several of the error
/// types it wraps over, so this creates an RC boxed version of the error that
/// can then be used elsewhere.
#[derive(Clone, Debug)]
pub struct ParseError(Rc<QXError>);
unsafe impl Collect for ParseError {
/// ParseError does not contain GC pointers.
fn trace(&self, _cc: CollectionContext<'_>) {}
}
impl ParseError {
///Convert a quick_xml error into a `ParseError`.
pub fn from_quickxml_error(err: QXError) -> Self {
ParseError(Rc::new(err))
}
pub fn ref_error(&self) -> &QXError {
&*self.0
}
}
impl Display for ParseError {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> {
self.0.fmt(fmt)
}
}
impl StdError for ParseError {
#[allow(deprecated)]
fn cause(&self) -> Option<&dyn StdError> {
self.0.cause()
}
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.0.source()
}
}

97
core/src/xml/namespace.rs Normal file
View File

@ -0,0 +1,97 @@
//! 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<String>,
name: String,
}
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, Error> {
Self::from_bytes_cow(Cow::Borrowed(bytes))
}
pub fn from_str(strval: &str) -> Self {
Self::from_str_cow(Cow::Borrowed(strval))
}
pub fn from_bytes_cow(bytes: Cow<[u8]>) -> Result<Self, Error> {
let full_name = match bytes {
Cow::Borrowed(ln) => Cow::Borrowed(std::str::from_utf8(ln)?),
Cow::Owned(ln) => Cow::Owned(String::from_utf8(ln)?),
};
Ok(Self::from_str_cow(full_name))
}
pub fn from_str_cow(full_name: Cow<str>) -> Self {
if let Some(colon_index) = full_name.find(':') {
Self {
namespace: Some(full_name[0..colon_index].to_owned()),
name: full_name[colon_index + 1..].to_owned(),
}
} else {
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 {
self.name.to_string()
}
}
}
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()
}
}

112
core/src/xml/tests.rs Normal file
View File

@ -0,0 +1,112 @@
//! XML tests
use crate::xml;
use crate::xml::{XMLDocument, XMLName};
use gc_arena::rootless_arena;
/// Tests very basic parsing of a single-element document.
#[test]
fn parse_single_element() {
rootless_arena(|mc| {
let xml = XMLDocument::new(mc);
xml.as_node()
.replace_with_str(mc, "<test></test>")
.expect("Parsed document");
dbg!(xml);
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);
assert_eq!(root.tag_name(), Some(XMLName::from_str("test")));
let mut root_children = root.children().unwrap();
assert!(root_children.next().is_none());
assert!(roots.next().is_none());
})
}
/// Tests double-ended traversal of child nodes via DoubleEndedIterator.
#[test]
fn double_ended_children() {
rootless_arena(|mc| {
let xml = XMLDocument::new(mc);
xml.as_node()
.replace_with_str(
mc,
"<test></test><test2></test2><test3></test3><test4></test4><test5></test5>",
)
.expect("Parsed document");
let mut roots = xml
.as_node()
.children()
.expect("Parsed document should be capable of having child nodes");
let root = roots.next().expect("Should have first root");
assert_eq!(root.node_type(), xml::ELEMENT_NODE);
assert_eq!(root.tag_name(), Some(XMLName::from_str("test")));
let root = roots.next_back().expect("Should have last root");
assert_eq!(root.node_type(), xml::ELEMENT_NODE);
assert_eq!(root.tag_name(), Some(XMLName::from_str("test5")));
let root = roots.next().expect("Should have next root");
assert_eq!(root.node_type(), xml::ELEMENT_NODE);
assert_eq!(root.tag_name(), Some(XMLName::from_str("test2")));
let root = roots.next_back().expect("Should have second-to-last root");
assert_eq!(root.node_type(), xml::ELEMENT_NODE);
assert_eq!(root.tag_name(), Some(XMLName::from_str("test4")));
let root = roots.next().expect("Should have next root");
assert_eq!(root.node_type(), xml::ELEMENT_NODE);
assert_eq!(root.tag_name(), Some(XMLName::from_str("test3")));
assert!(roots.next().is_none());
assert!(roots.next_back().is_none());
})
}
/// Tests round-trip XML writing behavior.
#[test]
fn round_trip_tostring() {
let test_string = "<test><!-- Comment -->This is a text node</test>";
rootless_arena(|mc| {
let xml = XMLDocument::new(mc);
xml.as_node()
.replace_with_str(mc, test_string)
.expect("Parsed document");
let result = xml
.as_node()
.into_string(&mut |_| true)
.expect("Successful toString");
assert_eq!(test_string, result);
})
}
/// Tests filtered XML writing behavior.
#[test]
fn round_trip_filtered_tostring() {
let test_string = "<test><!-- Comment -->This is a text node</test>";
rootless_arena(|mc| {
let xml = XMLDocument::new(mc);
xml.as_node()
.replace_with_str(mc, test_string)
.expect("Parsed document");
let result = xml
.as_node()
.into_string(&mut |node| !node.is_comment())
.expect("Successful toString");
assert_eq!("<test>This is a text node</test>", result);
})
}

1388
core/src/xml/tree.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -105,6 +105,27 @@ swf_tests! {
(strictequals_swf6, "avm1/strictequals_swf6", 1), (strictequals_swf6, "avm1/strictequals_swf6", 1),
(global_is_bare, "avm1/global_is_bare", 1), (global_is_bare, "avm1/global_is_bare", 1),
(as2_oop, "avm1/as2_oop", 1), (as2_oop, "avm1/as2_oop", 1),
(xml, "avm1/xml", 1),
(xml_namespaces, "avm1/xml_namespaces", 1),
(xml_node_namespaceuri, "avm1/xml_node_namespaceuri", 1),
(xml_node_weirdnamespace, "avm1/xml_node_weirdnamespace", 1),
(xml_clone_expandos, "avm1/xml_clone_expandos", 1),
(xml_has_child_nodes, "avm1/xml_has_child_nodes", 1),
(xml_first_last_child, "avm1/xml_first_last_child", 1),
(xml_parent_and_child, "avm1/xml_parent_and_child", 1),
(xml_siblings, "avm1/xml_siblings", 1),
(xml_attributes_read, "avm1/xml_attributes_read", 1),
(xml_append_child, "avm1/xml_append_child", 1),
(xml_append_child_with_parent, "avm1/xml_append_child_with_parent", 1),
(xml_remove_node, "avm1/xml_remove_node", 1),
(xml_insert_before, "avm1/xml_insert_before", 1),
(xml_to_string, "avm1/xml_to_string", 1),
(xml_to_string_comment, "avm1/xml_to_string_comment", 1),
(xml_idmap, "avm1/xml_idmap", 1),
(xml_inspect_doctype, "avm1/xml_inspect_doctype", 1),
#[ignore] (xml_inspect_xmldecl, "avm1/xml_inspect_xmldecl", 1),
(xml_inspect_createmethods, "avm1/xml_inspect_createmethods", 1),
(xml_inspect_parsexml, "avm1/xml_inspect_parsexml", 1),
} }
#[test] #[test]

View File

@ -0,0 +1,15 @@
1
null
null
null
null
Children:
Child 0
1
test
null
test
Children:
End children.
End children.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
true
true
null
true
true
null
true
true

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
true
null
true
true
null
true
Swapping nodes...
true
null
true
true
null
true
Failing to swap nodes...
true
null
true
true
null
true

View File

@ -0,0 +1,3 @@
attr y string
undefined

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,19 @@
Shallow copy
0
0
test
Deep copy
1
0
test
All expandos
xml_expando
xmlnode_expando
undefined
undefined
undefined
undefined
cloneNode w/o arg
false
0
undefined

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
true
true
null
null
true
true
undefined
undefined

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
false
true
false

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,21 @@
true
true
idmap has test
idmap has test2
Manually added new node via appendChild
Checking old copy of IDMAP...
idmap has test
idmap has test2
Checking new copy of IDMAP...
idmap has test
idmap has test2
Parsed another node with parseXML...
idmap has test
idmap has test2
idmap has test4
Checking for IDMAP expandos...
idmap has test
idmap has test124
idmap has test2
idmap has test4
trace

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
true
null
true
true
null
true
Swapping nodes...
true
null
true
true
null
true
Failing to swap nodes...
true
null
true
true
null
true

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,15 @@
Type: 1
Name: null
Value: null
Attributes defined
Has 1 children
Type: 1
Name: test
Value: null
Attributes defined
Has 1 children
Type: 3
Name: null
Value: This is a test
Attributes defined
Has 0 children

View File

@ -0,0 +1,7 @@
Type: 1
Name: null
Value: null
Attributes defined
Has 0 children
Doctype: <!DOCTYPE myfancydoctype>
XML decl: undefined

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,62 @@
Empty XML document
Type: 1
Name: null
Value: null
Attributes defined
Has 0 children
Replaced XML document
Type: 1
Name: null
Value: null
Attributes defined
Has 2 children
Type: 1
Name: test
Value: null
Attributes defined
Has 1 children
Type: 3
Name: null
Value: I'm a test text node.
Attributes defined
Has 0 children
Type: 3
Name: null
Value: Here's more text.
Attributes defined
Has 0 children
Second replaced XML document
Type: 1
Name: null
Value: null
Attributes defined
Has 2 children
Type: 3
Name: null
Value: Now it's different:
Attributes defined
Has 0 children
Type: 1
Name: test2
Value: null
Attributes defined
Has 0 children
First document's first node
Type: 1
Name: test
Value: null
Attributes defined
Has 1 children
Type: 3
Name: null
Value: I'm a test text node.
Attributes defined
Has 0 children
Parent: null
First document's second node
Type: 3
Name: null
Value: Here's more text.
Attributes defined
Has 0 children
Parent: null

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
Type: 1
Name: null
Value: null
Attributes defined
Has 0 children
Doctype: undefined
XML decl: <?xml version='1.1' encoding='utf-8' standalone='yes' ?>

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,12 @@
null
null
null
null
https://example.com
test
null
null
https://example.com/2
test
https://example.com/2
test

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,4 @@
null
http://example.com/2
http://example.com/3

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,4 @@
null
http://example.com/2plain
http://example.com/3

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
true
true
true
true
null

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
true
null
true
true
null
null
true
true
null
After node removal
true
1
true
null
null
null
null
null
true
true
null
null

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,10 @@
null
true
true
true
true
true
true
null
null
null

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,6 @@
<test />
I&apos;m a <b>text node</b>!
<test><test2 /></test>
I&apos;m a <b>text node</b>!
<test><test2>&quot;Test&quot; Node 2</test2></test>
I&apos;m a <b>text node</b>!

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<test>This is a text node</test>

Binary file not shown.

Binary file not shown.