avm2: Initial incomplete implementation of XML (#9647)

This commit is contained in:
Aaron Hill 2023-02-25 14:06:36 -06:00 committed by GitHub
parent a36ef7fd0c
commit b26f2fd6fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1079 additions and 26 deletions

View File

@ -28,6 +28,7 @@ pub mod bytearray;
mod call_stack; mod call_stack;
mod class; mod class;
mod domain; mod domain;
mod e4x;
pub mod error; pub mod error;
mod events; mod events;
mod function; mod function;

View File

@ -2619,8 +2619,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
fn op_strict_equals(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> { fn op_strict_equals(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
let value2 = self.pop_stack(); let value2 = self.pop_stack();
let value1 = self.pop_stack(); let value1 = self.pop_stack();
self.push_stack(value1.strict_eq(&value2));
self.push_stack(value1 == value2);
Ok(FrameControl::Continue) Ok(FrameControl::Continue)
} }
@ -2877,7 +2876,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
"object" "object"
} }
} }
Object::XmlObject(_) => { Object::XmlObject(_) | Object::XmlListObject(_) => {
if is_not_subclass { if is_not_subclass {
"xml" "xml"
} else { } else {

395
core/src/avm2/e4x.rs Normal file
View File

@ -0,0 +1,395 @@
use std::{
cell::Ref,
fmt::{self, Debug},
};
use gc_arena::{Collect, GcCell, MutationContext};
use quick_xml::{
events::{BytesStart, Event},
Reader,
};
use super::{object::E4XOrXml, string::AvmString, Activation, Error, Multiname, TObject, Value};
/// The underlying XML node data, based on E4XNode in avmplus
/// This wrapped by XMLObject when necessary (see `E4XOrXml`)
#[derive(Copy, Clone, Collect, Debug)]
#[collect(no_drop)]
pub struct E4XNode<'gc>(GcCell<'gc, E4XNodeData<'gc>>);
#[derive(Collect)]
#[collect(no_drop)]
pub struct E4XNodeData<'gc> {
parent: Option<E4XNode<'gc>>,
local_name: Option<AvmString<'gc>>,
kind: E4XNodeKind<'gc>,
}
impl<'gc> Debug for E4XNodeData<'gc> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("E4XNodeData")
// Don't print the actual parent, to avoid infinite recursion
.field("parent", &self.parent.is_some())
.field("local_name", &self.local_name)
.field("kind", &self.kind)
.finish()
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub enum E4XNodeKind<'gc> {
Text(AvmString<'gc>),
Comment(AvmString<'gc>),
ProcessingInstruction(AvmString<'gc>),
Attribute(AvmString<'gc>),
Element {
attributes: Vec<E4XNode<'gc>>,
children: Vec<E4XNode<'gc>>,
},
}
impl<'gc> E4XNode<'gc> {
pub fn dummy(mc: MutationContext<'gc, '_>) -> Self {
E4XNode(GcCell::allocate(
mc,
E4XNodeData {
parent: None,
local_name: None,
kind: E4XNodeKind::Element {
attributes: vec![],
children: vec![],
},
},
))
}
fn append_child(
&self,
gc_context: MutationContext<'gc, '_>,
child: Self,
) -> Result<(), Error<'gc>> {
let mut this = self.0.write(gc_context);
let mut child_data = match child.0.try_write(gc_context) {
Ok(data) => data,
Err(_) => {
return Err(Error::RustError(
format!(
"Circular write in append_child with self={:?} child={:?}",
self, child
)
.into(),
))
}
};
child_data.parent = Some(*self);
match &mut this.kind {
E4XNodeKind::Element { children, .. } => {
children.push(child);
}
_ => {
// FIXME - figure out exactly when appending is allowed in FP,
// and throw the proper AVM error.
return Err(Error::RustError(
format!("Cannot append child {child:?} to node {:?}", this.kind).into(),
));
}
}
Ok(())
}
/// Parses a value provided to `XML`/`XMLList` into a list of nodes.
/// The caller is responsible for validating that the number of top-level nodes
/// is correct (for XML, there should be exactly one.)
pub fn parse(
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Vec<Self>, Error<'gc>> {
let string = match value {
Value::Bool(_) | Value::Number(_) | Value::Integer(_) => {
value.coerce_to_string(activation)?
}
Value::String(string) => string,
Value::Object(obj) => {
if matches!(
obj.as_primitive().as_deref(),
Some(Value::Bool(_) | Value::Number(_) | Value::String(_))
) {
value.coerce_to_string(activation)?
} else if let Some(_xml) = obj.as_xml_object() {
return Err(Error::RustError(
"Deep clone of XML not yet implemented".into(),
));
} else {
return Err(Error::RustError(
format!("Could not convert value {value:?} to XML").into(),
));
}
}
// The docs claim that this throws a TypeError, but it actually doesn't
Value::Null | Value::Undefined => AvmString::default(),
};
let data_utf8 = string.to_utf8_lossy();
let mut parser = Reader::from_str(&data_utf8);
let mut buf = Vec::new();
let mut open_tags: Vec<E4XNode<'gc>> = vec![];
// FIXME - look this up from static property and settings
let ignore_white = true;
let mut top_level = vec![];
let mut depth = 0;
// This can't be a closure that captures these variables, because we need to modify them
// outside of this body.
fn push_childless_node<'gc>(
node: E4XNode<'gc>,
open_tags: &mut [E4XNode<'gc>],
top_level: &mut Vec<E4XNode<'gc>>,
depth: usize,
activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
if let Some(current_tag) = open_tags.last_mut() {
current_tag.append_child(activation.context.gc_context, node)?;
}
if depth == 0 {
top_level.push(node);
}
Ok(())
}
loop {
let event = parser.read_event(&mut buf).map_err(|error| {
Error::RustError(format!("XML parsing error: {error:?}").into())
})?;
match &event {
Event::Start(bs) => {
let child = E4XNode::from_start_event(activation, bs)?;
if let Some(current_tag) = open_tags.last_mut() {
current_tag.append_child(activation.context.gc_context, child)?;
}
open_tags.push(child);
depth += 1;
}
Event::Empty(bs) => {
let node = E4XNode::from_start_event(activation, bs)?;
push_childless_node(node, &mut open_tags, &mut top_level, depth, activation)?;
}
Event::End(_) => {
depth -= 1;
let node = open_tags.pop().unwrap();
if depth == 0 {
top_level.push(node);
}
}
Event::Text(bt) => {
let text = bt.unescaped()?;
let is_whitespace_char = |c: &u8| matches!(*c, b'\t' | b'\n' | b'\r' | b' ');
let is_whitespace_text = text.iter().all(is_whitespace_char);
if !(text.is_empty() || ignore_white && is_whitespace_text) {
let text = AvmString::new_utf8_bytes(activation.context.gc_context, &text);
let node = E4XNode(GcCell::allocate(
activation.context.gc_context,
E4XNodeData {
parent: None,
local_name: None,
kind: E4XNodeKind::Text(text),
},
));
push_childless_node(
node,
&mut open_tags,
&mut top_level,
depth,
activation,
)?;
}
}
Event::Comment(bt) | Event::PI(bt) => {
let text = bt.unescaped()?;
let text = AvmString::new_utf8_bytes(activation.context.gc_context, &text);
let kind = match event {
Event::Comment(_) => E4XNodeKind::Comment(text),
Event::PI(_) => E4XNodeKind::ProcessingInstruction(text),
_ => unreachable!(),
};
let node = E4XNode(GcCell::allocate(
activation.context.gc_context,
E4XNodeData {
parent: None,
local_name: None,
kind,
},
));
push_childless_node(node, &mut open_tags, &mut top_level, depth, activation)?;
}
Event::Decl(bd) => {
return Err(Error::RustError(
format!("XML declaration {bd:?} is not yet implemented").into(),
))
}
Event::DocType(bt) => {
return Err(Error::RustError(
format!("XML doctype {bt:?} is not yet implemented").into(),
))
}
Event::Eof => break,
_ => {}
}
}
if top_level.is_empty() {
top_level.push(E4XNode(GcCell::allocate(
activation.context.gc_context,
E4XNodeData {
parent: None,
local_name: None,
kind: E4XNodeKind::Text(AvmString::default()),
},
)));
}
Ok(top_level)
}
/// Construct an XML Element node from a `quick_xml` `BytesStart` event.
///
/// The returned node will always be an `Element`, and it must only contain
/// valid encoded UTF-8 data. (Other encoding support is planned later.)
pub fn from_start_event(
activation: &mut Activation<'_, 'gc>,
bs: &BytesStart<'_>,
) -> Result<Self, quick_xml::Error> {
// FIXME - handle namespace
let name = AvmString::new_utf8_bytes(activation.context.gc_context, bs.local_name());
let mut attribute_nodes = Vec::new();
let attributes: Result<Vec<_>, _> = bs.attributes().collect();
for attribute in attributes? {
let key = AvmString::new_utf8_bytes(activation.context.gc_context, attribute.key);
let value_bytes = attribute.unescaped_value()?;
let value = AvmString::new_utf8_bytes(activation.context.gc_context, &value_bytes);
let attribute_data = E4XNodeData {
parent: None,
local_name: Some(key),
kind: E4XNodeKind::Attribute(value),
};
let attribute = E4XNode(GcCell::allocate(
activation.context.gc_context,
attribute_data,
));
attribute_nodes.push(attribute);
}
let data = E4XNodeData {
parent: None,
local_name: Some(name),
kind: E4XNodeKind::Element {
attributes: attribute_nodes,
children: Vec::new(),
},
};
Ok(E4XNode(GcCell::allocate(
activation.context.gc_context,
data,
)))
}
pub fn local_name(&self) -> Option<AvmString<'gc>> {
self.0.read().local_name
}
pub fn matches_name(&self, name: &Multiname<'gc>) -> bool {
// FIXME - we need to handle namespaces heere
if let Some(local_name) = self.local_name() {
Some(local_name) == name.local_name()
} else {
false
}
}
pub fn has_simple_content(&self) -> bool {
match &self.0.read().kind {
E4XNodeKind::Element { children, .. } => children
.iter()
.all(|child| !matches!(&*child.kind(), E4XNodeKind::Element { .. })),
E4XNodeKind::Text(_) => true,
E4XNodeKind::Attribute(_) => true,
E4XNodeKind::Comment(_) => false,
E4XNodeKind::ProcessingInstruction(_) => false,
}
}
pub fn xml_to_string(
&self,
activation: &mut Activation<'_, 'gc>,
) -> Result<AvmString<'gc>, Error<'gc>> {
match &self.0.read().kind {
E4XNodeKind::Text(text) => Ok(*text),
E4XNodeKind::Attribute(text) => Ok(*text),
E4XNodeKind::Element { children, .. } => {
if self.has_simple_content() {
return simple_content_to_string(
children.iter().map(|node| E4XOrXml::E4X(*node)),
activation,
);
}
Err(format!(
"XML.toString(): Not yet implemented non-simple {:?} children {:?}",
self, children
)
.into())
}
other => Err(format!("XML.toString(): Not yet implemented for {other:?}").into()),
}
}
pub fn xml_to_xml_string(
&self,
_activation: &mut Activation<'_, 'gc>,
) -> Result<AvmString<'gc>, Error<'gc>> {
match &self.0.read().kind {
E4XNodeKind::Text(text) => Ok(*text),
E4XNodeKind::Element { .. } => {
Err(format!("XML.toXMLString(): Not yet implemented element {:?}", self).into())
}
other => Err(format!("XML.toXMLString(): Not yet implemented for {other:?}").into()),
}
}
pub fn kind(&self) -> Ref<'_, E4XNodeKind<'gc>> {
Ref::map(self.0.read(), |r| &r.kind)
}
pub fn ptr_eq(first: E4XNode<'gc>, second: E4XNode<'gc>) -> bool {
GcCell::ptr_eq(first.0, second.0)
}
}
pub fn simple_content_to_string<'gc>(
children: impl Iterator<Item = E4XOrXml<'gc>>,
activation: &mut Activation<'_, 'gc>,
) -> Result<AvmString<'gc>, Error<'gc>> {
let mut out = AvmString::default();
for child in children {
if matches!(
&*child.node().kind(),
E4XNodeKind::Comment(_) | E4XNodeKind::ProcessingInstruction(_)
) {
continue;
}
let child_str = child.node().xml_to_string(activation)?;
out = AvmString::concat(activation.context.gc_context, out, child_str);
}
Ok(out)
}

View File

@ -239,3 +239,11 @@ impl<'gc> From<ruffle_render::error::Error> for Error<'gc> {
Error::RustError(val.into()) Error::RustError(val.into())
} }
} }
// TODO - Remove this, and convert `quick_xml` errors into AVM errors,
// specific to the XML method that was original invoked.
impl<'gc> From<quick_xml::Error> for Error<'gc> {
fn from(val: quick_xml::Error) -> Error<'gc> {
Error::RustError(val.into())
}
}

