avm2: Implement property type coercions

Properties can be declared with a type
(e.g. `var foo:MyClass = new MyClass();`). When
`set_property`/`init_property` is invoked for that property,
the VM will attempt to coerce the value to the provided type,
throwing an error if this fails. This can have observable behavior
consequences - if a property has type `integer`, for example, then
storing a floating point `Number` to that property will cause the
value to be coerced to an integer. Some SWFs (e.g. 'Solarmax') rely
on this behavior in order to implicitly coerce a floating point value
that's later used for array indexing.

This PR implements property type coercions in Ruffle. There are several
important considerations:

* The class lookup for property types needs to be done lazily, since
we can have a cycle between two classes (e.g. `var prop1:Class2;`
and `var prop2:Class1` in two different classes).
* The class lookup uses special rules (different from
  `resolve_definition`), and does *not* use `ScopeStack/`ScopeTree`
This means that a private class can specified as a property name -
the lookup will succeed without using a scope, even though
`flash.utils.getDefinitionByName` would fail with the same name
* The specialized 'Vector' classes (e.g "Vector$int") can be used
as property types, even though they cannot be lookup up normally.

Some Ruffle class definitions were previously using nonexistent
classes as property types (e.g. "BareObject") - these are fixed
in this PR.
This commit is contained in:
Aaron Hill 2022-06-30 23:34:26 -05:00 committed by GitHub
parent c7bf11ece5
commit b56eace008
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 417 additions and 63 deletions

View File

@ -756,6 +756,20 @@ impl<'gc> Class<'gc> {
} }
} }
#[inline(never)]
pub fn define_public_slot_instance_traits_type_multiname(
&mut self,
items: &[(&'static str, Multiname<'gc>)],
) {
for (name, type_name) in items {
self.define_instance_trait(Trait::from_slot(
QName::new(Namespace::public(), *name),
type_name.clone(),
None,
));
}
}
#[inline(never)] #[inline(never)]
pub fn define_private_slot_instance_traits( pub fn define_private_slot_instance_traits(
&mut self, &mut self,

View File

@ -40,7 +40,7 @@ mod xml;
mod xml_list; mod xml_list;
pub(crate) const NS_RUFFLE_INTERNAL: &str = "https://ruffle.rs/AS3/impl/"; pub(crate) const NS_RUFFLE_INTERNAL: &str = "https://ruffle.rs/AS3/impl/";
const NS_VECTOR: &str = "__AS3__.vec"; pub(crate) const NS_VECTOR: &str = "__AS3__.vec";
pub use flash::utils::NS_FLASH_PROXY; pub use flash::utils::NS_FLASH_PROXY;
@ -177,7 +177,7 @@ fn function<'gc>(
let method = Method::from_builtin(nf, name, mc); let method = Method::from_builtin(nf, name, mc);
let as3fn = FunctionObject::from_method(activation, method, scope, None, None).into(); let as3fn = FunctionObject::from_method(activation, method, scope, None, None).into();
domain.export_definition(qname, script, mc)?; domain.export_definition(qname, script, mc)?;
global.install_const_late(mc, qname, as3fn); global.install_const_late(mc, qname, as3fn, activation.avm2().classes().function);
Ok(()) Ok(())
} }
@ -190,12 +190,14 @@ fn dynamic_class<'gc>(
mc: MutationContext<'gc, '_>, mc: MutationContext<'gc, '_>,
class_object: ClassObject<'gc>, class_object: ClassObject<'gc>,
script: Script<'gc>, script: Script<'gc>,
// The `ClassObject` of the `Class` class
class_class: ClassObject<'gc>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (_, mut global, mut domain) = script.init(); let (_, mut global, mut domain) = script.init();
let class = class_object.inner_class_definition(); let class = class_object.inner_class_definition();
let name = class.read().name(); let name = class.read().name();
global.install_const_late(mc, name, class_object.into()); global.install_const_late(mc, name, class_object.into(), class_class);
domain.export_definition(name, script, mc) domain.export_definition(name, script, mc)
} }
@ -243,6 +245,7 @@ fn class<'gc>(
activation.context.gc_context, activation.context.gc_context,
class_name, class_name,
class_object.into(), class_object.into(),
activation.avm2().classes().class,
); );
domain.export_definition(class_name, script, activation.context.gc_context)?; domain.export_definition(class_name, script, activation.context.gc_context)?;
@ -256,11 +259,12 @@ fn constant<'gc>(
name: impl Into<AvmString<'gc>>, name: impl Into<AvmString<'gc>>,
value: Value<'gc>, value: Value<'gc>,
script: Script<'gc>, script: Script<'gc>,
class: ClassObject<'gc>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (_, mut global, mut domain) = script.init(); let (_, mut global, mut domain) = script.init();
let name = QName::new(Namespace::package(package), name); let name = QName::new(Namespace::package(package), name);
domain.export_definition(name, script, mc)?; domain.export_definition(name, script, mc)?;
global.install_const_late(mc, name, value); global.install_const_late(mc, name, value, class);
Ok(()) Ok(())
} }
@ -279,6 +283,7 @@ fn namespace<'gc>(
name, name,
namespace.into(), namespace.into(),
script, script,
activation.avm2().classes().namespace,
) )
} }
@ -383,15 +388,13 @@ pub fn load_player_globals<'gc>(
// From this point, `globals` is safe to be modified // From this point, `globals` is safe to be modified
dynamic_class(mc, object_class, script)?; dynamic_class(mc, object_class, script, class_class)?;
dynamic_class(mc, fn_class, script)?; dynamic_class(mc, fn_class, script, class_class)?;
dynamic_class(mc, class_class, script)?; dynamic_class(mc, class_class, script, class_class)?;
// After this point, it is safe to initialize any other classes. // After this point, it is safe to initialize any other classes.
// Make sure to initialize superclasses *before* their subclasses! // Make sure to initialize superclasses *before* their subclasses!
load_playerglobal(activation, domain)?;
avm2_system_class!(string, activation, string::create_class(mc), script); avm2_system_class!(string, activation, string::create_class(mc), script);
avm2_system_class!(boolean, activation, boolean::create_class(mc), script); avm2_system_class!(boolean, activation, boolean::create_class(mc), script);
avm2_system_class!(number, activation, number::create_class(mc), script); avm2_system_class!(number, activation, number::create_class(mc), script);
@ -407,10 +410,17 @@ pub fn load_player_globals<'gc>(
function(activation, "", "parseInt", toplevel::parse_int, script)?; function(activation, "", "parseInt", toplevel::parse_int, script)?;
function(activation, "", "parseFloat", toplevel::parse_float, script)?; function(activation, "", "parseFloat", toplevel::parse_float, script)?;
function(activation, "", "escape", toplevel::escape, script)?; function(activation, "", "escape", toplevel::escape, script)?;
constant(mc, "", "undefined", Value::Undefined, script)?; constant(mc, "", "undefined", Value::Undefined, script, object_class)?;
constant(mc, "", "null", Value::Null, script)?; constant(mc, "", "null", Value::Null, script, object_class)?;
constant(mc, "", "NaN", f64::NAN.into(), script)?; constant(mc, "", "NaN", f64::NAN.into(), script, object_class)?;
constant(mc, "", "Infinity", f64::INFINITY.into(), script)?; constant(
mc,
"",
"Infinity",
f64::INFINITY.into(),
script,
object_class,
)?;
class(activation, math::create_class(mc), script)?; class(activation, math::create_class(mc), script)?;
class(activation, json::create_class(mc), script)?; class(activation, json::create_class(mc), script)?;
@ -784,6 +794,11 @@ pub fn load_player_globals<'gc>(
script, script,
)?; )?;
// Inside this call, the macro `avm2_system_classes_playerglobal`
// triggers classloading. Therefore, we run `load_playerglobal` last,
// so that all of our classes have been defined.
load_playerglobal(activation, domain)?;
Ok(()) Ok(())
} }

View File

@ -5,7 +5,6 @@ use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::events::{ use crate::avm2::events::{
dispatch_event as dispatch_event_internal, parent_of, NS_EVENT_DISPATCHER, dispatch_event as dispatch_event_internal, parent_of, NS_EVENT_DISPATCHER,
}; };
use crate::avm2::globals::NS_RUFFLE_INTERNAL;
use crate::avm2::method::{Method, NativeMethodImpl}; use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::names::{Namespace, QName}; use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::{DispatchObject, Object, TObject}; use crate::avm2::object::{DispatchObject, Object, TObject};
@ -271,12 +270,12 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
write.define_instance_trait(Trait::from_slot( write.define_instance_trait(Trait::from_slot(
QName::new(Namespace::private(NS_EVENT_DISPATCHER), "target"), QName::new(Namespace::private(NS_EVENT_DISPATCHER), "target"),
QName::new(Namespace::private(NS_RUFFLE_INTERNAL), "BareObject").into(), QName::new(Namespace::public(), "Object").into(),
None, None,
)); ));
write.define_instance_trait(Trait::from_slot( write.define_instance_trait(Trait::from_slot(
QName::new(Namespace::private(NS_EVENT_DISPATCHER), "dispatch_list"), QName::new(Namespace::private(NS_EVENT_DISPATCHER), "dispatch_list"),
QName::new(Namespace::private(NS_RUFFLE_INTERNAL), "BareObject").into(), QName::new(Namespace::public(), "Object").into(),
None, None,
)); ));

View File

@ -3,7 +3,7 @@
use crate::avm2::activation::Activation; use crate::avm2::activation::Activation;
use crate::avm2::class::Class; use crate::avm2::class::Class;
use crate::avm2::method::{Method, NativeMethodImpl}; use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::names::{Namespace, QName}; use crate::avm2::names::{Multiname, Namespace, QName};
use crate::avm2::object::TObject; use crate::avm2::object::TObject;
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::{Error, Object}; use crate::avm2::{Error, Object};
@ -74,7 +74,12 @@ fn bytes_total<'gc>(
if let Some(array) = data.as_bytearray() { if let Some(array) = data.as_bytearray() {
return Ok(array.len().into()); return Ok(array.len().into());
} else { } else {
return Err(format!("Unexpected value for `data` property: {:?}", data).into()); return Err(format!(
"Unexpected value for `data` property: {:?} {:?}",
data,
data.as_primitive()
)
.into());
} }
} else if let Value::String(data) = data { } else if let Value::String(data) = data {
return Ok(data.len().into()); return Ok(data.len().into());
@ -160,10 +165,13 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
]; ];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES); write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
const PUBLIC_INSTANCE_SLOTS: &[(&str, &str, &str)] = const PUBLIC_INSTANCE_SLOTS: &[(&str, &str, &str)] = &[("dataFormat", "", "String")];
&[("data", "", "Object"), ("dataFormat", "", "String")];
write.define_public_slot_instance_traits(PUBLIC_INSTANCE_SLOTS); write.define_public_slot_instance_traits(PUBLIC_INSTANCE_SLOTS);
// This can't be a constant, due to the inability to declare `Multiname<'gc>`
let public_instance_slots_any = &[("data", Multiname::any())];
write.define_public_slot_instance_traits_type_multiname(public_instance_slots_any);
const PUBLIC_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = &[("load", load)]; const PUBLIC_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = &[("load", load)];
write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS); write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS);

