avm2: Initial incomplete implementation of XML (#9647)
This commit is contained in:
parent
a36ef7fd0c
commit
b26f2fd6fb
|
@ -28,6 +28,7 @@ pub mod bytearray;
|
|||
mod call_stack;
|
||||
mod class;
|
||||
mod domain;
|
||||
mod e4x;
|
||||
pub mod error;
|
||||
mod events;
|
||||
mod function;
|
||||
|
|
|
@ -2619,8 +2619,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
|
|||
fn op_strict_equals(&mut self) -> Result<FrameControl<'gc>, Error<'gc>> {
|
||||
let value2 = self.pop_stack();
|
||||
let value1 = self.pop_stack();
|
||||
|
||||
self.push_stack(value1 == value2);
|
||||
self.push_stack(value1.strict_eq(&value2));
|
||||
|
||||
Ok(FrameControl::Continue)
|
||||
}
|
||||
|
@ -2877,7 +2876,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
|
|||
"object"
|
||||
}
|
||||
}
|
||||
Object::XmlObject(_) => {
|
||||
Object::XmlObject(_) | Object::XmlListObject(_) => {
|
||||
if is_not_subclass {
|
||||
"xml"
|
||||
} else {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -239,3 +239,11 @@ impl<'gc> From<ruffle_render::error::Error> for Error<'gc> {
|
|||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,44 @@
|
|||
package {
|
||||
[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();
|
||||
};
|
||||
|
||||
}
|
||||
}
|
|
@ -1,4 +1,16 @@
|
|||
package {
|
||||
[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;
|
||||
}
|
||||
}
|
|
@ -4,18 +4,7 @@ package flash.utils {
|
|||
public native function getQualifiedSuperclassName(value:*):String;
|
||||
public native function getTimer():int;
|
||||
|
||||
// note: this is an extremely silly hack,
|
||||
// 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 describeType(value:*): XML;
|
||||
|
||||
public native function setInterval(closure:Function, delay:Number, ... arguments):uint;
|
||||
public native function clearInterval(id:uint):void;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use crate::avm2::object::TObject;
|
||||
use crate::avm2::QName;
|
||||
use crate::avm2::{Activation, Error, Object, Value};
|
||||
use crate::avm2_stub_method;
|
||||
use crate::string::AvmString;
|
||||
use crate::string::WString;
|
||||
use instant::Instant;
|
||||
|
@ -260,3 +261,28 @@ pub fn get_definition_by_name<'gc>(
|
|||
let qname = QName::from_qualified_name(name, activation);
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -1,3 +1,83 @@
|
|||
//! XML builtin and prototype
|
||||
|
||||
use crate::avm2::e4x::E4XNode;
|
||||
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)?))
|
||||
}
|
||||
|
|
|
@ -1,4 +1,81 @@
|
|||
//! XMLList builtin and prototype
|
||||
|
||||
// XMLList currently uses the same instance allocator as XML
|
||||
pub use crate::avm2::object::xml_allocator as xml_list_allocator;
|
||||
pub use crate::avm2::object::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())
|
||||
}
|
||||
|
|
|
@ -58,6 +58,8 @@ bitflags! {
|
|||
/// Whether the name needs to be read at runtime before use
|
||||
/// This should only be set when lazy-initialized in Activation.
|
||||
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()
|
||||
}
|
||||
|
||||
#[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
|
||||
/// copied namespaces.
|
||||
fn abc_namespace_set(
|
||||
|
@ -146,7 +153,7 @@ impl<'gc> Multiname<'gc> {
|
|||
let abc = translation_unit.abc();
|
||||
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 } => {
|
||||
Self {
|
||||
ns: NamespaceSet::single(translation_unit.pool_namespace(*namespace, mc)?),
|
||||
|
@ -212,7 +219,19 @@ impl<'gc> Multiname<'gc> {
|
|||
}
|
||||
base
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if matches!(
|
||||
abc_multiname,
|
||||
AbcMultiname::QNameA { .. }
|
||||
| AbcMultiname::RTQNameA { .. }
|
||||
| AbcMultiname::RTQNameLA { .. }
|
||||
| AbcMultiname::MultinameA { .. }
|
||||
| AbcMultiname::MultinameLA { .. }
|
||||
) {
|
||||
multiname.flags |= MultinameFlags::ATTRIBUTE;
|
||||
}
|
||||
Ok(multiname)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
|
|
|
@ -55,6 +55,7 @@ mod stage_object;
|
|||
mod textformat_object;
|
||||
mod vector_object;
|
||||
mod vertex_buffer_3d_object;
|
||||
mod xml_list_object;
|
||||
mod xml_object;
|
||||
|
||||
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::vector_object::{vector_allocator, VectorObject};
|
||||
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};
|
||||
|
||||
/// 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>),
|
||||
DispatchObject(DispatchObject<'gc>),
|
||||
XmlObject(XmlObject<'gc>),
|
||||
XmlListObject(XmlListObject<'gc>),
|
||||
RegExpObject(RegExpObject<'gc>),
|
||||
ByteArrayObject(ByteArrayObject<'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)
|
||||
}
|
||||
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) {
|
||||
return Ok(bound_method.into());
|
||||
}
|
||||
|
@ -1207,7 +1217,11 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -1,14 +1,18 @@
|
|||
//! Object representation for XML objects
|
||||
|
||||
use crate::avm2::activation::Activation;
|
||||
use crate::avm2::e4x::{E4XNode, E4XNodeKind};
|
||||
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::Error;
|
||||
use crate::avm2::{Error, Multiname};
|
||||
use core::fmt;
|
||||
use gc_arena::{Collect, GcCell, MutationContext};
|
||||
use std::cell::{Ref, RefMut};
|
||||
|
||||
use super::xml_list_object::E4XOrXml;
|
||||
|
||||
/// A class instance allocator that allocates XML objects.
|
||||
pub fn xml_allocator<'gc>(
|
||||
class: ClassObject<'gc>,
|
||||
|
@ -18,7 +22,10 @@ pub fn xml_allocator<'gc>(
|
|||
|
||||
Ok(XmlObject(GcCell::allocate(
|
||||
activation.context.gc_context,
|
||||
XmlObjectData { base },
|
||||
XmlObjectData {
|
||||
base,
|
||||
node: E4XNode::dummy(activation.context.gc_context),
|
||||
},
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
@ -40,6 +47,35 @@ impl fmt::Debug for XmlObject<'_> {
|
|||
pub struct XmlObjectData<'gc> {
|
||||
/// Base script object
|
||||
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> {
|
||||
|
@ -59,7 +95,66 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> {
|
|||
Ok(Value::Object(Object::from(*self)))
|
||||
}
|
||||
|
||||
fn as_xml(&self) -> Option<Self> {
|
||||
fn as_xml_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 (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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ use gc_arena::{Collect, MutationContext};
|
|||
use std::cell::Ref;
|
||||
use swf::avm2::types::{DefaultValue as AbcDefaultValue, Index};
|
||||
|
||||
use super::e4x::E4XNode;
|
||||
|
||||
/// Indicate what kind of primitive coercion would be preferred when coercing
|
||||
/// objects.
|
||||
#[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.
|
||||
///
|
||||
/// This abstract equality algorithm is intended to match ECMA-262 3rd
|
||||
|
|
Binary file not shown.
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
@ -0,0 +1 @@
|
|||
num_frames = 1
|
Loading…
Reference in New Issue