View File

@ -1,4 +1,44 @@
package { package {
[Ruffle(InstanceAllocator)] [Ruffle(InstanceAllocator)]
public final dynamic class XML {} public final dynamic class XML {
public function XML(value:* = undefined) {
this.init(value);
}
private native function init(value:*):void;
AS3 native function name():Object;
AS3 native function localName():Object;
AS3 native function toXMLString():String;
AS3 native function toString():String;
prototype.name = function():Object {
var self:XML = this;
// NOTE - `self.name()` should be sufficient here (and in all of the other methods)
// However, asc.jar doesn't resolve the 'AS3' namespace when I do
// 'self.name()' here, which leads to the prototype method invoking
// itself, instead of the AS3 method.
return self.AS3::name();
};
prototype.localName = function():Object {
var self:XML = this;
return self.AS3::localName();
};
prototype.toXMLString = function():String {
var self:XML = this;
return self.AS3::toXMLString();
};
prototype.toString = function():String {
if (this === prototype) {
return "";
}
var self:XML = this;
return self.AS3::toString();
};
}
} }

View File

@ -1,4 +1,16 @@
package { package {
[Ruffle(InstanceAllocator)] [Ruffle(InstanceAllocator)]
public final dynamic class XMLList {} public final dynamic class XMLList {
public function XMLList(value:* = undefined) {
this.init(value);
}
private native function init(value:*): void;
AS3 native function hasSimpleContent():Boolean;
AS3 native function length():int
public native function toString():String;
}
} }