View File

@ -103,6 +103,7 @@ pub fn class_init<'gc>(
.get_defining_script(&QName::new(Namespace::public(), "Object").into())? .get_defining_script(&QName::new(Namespace::public(), "Object").into())?
.unwrap(); .unwrap();
let class_class = activation.avm2().classes().class;
let int_class = activation.avm2().classes().int; let int_class = activation.avm2().classes().int;
let int_vector_class = this.apply(activation, &[int_class.into()])?; let int_vector_class = this.apply(activation, &[int_class.into()])?;
let int_vector_name = QName::new(Namespace::internal(NS_VECTOR), "Vector$int"); let int_vector_name = QName::new(Namespace::internal(NS_VECTOR), "Vector$int");
@ -115,6 +116,7 @@ pub fn class_init<'gc>(
activation.context.gc_context, activation.context.gc_context,
int_vector_name, int_vector_name,
int_vector_class.into(), int_vector_class.into(),
class_class,
); );
domain.export_definition(int_vector_name, script, activation.context.gc_context)?; domain.export_definition(int_vector_name, script, activation.context.gc_context)?;
@ -130,6 +132,7 @@ pub fn class_init<'gc>(
activation.context.gc_context, activation.context.gc_context,
uint_vector_name, uint_vector_name,
uint_vector_class.into(), uint_vector_class.into(),
class_class,
); );
domain.export_definition(uint_vector_name, script, activation.context.gc_context)?; domain.export_definition(uint_vector_name, script, activation.context.gc_context)?;
@ -145,6 +148,7 @@ pub fn class_init<'gc>(
activation.context.gc_context, activation.context.gc_context,
number_vector_name, number_vector_name,
number_vector_class.into(), number_vector_class.into(),
class_class,
); );
domain.export_definition(number_vector_name, script, activation.context.gc_context)?; domain.export_definition(number_vector_name, script, activation.context.gc_context)?;
@ -159,6 +163,7 @@ pub fn class_init<'gc>(
activation.context.gc_context, activation.context.gc_context,
object_vector_name, object_vector_name,
object_vector_class.into(), object_vector_class.into(),
class_class,
); );
domain.export_definition(object_vector_name, script, activation.context.gc_context)?; domain.export_definition(object_vector_name, script, activation.context.gc_context)?;
} }

