avm2: Implement avmplus.describeTypeJSON and use it for describeType

Some SWFS (in particular, anything using Unity) call
avmplus.describeTypeJSON, and rely on the behavior of the various
flags.

This PR changes our internal implementation to implement
describeTypeJSON (producing an `Object` with dynamic fields).
We then convert this to XML in `describeType`, using an implementation
inspired by avmplus.

The existing describeType tests are passing - in a follow-up PR,
I'll add tests for describeTypeJSON
This commit is contained in:
Aaron Hill 2023-09-04 17:11:01 -04:00 committed by Adrian Wielgosik
parent 2ccd45a2af
commit ee62044cad
6 changed files with 685 additions and 413 deletions

View File

@ -118,7 +118,7 @@ fn resolve_multiname_name<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a st
fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str { fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str {
if let Multiname::QName { namespace, .. } = multiname { if let Multiname::QName { namespace, .. } = multiname {
let ns = &abc.constant_pool.namespaces[namespace.0 as usize - 1]; let ns = &abc.constant_pool.namespaces[namespace.0 as usize - 1];
if let Namespace::Package(p) = ns { if let Namespace::Package(p) | Namespace::PackageInternal(p) = ns {
&abc.constant_pool.strings[p.0 as usize - 1] &abc.constant_pool.strings[p.0 as usize - 1]
} else { } else {
panic!("Unexpected Namespace {ns:?}"); panic!("Unexpected Namespace {ns:?}");

View File

@ -1,3 +1,133 @@
package avmplus { package avmplus {
public native function getQualifiedClassName(value:*):String; public native function getQualifiedClassName(value:*):String;
internal native function describeTypeJSON(o:*, flags:uint):Object;
public const HIDE_NSURI_METHODS:uint = 0x0001;
public const INCLUDE_BASES:uint = 0x0002;
public const INCLUDE_INTERFACES:uint = 0x0004;
public const INCLUDE_VARIABLES:uint = 0x0008;
public const INCLUDE_ACCESSORS:uint = 0x0010;
public const INCLUDE_METHODS:uint = 0x0020;
public const INCLUDE_METADATA:uint = 0x0040;
public const INCLUDE_CONSTRUCTOR:uint = 0x0080;
public const INCLUDE_TRAITS:uint = 0x0100;
public const USE_ITRAITS:uint = 0x0200;
public const HIDE_OBJECT:uint = 0x0400;
public const FLASH10_FLAGS:uint = INCLUDE_BASES |
INCLUDE_INTERFACES |
INCLUDE_VARIABLES |
INCLUDE_ACCESSORS |
INCLUDE_METHODS |
INCLUDE_METADATA |
INCLUDE_CONSTRUCTOR |
INCLUDE_TRAITS |
HIDE_NSURI_METHODS |
HIDE_OBJECT;
internal function copyParams(params: Object, xml: XML) {
for (var i in params) {
var param = params[i];
var elem = <parameter />;
elem.@index = i + 1;
elem.@type = param.type;
elem.@optional = param.optional;
xml.appendChild(elem);
}
}
internal function copyMetadata(metadata: Array, xml: XML) {
for each (var md in metadata) {
var data = <metadata />;
data.@name = md.name;
for each (var metaValue in md.value) {
var elem = <arg />;
elem.@key = metaValue.key;
elem.@value = metaValue.value;
data.appendChild(elem);
}
xml.appendChild(data);
}
}
internal function copyUriAndMetadata(data: Object, xml: XML) {
if (data.uri) {
xml.@uri = data.uri;
}
if (data.metadata) {
copyMetadata(data.metadata, xml)
}
}
internal function copyTraits(traits: Object, xml: XML) {
for each (var base in traits.bases) {
var elem = <extendsClass />;
elem.@type = base;
xml.AS3::appendChild(elem);
}
for each (var iface in traits.interfaces) {
var elem = <implementsInterface />;
elem.@type = iface;
xml.AS3::appendChild(elem);
}
if (traits.constructor) {
var constructor = <constructor />;
copyParams(traits.constructor, constructor);
xml.AS3::appendChild(constructor)
}
for each (var variable in traits.variables) {
var variableXML = (variable.access == "readonly") ? <constant /> : <variable />;
variableXML.@name = variable.name;
variableXML.@type = variable.type;
copyUriAndMetadata(variable, variableXML);
xml.AS3::appendChild(variableXML);
}
for each (var accessor in traits.accessors) {
var accessorXML = <accessor />;
accessorXML.@name = accessor.name;
accessorXML.@access = accessor.access;
accessorXML.@type = accessor.type;
accessorXML.@declaredBy = accessor.declaredBy;
copyUriAndMetadata(accessor, accessorXML);
xml.AS3::appendChild(accessorXML);
}
for each (var method in traits.methods) {
var methodXML = <method />;
methodXML.@name = method.name;
methodXML.@declaredBy = method.declaredBy;
methodXML.@returnType = method.returnType;
copyParams(method.parameters, methodXML);
copyUriAndMetadata(method, methodXML);
xml.AS3::appendChild(methodXML);
}
copyMetadata(traits.metadata, xml);
}
public function describeType(value: *, flags: uint):XML {
var json = describeTypeJSON(value, flags);
var xml = <type />;
xml.@name = json.name;
if (json.traits.bases.length != 0) {
xml.@base = json.traits.bases[0];
}
xml.@isDynamic = json.isDynamic;
xml.@isFinal = json.isFinal;
xml.@isStatic = json.isStatic;
copyTraits(json.traits, xml);
var jsonITraits = describeTypeJSON(value, flags | USE_ITRAITS);
if (jsonITraits) {
var factory = <factory />;
factory.@type = jsonITraits.name;
copyTraits(jsonITraits.traits, factory);
xml.appendChild(factory);
}
return xml;
}
} }

View File

@ -1 +1,519 @@
pub use crate::avm2::globals::flash::utils::get_qualified_class_name; pub use crate::avm2::globals::flash::utils::get_qualified_class_name;
use crate::avm2::metadata::Metadata;
use crate::avm2::method::Method;
use crate::avm2::object::TObject;
use crate::avm2::parameters::ParametersExt;
use crate::avm2::property::Property;
use crate::avm2::ClassObject;
use crate::avm2::{Activation, Error, Object, Value};
// Implements `avmplus.describeTypeJSON`
pub fn describe_type_json<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let value = args[0].coerce_to_object(activation)?;
let flags = DescribeTypeFlags::from_bits(args.get_u32(activation, 1)?).expect("Invalid flags!");
let class_obj = value.as_class_object().or_else(|| value.instance_of());
let object = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
let Some(class_obj) = class_obj else {
return Ok(Value::Null);
};
let is_static = value.as_class_object().is_some();
if !is_static && flags.contains(DescribeTypeFlags::USE_ITRAITS) {
return Ok(Value::Null);
}
let class = class_obj.inner_class_definition();
let class = class.read();
let qualified_name = class
.name()
.to_qualified_name(activation.context.gc_context);
object.set_public_property("name", qualified_name.into(), activation)?;
object.set_public_property(
"isDynamic",
(is_static || !class.is_sealed()).into(),
activation,
)?;
object.set_public_property(
"isFinal",
(is_static || class.is_final()).into(),
activation,
)?;
object.set_public_property("isStatic", is_static.into(), activation)?;
let traits = describe_internal_body(activation, class_obj, is_static, flags)?;
object.set_public_property("traits", traits.into(), activation)?;
Ok(object.into())
}
bitflags::bitflags! {
pub struct DescribeTypeFlags: u32 {
const HIDE_NSURI_METHODS = 1 << 0;
const INCLUDE_BASES = 1 << 1;
const INCLUDE_INTERFACES = 1 << 2;
const INCLUDE_VARIABLES = 1 << 3;
const INCLUDE_ACCESSORS = 1 << 4;
const INCLUDE_METHODS = 1 << 5;
const INCLUDE_METADATA = 1 << 6;
const INCLUDE_CONSTRUCTOR = 1 << 7;
const INCLUDE_TRAITS = 1 << 8;
const USE_ITRAITS = 1 << 9;
const HIDE_OBJECT = 1 << 10;
}
}
fn describe_internal_body<'gc>(
activation: &mut Activation<'_, 'gc>,
class_obj: ClassObject<'gc>,
is_static: bool,
flags: DescribeTypeFlags,
) -> Result<Object<'gc>, Error<'gc>> {
// If we were passed a non-ClassObject, or the caller specifically requested it, then
// look at the instance "traits" (our implementation is different than avmplus)
let use_instance_traits = !is_static || flags.contains(DescribeTypeFlags::USE_ITRAITS);
let traits = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
let bases = activation
.avm2()
.classes()
.array
.construct(activation, &[])?
.as_array_object()
.unwrap();
let interfaces = activation
.avm2()
.classes()
.array
.construct(activation, &[])?
.as_array_object()
.unwrap();
let variables = activation
.avm2()
.classes()
.array
.construct(activation, &[])?
.as_array_object()
.unwrap();
let accessors = activation
.avm2()
.classes()
.array
.construct(activation, &[])?
.as_array_object()
.unwrap();
let methods = activation
.avm2()
.classes()
.array
.construct(activation, &[])?
.as_array_object()
.unwrap();
traits.set_public_property("bases", bases.into(), activation)?;
traits.set_public_property("interfaces", interfaces.into(), activation)?;
traits.set_public_property("variables", variables.into(), activation)?;
traits.set_public_property("accessors", accessors.into(), activation)?;
traits.set_public_property("methods", methods.into(), activation)?;
let mut bases_array = bases
.as_array_storage_mut(activation.context.gc_context)
.unwrap();
let mut interfaces_array = interfaces
.as_array_storage_mut(activation.context.gc_context)
.unwrap();
let mut variables_array = variables
.as_array_storage_mut(activation.context.gc_context)
.unwrap();
let mut accessors_array = accessors
.as_array_storage_mut(activation.context.gc_context)
.unwrap();
let mut methods_array = methods
.as_array_storage_mut(activation.context.gc_context)
.unwrap();
let superclass = if use_instance_traits {
class_obj.superclass_object()
} else {
Some(activation.avm2().classes().class)
};
if flags.contains(DescribeTypeFlags::INCLUDE_BASES) {
let mut current_super_obj = superclass;
while let Some(super_obj) = current_super_obj {
let super_name = super_obj
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
bases_array.push(super_name.into());
current_super_obj = super_obj.superclass_object();
}
} else {
traits.set_public_property("bases", Value::Null, activation)?;
}
// When we're describing a Class object, we use the class vtable (which hides instance properties)
let vtable = if use_instance_traits {
class_obj.instance_vtable()
} else {
class_obj.class_vtable()
};
let super_vtable = if use_instance_traits {
class_obj.superclass_object().map(|c| c.instance_vtable())
} else {
class_obj.instance_of().map(|c| c.instance_vtable())
};
if flags.contains(DescribeTypeFlags::INCLUDE_INTERFACES) && use_instance_traits {
for interface in class_obj.interfaces() {
let interface_name = interface
.read()
.name()
.to_qualified_name(activation.context.gc_context);
interfaces_array.push(interface_name.into());
}
} else {
traits.set_public_property("interfaces", Value::Null, activation)?;
}
// Implement the weird 'HIDE_NSURI_METHODS' behavior from avmplus:
// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/TypeDescriber.cpp#L237
let mut skip_ns = Vec::new();
if let Some(super_vtable) = super_vtable {
for (_, ns, prop) in super_vtable.resolved_traits().iter() {
if !ns.as_uri().is_empty() {
if let Property::Method { disp_id } = prop {
let method = super_vtable
.get_full_method(*disp_id)
.unwrap_or_else(|| panic!("Missing method for id {disp_id:?}"));
let is_playerglobals = method
.class
.class_scope()
.domain()
.is_playerglobals_domain(activation);
if !skip_ns.contains(&(ns, is_playerglobals)) {
skip_ns.push((ns, is_playerglobals));
}
}
}
}
}
let class_is_playerglobals = class_obj
.class_scope()
.domain()
.is_playerglobals_domain(activation);
// FIXME - avmplus iterates over their own hashtable, so the order in the final XML
// is different
for (prop_name, ns, prop) in vtable.resolved_traits().iter() {
if !ns.is_public_ignoring_ns() {
continue;
}
// Hack around our lack of namespace versioning.
// This is hack to work around the fact that we don't have namespace versioning
// Once we do, methods from playerglobals should end up distinct public and AS3
// namespaces, due to the special `kApiVersion_VM_ALLVERSIONS` used:
// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/AbcParser.cpp#L1497
//
// The main way this is
// observable is by having a class like this:
//
// ``
// class SubClass extends SuperClass {
// AS3 function subclassMethod {}
// }
// class SuperClass {}
// ```
//
// Here, `subclassMethod` will not get hidden - even though `Object`
// has AS3 methods, they are in the playerglobal AS3 namespace
// (with version kApiVersion_VM_ALLVERSIONS), which is distinct
// from the AS3 namespace used by SubClass. However, if we have any
// user-defined classes in the inheritance chain, then the namespace
// *should* match (if the swf version numbers match).
//
// For now, we approximate this by checking if the declaring class
// and our starting class are both in the playerglobals domain
// or both not in the playerglobals domain. If not, then we ignore
// `skip_ns`, since we should really have two different namespaces here.
if flags.contains(DescribeTypeFlags::HIDE_NSURI_METHODS)
&& skip_ns.contains(&(ns, class_is_playerglobals))
{
continue;
}
let uri = if ns.as_uri().is_empty() {
None
} else {
Some(ns.as_uri())
};
match prop {
Property::ConstSlot { slot_id } | Property::Slot { slot_id } => {
let prop_class_name = vtable
.slot_class_name(*slot_id, activation.context.gc_context)?
.to_qualified_name_or_star(activation.context.gc_context);
let access = match prop {
Property::ConstSlot { .. } => "readonly",
Property::Slot { .. } => "readwrite",
_ => unreachable!(),
};
let trait_metadata = vtable.get_metadata_for_slot(slot_id);
let variable = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
variable.set_public_property("name", prop_name.into(), activation)?;
variable.set_public_property("type", prop_class_name.into(), activation)?;
variable.set_public_property("access", access.into(), activation)?;
if let Some(metadata) = trait_metadata {
let metadata_object = activation
.avm2()
.classes()
.array
.construct(activation, &[])?;
write_metadata(metadata_object, &metadata, activation)?;
variable.set_public_property("metadata", metadata_object.into(), activation)?;
}
variables_array.push(variable.into());
}
Property::Method { disp_id } => {
let method = vtable
.get_full_method(*disp_id)
.unwrap_or_else(|| panic!("Missing method for id {disp_id:?}"));
let return_type_name = method
.method
.return_type()
.to_qualified_name_or_star(activation.context.gc_context);
let declared_by = method.class;
if flags.contains(DescribeTypeFlags::HIDE_OBJECT)
&& declared_by == activation.avm2().classes().object
{
continue;
}
let declared_by_name = declared_by
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
let trait_metadata = vtable.get_metadata_for_disp(disp_id);
let method_obj = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
method_obj.set_public_property("name", prop_name.into(), activation)?;
method_obj.set_public_property(
"returnType",
return_type_name.into(),
activation,
)?;
method_obj.set_public_property(
"declaredBy",
declared_by_name.into(),
activation,
)?;
if let Some(uri) = uri {
method_obj.set_public_property("uri", uri.into(), activation)?;
}
let params = write_params(&method.method, activation)?;
method_obj.set_public_property("parameters", params.into(), activation)?;
if let Some(metadata) = trait_metadata {
let metadata_object = activation
.avm2()
.classes()
.array
.construct(activation, &[])?;
write_metadata(metadata_object, &metadata, activation)?;
method_obj.set_public_property(
"metadata",
metadata_object.into(),
activation,
)?;
}
methods_array.push(method_obj.into());
}
Property::Virtual { get, set } => {
let access = match (get, set) {
(Some(_), Some(_)) => "readwrite",
(Some(_), None) => "readonly",
(None, Some(_)) => "writeonly",
(None, None) => unreachable!(),
};
// For getters, obtain the type by looking at the getter return type.
// For setters, obtain the type by looking at the setter's first parameter.
let (method_type, defining_class) = if let Some(get) = get {
let getter = vtable
.get_full_method(*get)
.unwrap_or_else(|| panic!("Missing 'get' method for id {get:?}"));
(getter.method.return_type(), getter.class)
} else if let Some(set) = set {
let setter = vtable
.get_full_method(*set)
.unwrap_or_else(|| panic!("Missing 'set' method for id {set:?}"));
(
setter.method.signature()[0].param_type_name.clone(),
setter.class,
)
} else {
unreachable!();
};
let uri = if ns.as_uri().is_empty() {
None
} else {
Some(ns.as_uri())
};
let accessor_type =
method_type.to_qualified_name_or_star(activation.context.gc_context);
let declared_by = defining_class
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
let accessor_obj = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
accessor_obj.set_public_property("name", prop_name.into(), activation)?;
accessor_obj.set_public_property("access", access.into(), activation)?;
accessor_obj.set_public_property("type", accessor_type.into(), activation)?;
accessor_obj.set_public_property("declaredBy", declared_by.into(), activation)?;
if let Some(uri) = uri {
accessor_obj.set_public_property("uri", uri.into(), activation)?;
}
let metadata_object = activation
.avm2()
.classes()
.array
.construct(activation, &[])?;
if let Some(get_disp_id) = get {
if let Some(metadata) = vtable.get_metadata_for_disp(get_disp_id) {
write_metadata(metadata_object, &metadata, activation)?;
}
}
if let Some(set_disp_id) = set {
if let Some(metadata) = vtable.get_metadata_for_disp(set_disp_id) {
write_metadata(metadata_object, &metadata, activation)?;
}
}
if metadata_object.as_array_storage().unwrap().length() > 0 {
accessor_obj.set_public_property(
"metadata",
metadata_object.into(),
activation,
)?;
}
accessors_array.push(accessor_obj.into());
}
}
}
let constructor = class_obj.constructor();
// Flash only shows a <constructor> element if it has at least one parameter
if flags.contains(DescribeTypeFlags::INCLUDE_CONSTRUCTOR)
&& use_instance_traits
&& !constructor.signature().is_empty()
{
let params = write_params(&constructor, activation)?;
traits.set_public_property("constructor", params.into(), activation)?;
} else {
// This is needed to override the normal 'constructor' property
traits.set_public_property("constructor", Value::Null, activation)?;
}
Ok(traits)
}
fn write_params<'gc>(
method: &Method<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<Object<'gc>, Error<'gc>> {
let params = activation
.avm2()
.classes()
.array
.construct(activation, &[])?;
let mut params_array = params
.as_array_storage_mut(activation.context.gc_context)
.unwrap();
for (i, param) in method.signature().iter().enumerate() {
let index = i + 1;
let param_type_name = param
.param_type_name
.to_qualified_name_or_star(activation.context.gc_context);
let optional = param.default_value.is_some();
let param_obj = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
param_obj.set_public_property("index", index.into(), activation)?;
param_obj.set_public_property("type", param_type_name.into(), activation)?;
param_obj.set_public_property("optional", optional.into(), activation)?;
params_array.push(param_obj.into());
}
Ok(params)
}
fn write_metadata<'gc>(
metadata_object: Object<'gc>,
trait_metadata: &[Metadata<'gc>],
activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
let mut metadata_array = metadata_object
.as_array_storage_mut(activation.context.gc_context)
.unwrap();
for single_trait in trait_metadata.iter() {
metadata_array.push(single_trait.as_json_object(activation)?.into());
}
Ok(())
}

View File

@ -1,10 +1,13 @@
package flash.utils { package flash.utils {
public native function getDefinitionByName(name:String):Object; public native function getDefinitionByName(name:String):Object;
public native function getQualifiedClassName(value:*):String; public native function getQualifiedClassName(value:*):String;
public native function getQualifiedSuperclassName(value:*):String; public native function getQualifiedSuperclassName(value:*):String;
public native function getTimer():int; public native function getTimer():int;
public native function describeType(value:*): XML; public function describeType(value:*): XML {
return avmplus.describeType(value, avmplus.FLASH10_FLAGS);
}
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

@ -1,10 +1,7 @@
//! `flash.utils` namespace //! `flash.utils` namespace
use crate::avm2::metadata::Metadata;
use crate::avm2::method::Method;
use crate::avm2::object::TObject; use crate::avm2::object::TObject;
use crate::avm2::property::Property;
use crate::avm2::ClassObject;
use crate::avm2::{Activation, Error, Object, Value}; use crate::avm2::{Activation, Error, Object, Value};
use crate::string::AvmString; use crate::string::AvmString;
use crate::string::WString; use crate::string::WString;
@ -264,388 +261,3 @@ pub fn get_definition_by_name<'gc>(
.coerce_to_string(activation)?; .coerce_to_string(activation)?;
appdomain.get_defined_value_handling_vector(activation, name) appdomain.get_defined_value_handling_vector(activation, name)
} }
// Implements `flash.utils.describeType`
pub fn describe_type<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let value = args[0].coerce_to_object(activation)?;
let class_obj = value.as_class_object().or_else(|| value.instance_of());
let Some(class_obj) = class_obj else {
return Ok(activation
.avm2()
.classes()
.xml
.construct(activation, &[])?
.into());
};
let mut xml_string = String::new();
let is_static = value.as_class_object().is_some();
let class = class_obj.inner_class_definition();
let class = class.read();
let qualified_name = class
.name()
.to_qualified_name(activation.context.gc_context);
// If we're describing a Class object, then the "superclass" the the Class class
let superclass = if is_static {
Some(activation.avm2().classes().class)
} else {
class_obj.superclass_object()
};
let base_attr = if let Some(superclass) = superclass {
format!(
" base=\"{}\"",
superclass
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context)
)
} else {
String::new()
};
let is_dynamic = is_static || !class.is_sealed();
let is_final = is_static || class.is_final();
write!(xml_string, "<type name=\"{qualified_name}\"{base_attr} isDynamic=\"{is_dynamic}\" isFinal=\"{is_final}\" isStatic=\"{is_static}\">").unwrap();
xml_string += &describe_internal_body(activation, class_obj, is_static)?;
xml_string += "</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())
}
bitflags::bitflags! {
pub struct DescribeTypeFlags: u32 {
const HIDE_NSURI_METHODS = 1 << 0;
const INCLUDE_BASES = 1 << 1;
const INCLUDE_INTERFACES = 1 << 2;
const INCLUDE_VARIABLES = 1 << 3;
const INCLUDE_ACCESSORS = 1 << 4;
const INCLUDE_METHODS = 1 << 5;
const INCLUDE_METADATA = 1 << 6;
const INCLUDE_CONSTRUCTOR = 1 << 7;
const INCLUDE_TRAITS = 1 << 8;
const USE_ITRAITS = 1 << 9;
const HIDE_OBJECT = 1 << 10;
}
}
fn describe_internal_body<'gc>(
activation: &mut Activation<'_, 'gc>,
class_obj: ClassObject<'gc>,
is_static: bool,
) -> Result<String, Error<'gc>> {
// This should be a const, but we can't use BitOr for bitflags in a const.
let flash10_flags = DescribeTypeFlags::INCLUDE_BASES
| DescribeTypeFlags::INCLUDE_INTERFACES
| DescribeTypeFlags::INCLUDE_VARIABLES
| DescribeTypeFlags::INCLUDE_ACCESSORS
| DescribeTypeFlags::INCLUDE_METHODS
| DescribeTypeFlags::INCLUDE_METADATA
| DescribeTypeFlags::INCLUDE_CONSTRUCTOR
| DescribeTypeFlags::INCLUDE_TRAITS
| DescribeTypeFlags::HIDE_NSURI_METHODS
| DescribeTypeFlags::HIDE_OBJECT;
// FIXME - take this in from `avmplus.describeTypeJSON`
let flags = flash10_flags;
let mut xml_string = String::new();
let class = class_obj.inner_class_definition();
let class = class.read();
let qualified_name = class
.name()
.to_qualified_name(activation.context.gc_context);
// If we're describing a Class object, then the "superclass" the the Class class
let superclass = if is_static {
Some(activation.avm2().classes().class)
} else {
class_obj.superclass_object()
};
let mut current_super_obj = superclass;
while let Some(super_obj) = current_super_obj {
let super_name = super_obj
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
write!(xml_string, "<extendsClass type=\"{super_name}\"/>").unwrap();
current_super_obj = super_obj.superclass_object();
}
// When we're describing a Class object, we use the class vtable (which hides instance properties)
let vtable = if is_static {
class_obj.class_vtable()
} else {
class_obj.instance_vtable()
};
let super_vtable = if is_static {
class_obj.instance_of().map(|c| c.instance_vtable())
} else {
class_obj.superclass_object().map(|c| c.instance_vtable())
};
if !is_static {
for interface in class_obj.interfaces() {
let interface_name = interface
.read()
.name()
.to_qualified_name(activation.context.gc_context);
write!(
xml_string,
"<implementsInterface type=\"{interface_name}\"/>"
)
.unwrap();
}
}
// Implement the weird 'HIDE_NSURI_METHODS' behavior from avmplus:
// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/TypeDescriber.cpp#L237
let mut skip_ns = Vec::new();
if let Some(super_vtable) = super_vtable {
for (_, ns, prop) in super_vtable.resolved_traits().iter() {
if !ns.as_uri().is_empty() {
if let Property::Method { disp_id } = prop {
let method = super_vtable
.get_full_method(*disp_id)
.unwrap_or_else(|| panic!("Missing method for id {disp_id:?}"));
let is_playerglobals = method
.class
.class_scope()
.domain()
.is_playerglobals_domain(activation);
if !skip_ns.contains(&(ns, is_playerglobals)) {
skip_ns.push((ns, is_playerglobals));
}
}
}
}
}
let class_is_playerglobals = class_obj
.class_scope()
.domain()
.is_playerglobals_domain(activation);
// FIXME - avmplus iterates over their own hashtable, so the order in the final XML
// is different
for (prop_name, ns, prop) in vtable.resolved_traits().iter() {
if !ns.is_public_ignoring_ns() {
continue;
}
// Hack around our lack of namespace versioning.
// This is hack to work around the fact that we don't have namespace versioning
// Once we do, methods from playerglobals should end up distinct public and AS3
// namespaces, due to the special `kApiVersion_VM_ALLVERSIONS` used:
// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/AbcParser.cpp#L1497
//
// The main way this is
// observable is by having a class like this:
//
// ``
// class SubClass extends SuperClass {
// AS3 function subclassMethod {}
// }
// class SuperClass {}
// ```
//
// Here, `subclassMethod` will not get hidden - even though `Object`
// has AS3 methods, they are in the playerglobal AS3 namespace
// (with version kApiVersion_VM_ALLVERSIONS), which is distinct
// from the AS3 namespace used by SubClass. However, if we have any
// user-defined classes in the inheritance chain, then the namespace
// *should* match (if the swf version numbers match).
//
// For now, we approximate this by checking if the declaring class
// and our starting class are both in the playerglobals domain
// or both not in the playerglobals domain. If not, then we ignore
// `skip_ns`, since we should really have two different namespaces here.
if flags.contains(DescribeTypeFlags::HIDE_NSURI_METHODS)
&& skip_ns.contains(&(ns, class_is_playerglobals))
{
continue;
}
let uri = if ns.as_uri().is_empty() {
String::new()
} else {
format!("uri=\"{}\"", ns.as_uri())
};
match prop {
Property::ConstSlot { slot_id } | Property::Slot { slot_id } => {
let prop_class_name = vtable
.slot_class_name(*slot_id, activation.context.gc_context)?
.to_qualified_name_or_star(activation.context.gc_context);
let elem_name = match prop {
Property::ConstSlot { .. } => "constant",
Property::Slot { .. } => "variable",
_ => unreachable!(),
};
let trait_metadata = vtable.get_metadata_for_slot(slot_id);
write!(
xml_string,
"<{elem_name} name=\"{prop_name}\" type=\"{prop_class_name}\">"
)
.unwrap();
if let Some(metadata) = trait_metadata {
write_metadata(&mut xml_string, &metadata);
}
write!(xml_string, "</{elem_name}>").unwrap();
}
Property::Method { disp_id } => {
let method = vtable
.get_full_method(*disp_id)
.unwrap_or_else(|| panic!("Missing method for id {disp_id:?}"));
let return_type_name = method
.method
.return_type()
.to_qualified_name_or_star(activation.context.gc_context);
let declared_by = method.class;
if flags.contains(DescribeTypeFlags::HIDE_OBJECT)
&& declared_by == activation.avm2().classes().object
{
continue;
}
let declared_by_name = declared_by
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
let trait_metadata = vtable.get_metadata_for_disp(disp_id);
write!(xml_string, "<method name=\"{prop_name}\" declaredBy=\"{declared_by_name}\" returnType=\"{return_type_name}\" {uri}>").unwrap();
write_params(&mut xml_string, &method.method, activation);
if let Some(metadata) = trait_metadata {
write_metadata(&mut xml_string, &metadata);
}
xml_string += "</method>";
}
Property::Virtual { get, set } => {
let access = match (get, set) {
(Some(_), Some(_)) => "readwrite",
(Some(_), None) => "readonly",
(None, Some(_)) => "writeonly",
(None, None) => unreachable!(),
};
// For getters, obtain the type by looking at the getter return type.
// For setters, obtain the type by looking at the setter's first parameter.
let (method_type, defining_class) = if let Some(get) = get {
let getter = vtable
.get_full_method(*get)
.unwrap_or_else(|| panic!("Missing 'get' method for id {get:?}"));
(getter.method.return_type(), getter.class)
} else if let Some(set) = set {
let setter = vtable
.get_full_method(*set)
.unwrap_or_else(|| panic!("Missing 'set' method for id {set:?}"));
(
setter.method.signature()[0].param_type_name.clone(),
setter.class,
)
} else {
unreachable!();
};
let uri = if ns.as_uri().is_empty() {
String::new()
} else {
format!("uri=\"{}\"", ns.as_uri())
};
let accessor_type =
method_type.to_qualified_name_or_star(activation.context.gc_context);
let declared_by = defining_class
.inner_class_definition()
.read()
.name()
.to_qualified_name(activation.context.gc_context);
write!(xml_string, "<accessor name=\"{prop_name}\" access=\"{access}\" type=\"{accessor_type}\" declaredBy=\"{declared_by}\" {uri}>").unwrap();
if let Some(get_disp_id) = get {
if let Some(metadata) = vtable.get_metadata_for_disp(get_disp_id) {
write_metadata(&mut xml_string, &metadata);
}
}
if let Some(set_disp_id) = set {
if let Some(metadata) = vtable.get_metadata_for_disp(set_disp_id) {
write_metadata(&mut xml_string, &metadata);
}
}
write!(xml_string, "</accessor>").unwrap();
}
}
}
let constructor = class_obj.constructor();
// Flash only shows a <constructor> element if it has at least one parameter
if !is_static && !constructor.signature().is_empty() {
xml_string += "<constructor>";
write_params(&mut xml_string, &constructor, activation);
xml_string += "</constructor>";
}
// If we're describing a Class object, add a <factory> element describing the instance.
if is_static {
write!(xml_string, "<factory type=\"{qualified_name}\">").unwrap();
xml_string += &describe_internal_body(activation, class_obj, false)?;
xml_string += "</factory>";
}
Ok(xml_string)
}
fn write_params<'gc>(
xml_string: &mut String,
method: &Method<'gc>,
activation: &mut Activation<'_, 'gc>,
) {
for (i, param) in method.signature().iter().enumerate() {
let index = i + 1;
let param_type_name = param
.param_type_name
.to_qualified_name_or_star(activation.context.gc_context);
let optional = param.default_value.is_some();
write!(
xml_string,
"<parameter index=\"{index}\" type=\"{param_type_name}\" optional=\"{optional}\"/>"
)
.unwrap();
}
}
fn write_metadata(xml_string: &mut String, trait_metadata: &[Metadata<'_>]) {
for single_trait in trait_metadata.iter() {
write!(xml_string, "{}", single_trait.as_xml_string()).unwrap();
}
}

View File

@ -3,9 +3,10 @@ use crate::avm2::{Activation, Error};
use crate::string::AvmString; use crate::string::AvmString;
use gc_arena::Collect; use gc_arena::Collect;
use std::fmt::Write;
use swf::avm2::types::{Index as AbcIndex, Metadata as AbcMetadata}; use swf::avm2::types::{Index as AbcIndex, Metadata as AbcMetadata};
use super::{ArrayObject, ArrayStorage, Object, TObject, Value};
// Represents a single key-value pair for a trait metadata. // Represents a single key-value pair for a trait metadata.
#[derive(Clone, Collect, Debug, Eq, PartialEq)] #[derive(Clone, Collect, Debug, Eq, PartialEq)]
#[collect(no_drop)] #[collect(no_drop)]
@ -71,28 +72,36 @@ impl<'gc> Metadata<'gc> {
Ok(Some(trait_metadata_list.into_boxed_slice())) Ok(Some(trait_metadata_list.into_boxed_slice()))
} }
// Converts the Metadata to an XML string of the form used in flash.utils:describeType(). // Converts the Metadata to an Object of the form used in avmplus:describeTypeJSON().
pub fn as_xml_string(&self) -> String { pub fn as_json_object(
let mut xml_string = String::new(); &self,
if self.items.is_empty() { activation: &mut Activation<'_, 'gc>,
// This was in the form of [metadata] ) -> Result<Object<'gc>, Error<'gc>> {
write!(xml_string, "<metadata name=\"{}\"/>", self.name).unwrap(); let object = activation
} else { .avm2()
// This was in the form of [metadata(key="value", otherkey="othervalue")] .classes()
write!(xml_string, "<metadata name=\"{}\">", self.name).unwrap(); .object
.construct(activation, &[])?;
object.set_public_property("name", self.name.into(), activation)?;
for item in self.items.iter() { let values = self
write!( .items
xml_string, .iter()
"<arg key=\"{}\" value=\"{}\"/>", .map(|item| {
item.key, item.value, let value_object = activation
) .avm2()
.unwrap(); .classes()
} .object
.construct(activation, &[])?;
value_object.set_public_property("key", item.key.into(), activation)?;
value_object.set_public_property("value", item.value.into(), activation)?;
Ok(Some(value_object.into()))
})
.collect::<Result<Vec<Option<Value<'gc>>>, Error<'gc>>>()?;
write!(xml_string, "</metadata>").unwrap(); let values_array =
}; ArrayObject::from_storage(activation, ArrayStorage::from_storage(values))?;
object.set_public_property("value", values_array.into(), activation)?;
xml_string Ok(object)
} }
} }