View File

@ -4,18 +4,7 @@ package flash.utils {
public native function getQualifiedSuperclassName(value:*):String; public native function getQualifiedSuperclassName(value:*):String;
public native function getTimer():int; public native function getTimer():int;
// note: this is an extremely silly hack, public native function describeType(value:*): XML;
// made specifically to fool com.adobe.serialization.json.JsonEncoder.
// this relies on the fact that a.@b in Ruffle is unimplemented and behaves like a.b.
// once we get proper XML support, this entire impl is to be trashed.
public function describeType(value:*): XML {
import __ruffle__.stub_method;
stub_method("flash.utils", "describeType");
var ret = new XML();
ret.name = getQualifiedClassName(value);
return ret;
}
public native function setInterval(closure:Function, delay:Number, ... arguments):uint; public native function setInterval(closure:Function, delay:Number, ... arguments):uint;
public native function clearInterval(id:uint):void; public native function clearInterval(id:uint):void;

View File

@ -3,6 +3,7 @@
use crate::avm2::object::TObject; use crate::avm2::object::TObject;
use crate::avm2::QName; use crate::avm2::QName;
use crate::avm2::{Activation, Error, Object, Value}; use crate::avm2::{Activation, Error, Object, Value};
use crate::avm2_stub_method;
use crate::string::AvmString; use crate::string::AvmString;
use crate::string::WString; use crate::string::WString;
use instant::Instant; use instant::Instant;
@ -260,3 +261,28 @@ pub fn get_definition_by_name<'gc>(
let qname = QName::from_qualified_name(name, activation); let qname = QName::from_qualified_name(name, activation);
appdomain.get_defined_value(activation, qname) appdomain.get_defined_value(activation, qname)
} }
// Implements `flash.utils.describeType`
pub fn describe_type<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
// This method is very incomplete, and should be fully implemented
// once we have a better way of constructing XML from the Rust side
avm2_stub_method!(activation, "flash.utils", "describeType");
let mut xml_string = String::new();
let qualified_name =
get_qualified_class_name(activation, None, &[args[0]])?.coerce_to_string(activation)?;
xml_string += &format!("<type name=\"{qualified_name}\"></type>");
let xml_avm_string = AvmString::new_utf8(activation.context.gc_context, xml_string);
Ok(activation
.avm2()
.classes()
.xml
.construct(activation, &[xml_avm_string.into()])?
.into())
}