View File

@ -141,9 +141,8 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) { match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) {
Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => { Some(Property::Slot { slot_id, class: _ })
self.base().get_slot(slot_id) | Some(Property::ConstSlot { slot_id, class: _ }) => self.base().get_slot(slot_id),
}
Some(Property::Method { disp_id }) => { Some(Property::Method { disp_id }) => {
if let Some(bound_method) = self.get_bound_method(disp_id) { if let Some(bound_method) = self.get_bound_method(disp_id) {
return Ok(bound_method.into()); return Ok(bound_method.into());
@ -197,9 +196,14 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
) -> Result<(), Error> { ) -> Result<(), Error> {
match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) { match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) {
Some(Property::Slot { slot_id }) => self Some(Property::Slot { slot_id, mut class }) => {
.base_mut(activation.context.gc_context) let value = class.coerce(activation, value)?;
.set_slot(slot_id, value, activation.context.gc_context), self.base_mut(activation.context.gc_context).set_slot(
slot_id,
value,
activation.context.gc_context,
)
}
Some(Property::ConstSlot { .. }) => Err("Illegal write to read-only property".into()), Some(Property::ConstSlot { .. }) => Err("Illegal write to read-only property".into()),
Some(Property::Method { .. }) => Err("Cannot assign to a method".into()), Some(Property::Method { .. }) => Err("Cannot assign to a method".into()),
Some(Property::Virtual { set: Some(set), .. }) => { Some(Property::Virtual { set: Some(set), .. }) => {
@ -242,9 +246,15 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
) -> Result<(), Error> { ) -> Result<(), Error> {
match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) { match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) {
Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => self Some(Property::Slot { slot_id, mut class })
.base_mut(activation.context.gc_context) | Some(Property::ConstSlot { slot_id, mut class }) => {
.set_slot(slot_id, value, activation.context.gc_context), let value = class.coerce(activation, value)?;
self.base_mut(activation.context.gc_context).set_slot(
slot_id,
value,
activation.context.gc_context,
)
}
Some(Property::Method { .. }) => Err("Cannot assign to a method".into()), Some(Property::Method { .. }) => Err("Cannot assign to a method".into()),
Some(Property::Virtual { set: Some(set), .. }) => { Some(Property::Virtual { set: Some(set), .. }) => {
self.call_method(set, &[value], activation).map(|_| ()) self.call_method(set, &[value], activation).map(|_| ())
@ -292,7 +302,8 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) { match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) {
Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => { Some(Property::Slot { slot_id, class: _ })
| Some(Property::ConstSlot { slot_id, class: _ }) => {
let obj = self.base().get_slot(slot_id)?.as_callable( let obj = self.base().get_slot(slot_id)?.as_callable(
activation, activation,
Some(multiname), Some(multiname),
@ -595,11 +606,12 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
mc: MutationContext<'gc, '_>, mc: MutationContext<'gc, '_>,
name: QName<'gc>, name: QName<'gc>,
value: Value<'gc>, value: Value<'gc>,
class: ClassObject<'gc>,
) { ) {
let new_slot_id = self let new_slot_id = self
.vtable() .vtable()
.unwrap() .unwrap()
.install_const_trait_late(mc, name, value); .install_const_trait_late(mc, name, value, class);
self.base_mut(mc) self.base_mut(mc)
.install_const_slot_late(new_slot_id, value); .install_const_slot_late(new_slot_id, value);
} }

View File

@ -13,6 +13,7 @@ use crate::avm2::scope::{Scope, ScopeChain};
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::vtable::{ClassBoundMethod, VTable}; use crate::avm2::vtable::{ClassBoundMethod, VTable};
use crate::avm2::Error; use crate::avm2::Error;
use crate::avm2::TranslationUnit;
use crate::string::AvmString; use crate::string::AvmString;
use fnv::FnvHashMap; use fnv::FnvHashMap;
use gc_arena::{Collect, GcCell, MutationContext}; use gc_arena::{Collect, GcCell, MutationContext};
@ -689,6 +690,14 @@ impl<'gc> ClassObject<'gc> {
} }
} }
pub fn translation_unit(self) -> Option<TranslationUnit<'gc>> {
if let Method::Bytecode(bc) = self.0.read().constructor {
Some(bc.txunit)
} else {
None
}
}
pub fn instance_vtable(self) -> VTable<'gc> { pub fn instance_vtable(self) -> VTable<'gc> {
self.0.read().instance_vtable self.0.read().instance_vtable
} }

View File

@ -1,17 +1,160 @@
//! Property data structures //! Property data structures
use gc_arena::Collect; use crate::avm2::names::Multiname;
use crate::avm2::object::TObject;
use crate::avm2::Activation;
use crate::avm2::ClassObject;
use crate::avm2::Error;
use crate::avm2::TranslationUnit;
use crate::avm2::Value;
use gc_arena::{Collect, Gc};
#[derive(Debug, Collect, Clone, Copy)] #[derive(Debug, Collect, Clone, Copy)]
#[collect(require_static)] #[collect(no_drop)]
pub enum Property { pub enum Property<'gc> {
Virtual { get: Option<u32>, set: Option<u32> }, Virtual {
Method { disp_id: u32 }, get: Option<u32>,
Slot { slot_id: u32 }, set: Option<u32>,
ConstSlot { slot_id: u32 }, },
Method {
disp_id: u32,
},
Slot {
slot_id: u32,
class: PropertyClass<'gc>,
},
ConstSlot {
slot_id: u32,
class: PropertyClass<'gc>,
},
} }
impl Property { /// The type of a `Slot`/`ConstSlot` property, represented
/// as a lazily-resolved class. This also implements the
/// property-specific coercion logic applied when setting
/// or initializing a property.
///
/// The class resolution needs to be lazy, since we can have
/// a cycle of property type references between classes
/// (e.g. Class1 has `var prop1:Class2`, and Class2
/// has `var prop2:Class1`).
///
/// Additionally, property class resolution uses special
/// logic, different from normal "runtime" class resolution,
/// that allows private types to be referenced.
#[derive(Debug, Collect, Clone, Copy)]
#[collect(no_drop)]
pub enum PropertyClass<'gc> {
/// The type `*` (Multiname::is_any()). This allows
/// `Value::Undefined`, so it needs to be distinguished
/// from the `Object` class
Any,
Class(ClassObject<'gc>),
Name(Gc<'gc, (Multiname<'gc>, Option<TranslationUnit<'gc>>)>),
}
impl<'gc> PropertyClass<'gc> {
pub fn name(
activation: &mut Activation<'_, 'gc, '_>,
name: Multiname<'gc>,
unit: Option<TranslationUnit<'gc>>,
) -> Self {
PropertyClass::Name(Gc::allocate(activation.context.gc_context, (name, unit)))
}
pub fn coerce(
&mut self,
activation: &mut Activation<'_, 'gc, '_>,
value: Value<'gc>,
) -> Result<Value<'gc>, Error> {
let class = match self {
PropertyClass::Class(class) => Some(*class),
PropertyClass::Name(gc) => {
let (name, unit) = &**gc;
let class = resolve_class_private(&name, *unit, activation)?;
*self = match class {
Some(class) => PropertyClass::Class(class),
None => PropertyClass::Any,
};
class
}
PropertyClass::Any => None,
};
if let Some(class) = class {
value.coerce_to_type(activation, class)
} else {
// We have a type of '*' ("any"), so don't
// perform any coercions
Ok(value)
}
}
}
/// Resolves a class definition referenced by the type of a property.
/// This supports private (`Namespace::Private`) classes,
/// and does not use the `ScopeStack`/`ScopeChain`.
///
/// This is an internal operation used to resolve property type names.
/// It does not correspond to any opcode or native method.
fn resolve_class_private<'gc>(
name: &Multiname<'gc>,
unit: Option<TranslationUnit<'gc>>,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Option<ClassObject<'gc>>, Error> {
// A Property may have a type of '*' (which corresponds to 'Multiname::any()')
// We don't want to perform any coercions in this case - in particular,
// this means that the property can have a value of `Undefined`.
// If the type is `Object`, then a value of `Undefind` gets coerced
// to `Null`
if name.is_any() {
Ok(None)
} else {
// First, check the domain for an exported (non-private) class.
// If the property we're resolving for lacks a `TranslationUnit`,
// then it must have come from `load_player_globals`, so we use
// the top-level `Domain`
let domain = unit.map_or(activation.avm2().globals, |u| u.domain());
let globals = if let Some((_, mut script)) = domain.get_defining_script(name)? {
script.globals(&mut activation.context)?
} else if let Some(txunit) = unit {
// If we couldn't find an exported symbol, then check for a
// private trait in the translation unit. This kind of trait
// is inaccessible to `resolve_class`.
//
// Subtle: `get_loaded_private_trait_script` requires us to have already
// performed the lazy-loading of `script.globals` for the correct script.
// Since we are setting/initializing a property with an instance of
// the class `name`, user bytecode must have already initialized
// the `ClassObject` in order to have created the value we're setting.
// Therefore, we don't need to run `script.globals()` for every script
// in the `TranslationUnit` - we're guaranteed to have already loaded
// the proper script.
txunit
.get_loaded_private_trait_script(name)
.ok_or_else(|| {
Error::from(format!("Could not find script for class trait {:?}", name))
})?
.globals(&mut activation.context)?
} else {
return Err(format!("Missing script and translation unit for class {:?}", name).into());
};
Ok(Some(
globals
.get_property(name, activation)?
.as_object()
.and_then(|o| o.as_class_object())
.ok_or_else(|| {
Error::from(format!(
"Attempted to perform (private) resolution of nonexistent type {:?}",
name
))
})?,
))
}
}
impl<'gc> Property<'gc> {
pub fn new_method(disp_id: u32) -> Self { pub fn new_method(disp_id: u32) -> Self {
Property::Method { disp_id } Property::Method { disp_id }
} }
@ -30,11 +173,11 @@ impl Property {
} }
} }
pub fn new_slot(slot_id: u32) -> Self { pub fn new_slot(slot_id: u32, class: PropertyClass<'gc>) -> Self {
Property::Slot { slot_id } Property::Slot { slot_id, class }
} }
pub fn new_const_slot(slot_id: u32) -> Self { pub fn new_const_slot(slot_id: u32, class: PropertyClass<'gc>) -> Self {
Property::ConstSlot { slot_id } Property::ConstSlot { slot_id, class }
} }
} }

View File

@ -4,7 +4,9 @@ use crate::avm2::activation::Activation;
use crate::avm2::class::Class; use crate::avm2::class::Class;
use crate::avm2::domain::Domain; use crate::avm2::domain::Domain;
use crate::avm2::method::{BytecodeMethod, Method}; use crate::avm2::method::{BytecodeMethod, Method};
use crate::avm2::names::Multiname;
use crate::avm2::object::{Object, TObject}; use crate::avm2::object::{Object, TObject};
use crate::avm2::property_map::PropertyMap;
use crate::avm2::scope::ScopeChain; use crate::avm2::scope::ScopeChain;
use crate::avm2::traits::Trait; use crate::avm2::traits::Trait;
use crate::avm2::value::Value; use crate::avm2::value::Value;
@ -55,6 +57,23 @@ pub struct TranslationUnitData<'gc> {
/// All strings loaded from the ABC's strings list. /// All strings loaded from the ABC's strings list.
/// They're lazy loaded and offset by 1, with the 0th element being always None. /// They're lazy loaded and offset by 1, with the 0th element being always None.
strings: Vec<Option<AvmString<'gc>>>, strings: Vec<Option<AvmString<'gc>>>,
/// A map from trait names to their defining `Scripts`.
/// This is very similar to `Domain.defs`, except it
/// only stores traits with `Namespace::Private`, which are
/// not exported/stored in `Domain`.
///
/// This should only be used in very specific circumstances -
/// such as resolving classes for property types. All other
/// lookups should go through `Activation.resolve_definition`,
/// which takes scopes and privacy into account.
///
/// Note that this map is 'gradually' populated over time -
/// each call to `script.load_traits` inserts all private
/// traits declared by that script. When looking up a
/// trait name in this map, you must ensure that its
/// corresponing `Script` will have already been loaded.
private_trait_scripts: PropertyMap<'gc, Script<'gc>>,
} }
impl<'gc> TranslationUnit<'gc> { impl<'gc> TranslationUnit<'gc> {
@ -65,6 +84,7 @@ impl<'gc> TranslationUnit<'gc> {
let methods = vec![None; abc.methods.len()]; let methods = vec![None; abc.methods.len()];
let scripts = vec![None; abc.scripts.len()]; let scripts = vec![None; abc.scripts.len()];
let strings = vec![None; abc.constant_pool.strings.len() + 1]; let strings = vec![None; abc.constant_pool.strings.len() + 1];
let private_trait_scripts = PropertyMap::new();
Self(GcCell::allocate( Self(GcCell::allocate(
mc, mc,
@ -75,15 +95,28 @@ impl<'gc> TranslationUnit<'gc> {
methods, methods,
scripts, scripts,
strings, strings,
private_trait_scripts,
}, },
)) ))
} }
pub fn domain(self) -> Domain<'gc> {
self.0.read().domain
}
/// Retrieve the underlying `AbcFile` for this translation unit. /// Retrieve the underlying `AbcFile` for this translation unit.
pub fn abc(self) -> Rc<AbcFile> { pub fn abc(self) -> Rc<AbcFile> {
self.0.read().abc.clone() self.0.read().abc.clone()
} }
pub fn get_loaded_private_trait_script(self, name: &Multiname<'gc>) -> Option<Script<'gc>> {
self.0
.read()
.private_trait_scripts
.get_for_multiname(name)
.copied()
}
/// Load a method from the ABC file and return its method definition. /// Load a method from the ABC file and return its method definition.
pub fn load_method( pub fn load_method(
self, self,
@ -338,7 +371,13 @@ impl<'gc> Script<'gc> {
for abc_trait in script.traits.iter() { for abc_trait in script.traits.iter() {
let newtrait = Trait::from_abc_trait(unit, abc_trait, activation)?; let newtrait = Trait::from_abc_trait(unit, abc_trait, activation)?;
if !newtrait.name().namespace().is_private() { let name = newtrait.name();
if name.namespace().is_private() {
unit.0
.write(activation.context.gc_context)
.private_trait_scripts
.insert(name, *self);
} else {
write.domain.export_definition( write.domain.export_definition(
newtrait.name(), newtrait.name(),
*self, *self,

View File

@ -70,6 +70,7 @@ pub enum TraitKind<'gc> {
slot_id: u32, slot_id: u32,
type_name: Multiname<'gc>, type_name: Multiname<'gc>,
default_value: Value<'gc>, default_value: Value<'gc>,
unit: Option<TranslationUnit<'gc>>,
}, },
/// A method on an object that can be called. /// A method on an object that can be called.
@ -97,6 +98,7 @@ pub enum TraitKind<'gc> {
slot_id: u32, slot_id: u32,
type_name: Multiname<'gc>, type_name: Multiname<'gc>,
default_value: Value<'gc>, default_value: Value<'gc>,
unit: Option<TranslationUnit<'gc>>,
}, },
} }
@ -158,6 +160,7 @@ impl<'gc> Trait<'gc> {
slot_id: 0, slot_id: 0,
default_value: default_value.unwrap_or_else(|| default_value_for_type(&type_name)), default_value: default_value.unwrap_or_else(|| default_value_for_type(&type_name)),
type_name, type_name,
unit: None,
}, },
} }
} }
@ -174,6 +177,7 @@ impl<'gc> Trait<'gc> {
slot_id: 0, slot_id: 0,
default_value: default_value.unwrap_or_else(|| default_value_for_type(&type_name)), default_value: default_value.unwrap_or_else(|| default_value_for_type(&type_name)),
type_name, type_name,
unit: None,
}, },
} }
} }
@ -206,6 +210,7 @@ impl<'gc> Trait<'gc> {
slot_id: *slot_id, slot_id: *slot_id,
type_name, type_name,
default_value, default_value,
unit: Some(unit),
}, },
} }
} }
@ -267,6 +272,7 @@ impl<'gc> Trait<'gc> {
slot_id: *slot_id, slot_id: *slot_id,
type_name, type_name,
default_value, default_value,
unit: Some(unit),
}, },
} }
} }

