From ee62044cadd9856d86f296e7d52e64e673895b92 Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Mon, 4 Sep 2023 17:11:01 -0400 Subject: [PATCH] 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 --- core/build_playerglobal/src/lib.rs | 2 +- core/src/avm2/globals/avmplus.as | 130 +++++++ core/src/avm2/globals/avmplus.rs | 518 +++++++++++++++++++++++++++ core/src/avm2/globals/flash/utils.as | 5 +- core/src/avm2/globals/flash/utils.rs | 390 +------------------- core/src/avm2/metadata.rs | 53 +-- 6 files changed, 685 insertions(+), 413 deletions(-) diff --git a/core/build_playerglobal/src/lib.rs b/core/build_playerglobal/src/lib.rs index 2fa7570ae..58343a1d7 100644 --- a/core/build_playerglobal/src/lib.rs +++ b/core/build_playerglobal/src/lib.rs @@ -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 { if let Multiname::QName { namespace, .. } = multiname { 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] } else { panic!("Unexpected Namespace {ns:?}"); diff --git a/core/src/avm2/globals/avmplus.as b/core/src/avm2/globals/avmplus.as index f115d20ef..4233fa969 100644 --- a/core/src/avm2/globals/avmplus.as +++ b/core/src/avm2/globals/avmplus.as @@ -1,3 +1,133 @@ package avmplus { 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 = ; + 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 = ; + data.@name = md.name; + for each (var metaValue in md.value) { + var elem = ; + 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 = ; + elem.@type = base; + xml.AS3::appendChild(elem); + } + for each (var iface in traits.interfaces) { + var elem = ; + elem.@type = iface; + xml.AS3::appendChild(elem); + } + if (traits.constructor) { + var constructor = ; + copyParams(traits.constructor, constructor); + xml.AS3::appendChild(constructor) + } + + for each (var variable in traits.variables) { + var variableXML = (variable.access == "readonly") ? : ; + variableXML.@name = variable.name; + variableXML.@type = variable.type; + copyUriAndMetadata(variable, variableXML); + xml.AS3::appendChild(variableXML); + } + + for each (var accessor in traits.accessors) { + var accessorXML = ; + 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 = ; + 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 = ; + 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.@type = jsonITraits.name; + copyTraits(jsonITraits.traits, factory); + xml.appendChild(factory); + } + + return xml; + } } \ No newline at end of file diff --git a/core/src/avm2/globals/avmplus.rs b/core/src/avm2/globals/avmplus.rs index c9f090e21..209444723 100644 --- a/core/src/avm2/globals/avmplus.rs +++ b/core/src/avm2/globals/avmplus.rs @@ -1 +1,519 @@ 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, 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, 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 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, 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(()) +} diff --git a/core/src/avm2/globals/flash/utils.as b/core/src/avm2/globals/flash/utils.as index 1340f5187..fa43e6a8a 100644 --- a/core/src/avm2/globals/flash/utils.as +++ b/core/src/avm2/globals/flash/utils.as @@ -1,10 +1,13 @@ package flash.utils { + public native function getDefinitionByName(name:String):Object; public native function getQualifiedClassName(value:*):String; public native function getQualifiedSuperclassName(value:*):String; 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 clearInterval(id:uint):void; diff --git a/core/src/avm2/globals/flash/utils.rs b/core/src/avm2/globals/flash/utils.rs index eb3243218..7c1d13bea 100644 --- a/core/src/avm2/globals/flash/utils.rs +++ b/core/src/avm2/globals/flash/utils.rs @@ -1,10 +1,7 @@ //! `flash.utils` namespace -use crate::avm2::metadata::Metadata; -use crate::avm2::method::Method; use crate::avm2::object::TObject; -use crate::avm2::property::Property; -use crate::avm2::ClassObject; + use crate::avm2::{Activation, Error, Object, Value}; use crate::string::AvmString; use crate::string::WString; @@ -264,388 +261,3 @@ pub fn get_definition_by_name<'gc>( .coerce_to_string(activation)?; 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, 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, "").unwrap(); - xml_string += &describe_internal_body(activation, class_obj, is_static)?; - xml_string += ""; - - 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> { - // 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, "").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, - "" - ) - .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, "").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, "").unwrap(); - write_params(&mut xml_string, &method.method, activation); - if let Some(metadata) = trait_metadata { - write_metadata(&mut xml_string, &metadata); - } - xml_string += ""; - } - 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, "").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, "").unwrap(); - } - } - } - - let constructor = class_obj.constructor(); - // Flash only shows a element if it has at least one parameter - if !is_static && !constructor.signature().is_empty() { - xml_string += ""; - write_params(&mut xml_string, &constructor, activation); - xml_string += ""; - } - - // If we're describing a Class object, add a element describing the instance. - if is_static { - write!(xml_string, "").unwrap(); - xml_string += &describe_internal_body(activation, class_obj, false)?; - xml_string += ""; - } - 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, - "" - ) - .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(); - } -} diff --git a/core/src/avm2/metadata.rs b/core/src/avm2/metadata.rs index 6ed6ca84e..1e777ca04 100644 --- a/core/src/avm2/metadata.rs +++ b/core/src/avm2/metadata.rs @@ -3,9 +3,10 @@ use crate::avm2::{Activation, Error}; use crate::string::AvmString; use gc_arena::Collect; -use std::fmt::Write; 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. #[derive(Clone, Collect, Debug, Eq, PartialEq)] #[collect(no_drop)] @@ -71,28 +72,36 @@ impl<'gc> Metadata<'gc> { Ok(Some(trait_metadata_list.into_boxed_slice())) } - // Converts the Metadata to an XML string of the form used in flash.utils:describeType(). - pub fn as_xml_string(&self) -> String { - let mut xml_string = String::new(); - if self.items.is_empty() { - // This was in the form of [metadata] - write!(xml_string, "", self.name).unwrap(); - } else { - // This was in the form of [metadata(key="value", otherkey="othervalue")] - write!(xml_string, "", self.name).unwrap(); + // Converts the Metadata to an Object of the form used in avmplus:describeTypeJSON(). + pub fn as_json_object( + &self, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + let object = activation + .avm2() + .classes() + .object + .construct(activation, &[])?; + object.set_public_property("name", self.name.into(), activation)?; - for item in self.items.iter() { - write!( - xml_string, - "", - item.key, item.value, - ) - .unwrap(); - } + let values = self + .items + .iter() + .map(|item| { + let value_object = activation + .avm2() + .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::>>, Error<'gc>>>()?; - write!(xml_string, "").unwrap(); - }; - - xml_string + let values_array = + ArrayObject::from_storage(activation, ArrayStorage::from_storage(values))?; + object.set_public_property("value", values_array.into(), activation)?; + Ok(object) } }