View File

@ -1,3 +1,83 @@
//! XML builtin and prototype //! XML builtin and prototype
use crate::avm2::e4x::E4XNode;
pub use crate::avm2::object::xml_allocator; pub use crate::avm2::object::xml_allocator;
use crate::avm2::object::{QNameObject, TObject};
use crate::avm2::{Activation, Error, Object, QName, Value};
use crate::avm2_stub_method;
pub fn init<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.unwrap().as_xml_object().unwrap();
let value = args[0];
match E4XNode::parse(value, activation) {
Ok(nodes) => {
if nodes.len() != 1 {
return Err(Error::RustError(
format!(
"XML constructor must be called with a single node: found {:?}",
nodes
)
.into(),
));
}
this.set_node(activation.context.gc_context, nodes[0])
}
Err(e) => {
return Err(Error::RustError(
format!("Failed to parse XML: {e:?}").into(),
))
}
}
Ok(Value::Undefined)
}
pub fn name<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let node = this.unwrap().as_xml_object().unwrap();
if let Some(local_name) = node.local_name() {
avm2_stub_method!(activation, "XML", "name", "namespaces");
// FIXME - use namespace
let namespace = activation.avm2().public_namespace;
Ok(QNameObject::from_qname(activation, QName::new(namespace, local_name))?.into())
} else {
Ok(Value::Null)
}
}
pub fn local_name<'gc>(
_activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let node = this.unwrap().as_xml_object().unwrap();
Ok(node.local_name().map_or(Value::Null, Value::String))
}
pub fn to_string<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let xml = this.unwrap().as_xml_object().unwrap();
let node = xml.node();
Ok(Value::String(node.xml_to_string(activation)?))
}
pub fn to_xml_string<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let xml = this.unwrap().as_xml_object().unwrap();
let node = xml.node();
Ok(Value::String(node.xml_to_xml_string(activation)?))
}

View File

@ -1,4 +1,81 @@
//! XMLList builtin and prototype //! XMLList builtin and prototype
// XMLList currently uses the same instance allocator as XML pub use crate::avm2::object::xml_list_allocator;
pub use crate::avm2::object::xml_allocator as xml_list_allocator; use crate::{
avm2::{
e4x::{simple_content_to_string, E4XNode, E4XNodeKind},
object::E4XOrXml,
Activation, Error, Object, TObject, Value,
},
avm2_stub_method,
};
fn has_simple_content_inner(children: &[E4XOrXml<'_>]) -> bool {
match children {
[] => true,
[child] => child.node().has_simple_content(),
_ => children
.iter()
.all(|child| !matches!(&*child.node().kind(), E4XNodeKind::Element { .. })),
}
}
pub fn init<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.unwrap().as_xml_list_object().unwrap();
let value = args[0];
match E4XNode::parse(value, activation) {
Ok(nodes) => {
this.set_children(
activation.context.gc_context,
nodes.into_iter().map(E4XOrXml::E4X).collect(),
);
}
Err(e) => {
return Err(Error::RustError(
format!("Failed to parse XML: {e:?}").into(),
))
}
}
Ok(Value::Undefined)
}
pub fn has_simple_content<'gc>(
_activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let list = this.unwrap().as_xml_list_object().unwrap();
let children = list.children();
Ok(has_simple_content_inner(&children).into())
}
pub fn to_string<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let list = this.unwrap().as_xml_list_object().unwrap();
let children = list.children();
if has_simple_content_inner(&children) {
Ok(simple_content_to_string(children.iter().cloned(), activation)?.into())
} else {
avm2_stub_method!(activation, "XMLList", "toString", "non-simple content");
Err("XMLList.toString() for non-simple content: not yet implemented".into())
}
}
pub fn length<'gc>(
_activation: &mut Activation<'_, 'gc>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let list = this.unwrap().as_xml_list_object().unwrap();
let children = list.children();
Ok(children.len().into())
}