View File

@ -1,6 +1,7 @@
//! AVM2 values //! AVM2 values
use crate::avm2::activation::Activation; use crate::avm2::activation::Activation;
use crate::avm2::globals::NS_VECTOR;
use crate::avm2::names::{Multiname, Namespace, QName}; use crate::avm2::names::{Multiname, Namespace, QName};
use crate::avm2::object::{ClassObject, NamespaceObject, Object, PrimitiveObject, TObject}; use crate::avm2::object::{ClassObject, NamespaceObject, Object, PrimitiveObject, TObject};
use crate::avm2::script::TranslationUnit; use crate::avm2::script::TranslationUnit;
@ -687,15 +688,27 @@ impl<'gc> Value<'gc> {
if object.is_of_type(class, activation)? { if object.is_of_type(class, activation)? {
return Ok(object.into()); return Ok(object.into());
} }
if let Some(vector) = object.as_vector_storage() {
let name = class.inner_class_definition().read().name();
if name == QName::new(Namespace::package(NS_VECTOR), "Vector")
|| (name == QName::new(Namespace::internal(NS_VECTOR), "Vector$int")
&& vector.value_type() == activation.avm2().classes().int)
|| (name == QName::new(Namespace::internal(NS_VECTOR), "Vector$uint")
&& vector.value_type() == activation.avm2().classes().uint)
|| (name == QName::new(Namespace::internal(NS_VECTOR), "Vector$number")
&& vector.value_type() == activation.avm2().classes().number)
|| (name == QName::new(Namespace::internal(NS_VECTOR), "Vector$object")
&& vector.value_type() == activation.avm2().classes().object)
{
return Ok(*self);
}
}
} }
let static_class = class.inner_class_definition(); let name = class.inner_class_definition().read().name();
Err(format!(
"Cannot coerce {:?} to an {:?}", Err(format!("Cannot coerce {:?} to an {:?}", self, name).into())
self,
static_class.read().name()
)
.into())
} }
/// Determine if this value is any kind of number. /// Determine if this value is any kind of number.