View File

@ -58,6 +58,8 @@ bitflags! {
/// Whether the name needs to be read at runtime before use /// Whether the name needs to be read at runtime before use
/// This should only be set when lazy-initialized in Activation. /// This should only be set when lazy-initialized in Activation.
const HAS_LAZY_NAME = 1 << 1; const HAS_LAZY_NAME = 1 << 1;
/// Whether this was a 'MultinameA' - used for XML attribute lookups
const ATTRIBUTE = 1 << 2;
} }
} }
@ -101,6 +103,11 @@ impl<'gc> Multiname<'gc> {
self.has_lazy_ns() || self.has_lazy_name() self.has_lazy_ns() || self.has_lazy_name()
} }
#[inline(always)]
pub fn is_attribute(&self) -> bool {
self.flags.contains(MultinameFlags::ATTRIBUTE)
}
/// Read a namespace set from the ABC constant pool, and return a list of /// Read a namespace set from the ABC constant pool, and return a list of
/// copied namespaces. /// copied namespaces.
fn abc_namespace_set( fn abc_namespace_set(
@ -146,7 +153,7 @@ impl<'gc> Multiname<'gc> {
let abc = translation_unit.abc(); let abc = translation_unit.abc();
let abc_multiname = Self::resolve_multiname_index(&abc, multiname_index)?; let abc_multiname = Self::resolve_multiname_index(&abc, multiname_index)?;
Ok(match abc_multiname { let mut multiname = match abc_multiname {
AbcMultiname::QName { namespace, name } | AbcMultiname::QNameA { namespace, name } => { AbcMultiname::QName { namespace, name } | AbcMultiname::QNameA { namespace, name } => {
Self { Self {
ns: NamespaceSet::single(translation_unit.pool_namespace(*namespace, mc)?), ns: NamespaceSet::single(translation_unit.pool_namespace(*namespace, mc)?),
@ -212,7 +219,19 @@ impl<'gc> Multiname<'gc> {
} }
base base
} }
}) };
if matches!(
abc_multiname,
AbcMultiname::QNameA { .. }
| AbcMultiname::RTQNameA { .. }
| AbcMultiname::RTQNameLA { .. }
| AbcMultiname::MultinameA { .. }
| AbcMultiname::MultinameLA { .. }
) {
multiname.flags |= MultinameFlags::ATTRIBUTE;
}
Ok(multiname)
} }
#[inline(never)] #[inline(never)]

View File

@ -55,6 +55,7 @@ mod stage_object;
mod textformat_object; mod textformat_object;
mod vector_object; mod vector_object;
mod vertex_buffer_3d_object; mod vertex_buffer_3d_object;
mod xml_list_object;
mod xml_object; mod xml_object;
pub use crate::avm2::object::array_object::{array_allocator, ArrayObject}; pub use crate::avm2::object::array_object::{array_allocator, ArrayObject};
@ -87,6 +88,7 @@ pub use crate::avm2::object::stage_object::{stage_allocator, StageObject};
pub use crate::avm2::object::textformat_object::{textformat_allocator, TextFormatObject}; pub use crate::avm2::object::textformat_object::{textformat_allocator, TextFormatObject};
pub use crate::avm2::object::vector_object::{vector_allocator, VectorObject}; pub use crate::avm2::object::vector_object::{vector_allocator, VectorObject};
pub use crate::avm2::object::vertex_buffer_3d_object::VertexBuffer3DObject; pub use crate::avm2::object::vertex_buffer_3d_object::VertexBuffer3DObject;
pub use crate::avm2::object::xml_list_object::{xml_list_allocator, E4XOrXml, XmlListObject};
pub use crate::avm2::object::xml_object::{xml_allocator, XmlObject}; pub use crate::avm2::object::xml_object::{xml_allocator, XmlObject};
/// Represents an object that can be directly interacted with by the AVM2 /// Represents an object that can be directly interacted with by the AVM2
@ -106,6 +108,7 @@ pub use crate::avm2::object::xml_object::{xml_allocator, XmlObject};
EventObject(EventObject<'gc>), EventObject(EventObject<'gc>),
DispatchObject(DispatchObject<'gc>), DispatchObject(DispatchObject<'gc>),
XmlObject(XmlObject<'gc>), XmlObject(XmlObject<'gc>),
XmlListObject(XmlListObject<'gc>),
RegExpObject(RegExpObject<'gc>), RegExpObject(RegExpObject<'gc>),
ByteArrayObject(ByteArrayObject<'gc>), ByteArrayObject(ByteArrayObject<'gc>),
LoaderInfoObject(LoaderInfoObject<'gc>), LoaderInfoObject(LoaderInfoObject<'gc>),
@ -165,6 +168,13 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
self.base().get_slot(slot_id) self.base().get_slot(slot_id)
} }
Some(Property::Method { disp_id }) => { Some(Property::Method { disp_id }) => {
// avmplus has a special case for XML and XMLList objects, so we need one as well
// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/Toplevel.cpp#L629-L634
if (self.as_xml_object().is_some() || self.as_xml_list_object().is_some())
&& multiname.contains_public_namespace()
{
return self.get_property_local(multiname, activation);
}
if let Some(bound_method) = self.get_bound_method(disp_id) { if let Some(bound_method) = self.get_bound_method(disp_id) {
return Ok(bound_method.into()); return Ok(bound_method.into());
} }
@ -1207,7 +1217,11 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
None None
} }
fn as_xml(&self) -> Option<XmlObject<'gc>> { fn as_xml_object(&self) -> Option<XmlObject<'gc>> {
None
}
fn as_xml_list_object(&self) -> Option<XmlListObject<'gc>> {
None None
} }

View File

@ -0,0 +1,198 @@
use crate::avm2::activation::Activation;
use crate::avm2::e4x::E4XNode;
use crate::avm2::object::script_object::ScriptObjectData;
use crate::avm2::object::{Object, ObjectPtr, TObject};
use crate::avm2::value::Value;
use crate::avm2::{Error, Multiname};
use gc_arena::{Collect, GcCell, MutationContext};
use std::cell::{Ref, RefMut};
use std::fmt::{self, Debug};
use std::ops::Deref;
use super::{ClassObject, XmlObject};
/// A class instance allocator that allocates XMLList objects.
pub fn xml_list_allocator<'gc>(
class: ClassObject<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Object<'gc>, Error<'gc>> {
let base = ScriptObjectData::new(class);
Ok(XmlListObject(GcCell::allocate(
activation.context.gc_context,
XmlListObjectData {
base,
children: Vec::new(),
},
))
.into())
}
#[derive(Clone, Collect, Copy)]
#[collect(no_drop)]
pub struct XmlListObject<'gc>(GcCell<'gc, XmlListObjectData<'gc>>);
impl<'gc> Debug for XmlListObject<'gc> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("XmlListObject")
.field("ptr", &self.0.as_ptr())
.finish()
}
}
impl<'gc> XmlListObject<'gc> {
pub fn new(activation: &mut Activation<'_, 'gc>, children: Vec<E4XOrXml<'gc>>) -> Self {
let base = ScriptObjectData::new(activation.context.avm2.classes().xml_list);
XmlListObject(GcCell::allocate(
activation.context.gc_context,
XmlListObjectData { base, children },
))
}
pub fn children(&self) -> Ref<'_, Vec<E4XOrXml<'gc>>> {
Ref::map(self.0.read(), |d| &d.children)
}
pub fn set_children(&self, mc: MutationContext<'gc, '_>, children: Vec<E4XOrXml<'gc>>) {
self.0.write(mc).children = children;
}
}
#[derive(Clone, Collect)]
#[collect(no_drop)]
pub struct XmlListObjectData<'gc> {
/// Base script object
base: ScriptObjectData<'gc>,
children: Vec<E4XOrXml<'gc>>,
}
/// Holds either an `E4XNode` or an `XmlObject`. This can be converted
/// in-palce to an `XmlObject` via `get_or_create_xml`.
/// This deliberately does not implement `Copy`, since `get_or_create_xml`
/// takes `&mut self`
#[derive(Clone, Collect, Debug)]
#[collect(no_drop)]
pub enum E4XOrXml<'gc> {
E4X(E4XNode<'gc>),
Xml(XmlObject<'gc>),
}
impl<'gc> E4XOrXml<'gc> {
pub fn get_or_create_xml(&mut self, activation: &mut Activation<'_, 'gc>) -> XmlObject<'gc> {
match self {
E4XOrXml::E4X(node) => {
let xml = XmlObject::new(*node, activation);
*self = E4XOrXml::Xml(xml);
xml
}
E4XOrXml::Xml(xml) => *xml,
}
}
pub fn node(&self) -> E4XWrapper<'_, 'gc> {
match self {
E4XOrXml::E4X(node) => E4XWrapper::E4X(*node),
E4XOrXml::Xml(xml) => E4XWrapper::XmlRef(xml.node()),
}
}
}
// Allows using `E4XOrXml` as an `E4XNode` via deref coercions, while
// storing the needed `Ref` wrappers
#[derive(Debug)]
pub enum E4XWrapper<'a, 'gc> {
E4X(E4XNode<'gc>),
XmlRef(Ref<'a, E4XNode<'gc>>),
}
impl<'a, 'gc> Deref for E4XWrapper<'a, 'gc> {
type Target = E4XNode<'gc>;
fn deref(&self) -> &Self::Target {
match self {
E4XWrapper::E4X(node) => node,
E4XWrapper::XmlRef(node) => node,
}
}
}
impl<'gc> TObject<'gc> for XmlListObject<'gc> {
fn base(&self) -> Ref<ScriptObjectData<'gc>> {
Ref::map(self.0.read(), |read| &read.base)
}
fn base_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut<ScriptObjectData<'gc>> {
RefMut::map(self.0.write(mc), |write| &mut write.base)
}
fn as_ptr(&self) -> *const ObjectPtr {
self.0.as_ptr() as *const ObjectPtr
}
fn value_of(&self, _mc: MutationContext<'gc, '_>) -> Result<Value<'gc>, Error<'gc>> {
Ok(Value::Object(Object::from(*self)))
}
fn as_xml_list_object(&self) -> Option<Self> {
Some(*self)
}
fn get_property_local(
self,
name: &Multiname<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
// FIXME - implement everything from E4X spec (XMLListObject::getMultinameProperty in avmplus)
let mut write = self.0.write(activation.context.gc_context);
if name.contains_public_namespace() {
if let Some(local_name) = name.local_name() {
if let Ok(index) = local_name.parse::<usize>() {
if let Some(child) = write.children.get_mut(index) {
return Ok(Value::Object(child.get_or_create_xml(activation).into()));
} else {
return Ok(Value::Undefined);
}
}
let matched_children = write
.children
.iter_mut()
.flat_map(|child| {
let child_prop = child
.get_or_create_xml(activation)
.get_property_local(name, activation)
.unwrap();
if let Some(prop_xml) =
child_prop.as_object().and_then(|obj| obj.as_xml_object())
{
vec![E4XOrXml::Xml(prop_xml)]
} else if let Some(prop_xml_list) = child_prop
.as_object()
.and_then(|obj| obj.as_xml_list_object())
{
// Flatten children
prop_xml_list.children().clone()
} else {
vec![]
}
})
.collect();
return Ok(XmlListObject::new(activation, matched_children).into());
}
}
write.base.get_property_local(name, activation)
}
fn set_property_local(
self,
_name: &Multiname<'gc>,
_value: Value<'gc>,
_activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
Err("Modifying an XMLList object is not yet implemented".into())
}
}