View File

@ -2,7 +2,7 @@ use crate::avm2::activation::Activation;
use crate::avm2::method::Method; use crate::avm2::method::Method;
use crate::avm2::names::{Multiname, Namespace, QName}; use crate::avm2::names::{Multiname, Namespace, QName};
use crate::avm2::object::{ClassObject, FunctionObject, Object}; use crate::avm2::object::{ClassObject, FunctionObject, Object};
use crate::avm2::property::Property; use crate::avm2::property::{Property, PropertyClass};
use crate::avm2::property_map::PropertyMap; use crate::avm2::property_map::PropertyMap;
use crate::avm2::scope::ScopeChain; use crate::avm2::scope::ScopeChain;
use crate::avm2::traits::{Trait, TraitKind}; use crate::avm2::traits::{Trait, TraitKind};
@ -27,7 +27,7 @@ pub struct VTableData<'gc> {
protected_namespace: Option<Namespace<'gc>>, protected_namespace: Option<Namespace<'gc>>,
resolved_traits: PropertyMap<'gc, Property>, resolved_traits: PropertyMap<'gc, Property<'gc>>,
method_table: Vec<ClassBoundMethod<'gc>>, method_table: Vec<ClassBoundMethod<'gc>>,
@ -64,7 +64,7 @@ impl<'gc> VTable<'gc> {
VTable(GcCell::allocate(mc, self.0.read().clone())) VTable(GcCell::allocate(mc, self.0.read().clone()))
} }
pub fn get_trait(self, name: &Multiname<'gc>) -> Option<Property> { pub fn get_trait(self, name: &Multiname<'gc>) -> Option<Property<'gc>> {
self.0 self.0
.read() .read()
.resolved_traits .resolved_traits
@ -300,12 +300,26 @@ impl<'gc> VTable<'gc> {
}; };
let new_prop = match trait_data.kind() { let new_prop = match trait_data.kind() {
TraitKind::Slot { .. } | TraitKind::Function { .. } => { TraitKind::Slot {
Property::new_slot(new_slot_id) type_name, unit, ..
} } => Property::new_slot(
TraitKind::Const { .. } | TraitKind::Class { .. } => { new_slot_id,
Property::new_const_slot(new_slot_id) PropertyClass::name(activation, type_name.clone(), *unit),
} ),
TraitKind::Function { .. } => Property::new_slot(
new_slot_id,
PropertyClass::Class(activation.avm2().classes().function),
),
TraitKind::Const {
type_name, unit, ..
} => Property::new_const_slot(
new_slot_id,
PropertyClass::name(activation, type_name.clone(), *unit),
),
TraitKind::Class { .. } => Property::new_const_slot(
new_slot_id,
PropertyClass::Class(activation.avm2().classes().class),
),
_ => unreachable!(), _ => unreachable!(),
}; };
@ -359,14 +373,16 @@ impl<'gc> VTable<'gc> {
mc: MutationContext<'gc, '_>, mc: MutationContext<'gc, '_>,
name: QName<'gc>, name: QName<'gc>,
value: Value<'gc>, value: Value<'gc>,
class: ClassObject<'gc>,
) -> u32 { ) -> u32 {
let mut write = self.0.write(mc); let mut write = self.0.write(mc);
write.default_slots.push(Some(value)); write.default_slots.push(Some(value));
let new_slot_id = write.default_slots.len() as u32 - 1; let new_slot_id = write.default_slots.len() as u32 - 1;
write write.resolved_traits.insert(
.resolved_traits name,
.insert(name, Property::new_slot(new_slot_id)); Property::new_slot(new_slot_id, PropertyClass::Class(class)),
);
new_slot_id new_slot_id
} }