View File

@ -1,14 +1,18 @@
//! Object representation for XML objects //! Object representation for XML objects
use crate::avm2::activation::Activation; use crate::avm2::activation::Activation;
use crate::avm2::e4x::{E4XNode, E4XNodeKind};
use crate::avm2::object::script_object::ScriptObjectData; use crate::avm2::object::script_object::ScriptObjectData;
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject, XmlListObject};
use crate::avm2::string::AvmString;
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::Error; use crate::avm2::{Error, Multiname};
use core::fmt; use core::fmt;
use gc_arena::{Collect, GcCell, MutationContext}; use gc_arena::{Collect, GcCell, MutationContext};
use std::cell::{Ref, RefMut}; use std::cell::{Ref, RefMut};
use super::xml_list_object::E4XOrXml;
/// A class instance allocator that allocates XML objects. /// A class instance allocator that allocates XML objects.
pub fn xml_allocator<'gc>( pub fn xml_allocator<'gc>(
class: ClassObject<'gc>, class: ClassObject<'gc>,
@ -18,7 +22,10 @@ pub fn xml_allocator<'gc>(
Ok(XmlObject(GcCell::allocate( Ok(XmlObject(GcCell::allocate(
activation.context.gc_context, activation.context.gc_context,
XmlObjectData { base }, XmlObjectData {
base,
node: E4XNode::dummy(activation.context.gc_context),
},
)) ))
.into()) .into())
} }
@ -40,6 +47,35 @@ impl fmt::Debug for XmlObject<'_> {
pub struct XmlObjectData<'gc> { pub struct XmlObjectData<'gc> {
/// Base script object /// Base script object
base: ScriptObjectData<'gc>, base: ScriptObjectData<'gc>,
node: E4XNode<'gc>,
}
impl<'gc> XmlObject<'gc> {
pub fn new(node: E4XNode<'gc>, activation: &mut Activation<'_, 'gc>) -> Self {
XmlObject(GcCell::allocate(
activation.context.gc_context,
XmlObjectData {
base: ScriptObjectData::new(activation.context.avm2.classes().xml),
node,
},
))
}
pub fn set_node(&self, mc: MutationContext<'gc, '_>, node: E4XNode<'gc>) {
self.0.write(mc).node = node;
}
pub fn local_name(&self) -> Option<AvmString<'gc>> {
self.0.read().node.local_name()
}
pub fn matches_name(&self, multiname: &Multiname<'gc>) -> bool {
self.0.read().node.matches_name(multiname)
}
pub fn node(&self) -> Ref<'_, E4XNode<'gc>> {
Ref::map(self.0.read(), |data| &data.node)
}
} }
impl<'gc> TObject<'gc> for XmlObject<'gc> { impl<'gc> TObject<'gc> for XmlObject<'gc> {
@ -59,7 +95,66 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> {
Ok(Value::Object(Object::from(*self))) Ok(Value::Object(Object::from(*self)))
} }
fn as_xml(&self) -> Option<Self> { fn as_xml_object(&self) -> Option<Self> {
Some(*self) Some(*self)
} }
fn get_property_local(
self,
name: &Multiname<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
// FIXME - implement everything from E4X spec (XMLObject::getMultinameProperty in avmplus)
let read = self.0.read();
if name.contains_public_namespace() {
if let Some(local_name) = name.local_name() {
// The only supported numerical index is 0
if let Ok(index) = local_name.parse::<usize>() {
if index == 0 {
return Ok(self.into());
} else {
return Ok(Value::Undefined);
}
}
let matched_children = if let E4XNodeKind::Element {
children,
attributes,
} = &*read.node.kind()
{
let search_children = if name.is_attribute() {
attributes
} else {
children
};
search_children
.iter()
.filter_map(|child| {
if child.matches_name(name) {
Some(E4XOrXml::E4X(*child))
} else {
None
}
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
return Ok(XmlListObject::new(activation, matched_children).into());
}
}
read.base.get_property_local(name, activation)
}
fn set_property_local(
self,
_name: &Multiname<'gc>,
_value: Value<'gc>,
_activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
Err("Modifying an XML object is not yet implemented".into())
}
} }

View File

@ -14,6 +14,8 @@ use gc_arena::{Collect, MutationContext};
use std::cell::Ref; use std::cell::Ref;
use swf::avm2::types::{DefaultValue as AbcDefaultValue, Index}; use swf::avm2::types::{DefaultValue as AbcDefaultValue, Index};
use super::e4x::E4XNode;
/// Indicate what kind of primitive coercion would be preferred when coercing /// Indicate what kind of primitive coercion would be preferred when coercing
/// objects. /// objects.
#[derive(Eq, PartialEq)] #[derive(Eq, PartialEq)]
@ -1044,6 +1046,21 @@ impl<'gc> Value<'gc> {
} }
} }
/// Implements the strict-equality `===` check for AVM2.
pub fn strict_eq(&self, other: &Value<'gc>) -> bool {
if self == other {
true
} else {
// TODO - this should apply to (Array/Vector).indexOf, and possibility more places as well
if let Some(xml1) = self.as_object().and_then(|obj| obj.as_xml_object()) {
if let Some(xml2) = other.as_object().and_then(|obj| obj.as_xml_object()) {
return E4XNode::ptr_eq(*xml1.node(), *xml2.node());
}
}
false
}
}
/// Determine if two values are abstractly equal to each other. /// Determine if two values are abstractly equal to each other.
/// ///
/// This abstract equality algorithm is intended to match ECMA-262 3rd /// This abstract equality algorithm is intended to match ECMA-262 3rd