View File

@ -188,6 +188,7 @@ swf_tests! {
(as3_class_to_string, "avm2/class_to_string", 1), (as3_class_to_string, "avm2/class_to_string", 1),
(as3_class_value_of, "avm2/class_value_of", 1), (as3_class_value_of, "avm2/class_value_of", 1),
(as3_closures, "avm2/closures", 1), (as3_closures, "avm2/closures", 1),
(as3_coerce_property, "avm2/coerce_property", 1),
(as3_coerce_string, "avm2/coerce_string", 1), (as3_coerce_string, "avm2/coerce_string", 1),
(as3_constructor_call, "avm2/constructor_call", 1), (as3_constructor_call, "avm2/constructor_call", 1),
(as3_control_flow_bool, "avm2/control_flow_bool", 1), (as3_control_flow_bool, "avm2/control_flow_bool", 1),

View File

@ -0,0 +1,54 @@
package {
public class Test {
}
}
namespace ruffle = "https://ruffle.rs/AS3/test_ns";
class First {
var prop1:Second;
}
class Second {
var prop2:First;
}
var any_var:* = undefined;
trace("///any_var");
trace(any_var);
var object_var:Object = undefined;
trace("///object_var");
trace(object_var);
trace("///var integer_var:int = 1.5");
var integer_var:int = 1.5;
trace("///integer_var");
trace(integer_var);
trace("///integer_var = 6.7;");
integer_var = 6.7;
trace(integer_var);
trace
var first:First = new First();
var second:Second = new Second();
trace("///first.prop1");
trace(first.prop1);
trace("///second.prop2");
trace(second.prop2);
trace("///first.prop1 = second;");
first.prop1 = second;
trace("///second.prop2 = first");
second.prop2 = first;
trace("///first.prop1");
trace(first.prop1);
trace("///second.prop2");
trace(second.prop2);
trace("//first.prop1 = new Object();");
first.prop1 = new Object();
trace("ERROR: This should be unreachable due to error being thrown");

View File

@ -0,0 +1,20 @@
///any_var
undefined
///object_var
null
///var integer_var:int = 1.5
///integer_var
1
///integer_var = 6.7;
6
///first.prop1
null
///second.prop2
null
///first.prop1 = second;
///second.prop2 = first
///first.prop1
[object Second]
///second.prop2
[object First]
//first.prop1 = new Object();

Binary file not shown.

Binary file not shown.