View File

@ -0,0 +1,62 @@
package {
public class Test {
public static function run() {
var soapXML:XML =
<soap:Envelope xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Body xmlns:wx = "http://example.com/weather">
<wx:forecast>
<wx:city>Quito</wx:city>
</wx:forecast>
</soap:Body>
</soap:Envelope>;
trace(soapXML.localName()); // Envelope
trace(XML.prototype.localName.call(soapXML));
var simpleXML:XML = <outerElem><innerElem><p>Hello world</p></innerElem></outerElem>;
trace("simpleXML.innerElem.p = " + simpleXML.innerElem.p);
trace("XML.prototype.toString() = " + XML.prototype.toString());
var noArgs = new XML();
trace("noArgs.toString() = " + noArgs.toString());
trace("XML.prototype.toString.call(noArgs): " + XML.prototype.toString.call(noArgs));
trace("noArgs.toXMLString() = " + noArgs.toXMLString());
var nullArg = new XML(null);
trace("nullArg.toString() = " + nullArg.toString());
trace("nullArg.toString() = " + nullArg.toXMLString());
var undefinedArg = new XML(undefined);
trace("undefinedArg.toString() = " + undefinedArg.toString());
trace("undefinedArg.toXMLString() = " + undefinedArg.toString());
var plainString:XML = new XML("Hello");
trace("plainString.toString() = " + plainString.toString());
trace("plainString.toXMLString() = " + plainString.toString());
var list = new XMLList("<p>First</p><p>Second</p>");
trace("List children: " + list.length());
trace("List first child: " + list[0]);
trace("List second child: " + list[1]);
var a = <a><x>asdf</x></a>;
var a1 = a.x;
var a2 = a1[0];
var b1 = a.x;
var b2 = b1[0];
trace("XMLList strict equal: " + (a1 === b1));
trace("XML strict equal: " + (a2 === b2));
var weird = <outer><name>My Name</name></outer>;
trace("Get 'name' property': " + weird.name);
trace("Get 'AS#::name' property': " + (typeof a.AS3::name));
// FIXME - enable this when Ruffle throws coercion errors
//XML.prototype.name.apply(5);
}
}
}

View File

@ -0,0 +1,20 @@
Envelope
Envelope
simpleXML.innerElem.p = Hello world
XML.prototype.toString() =
noArgs.toString() =
XML.prototype.toString.call(noArgs):
noArgs.toXMLString() =
nullArg.toString() =
nullArg.toString() =
undefinedArg.toString() =
undefinedArg.toXMLString() =
plainString.toString() = Hello
plainString.toXMLString() = Hello
List children: 2
List first child: First
List second child: Second
XMLList strict equal: false
XML strict equal: true
Get 'name' property': My Name
Get 'AS#::name' property': function

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_frames = 1