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:
parent
c7bf11ece5
commit
b56eace008
|
@ -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)]
|
||||
pub fn define_private_slot_instance_traits(
|
||||
&mut self,
|
||||
|
|
|
@ -40,7 +40,7 @@ mod xml;
|
|||
mod xml_list;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -177,7 +177,7 @@ fn function<'gc>(
|
|||
let method = Method::from_builtin(nf, name, mc);
|
||||
let as3fn = FunctionObject::from_method(activation, method, scope, None, None).into();
|
||||
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(())
|
||||
}
|
||||
|
@ -190,12 +190,14 @@ fn dynamic_class<'gc>(
|
|||
mc: MutationContext<'gc, '_>,
|
||||
class_object: ClassObject<'gc>,
|
||||
script: Script<'gc>,
|
||||
// The `ClassObject` of the `Class` class
|
||||
class_class: ClassObject<'gc>,
|
||||
) -> Result<(), Error> {
|
||||
let (_, mut global, mut domain) = script.init();
|
||||
let class = class_object.inner_class_definition();
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -243,6 +245,7 @@ fn class<'gc>(
|
|||
activation.context.gc_context,
|
||||
class_name,
|
||||
class_object.into(),
|
||||
activation.avm2().classes().class,
|
||||
);
|
||||
domain.export_definition(class_name, script, activation.context.gc_context)?;
|
||||
|
||||
|
@ -256,11 +259,12 @@ fn constant<'gc>(
|
|||
name: impl Into<AvmString<'gc>>,
|
||||
value: Value<'gc>,
|
||||
script: Script<'gc>,
|
||||
class: ClassObject<'gc>,
|
||||
) -> Result<(), Error> {
|
||||
let (_, mut global, mut domain) = script.init();
|
||||
let name = QName::new(Namespace::package(package), name);
|
||||
domain.export_definition(name, script, mc)?;
|
||||
global.install_const_late(mc, name, value);
|
||||
global.install_const_late(mc, name, value, class);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -279,6 +283,7 @@ fn namespace<'gc>(
|
|||
name,
|
||||
namespace.into(),
|
||||
script,
|
||||
activation.avm2().classes().namespace,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -383,15 +388,13 @@ pub fn load_player_globals<'gc>(
|
|||
|
||||
// From this point, `globals` is safe to be modified
|
||||
|
||||
dynamic_class(mc, object_class, script)?;
|
||||
dynamic_class(mc, fn_class, script)?;
|
||||
dynamic_class(mc, class_class, script)?;
|
||||
dynamic_class(mc, object_class, script, class_class)?;
|
||||
dynamic_class(mc, fn_class, script, class_class)?;
|
||||
dynamic_class(mc, class_class, script, class_class)?;
|
||||
|
||||
// After this point, it is safe to initialize any other classes.
|
||||
// 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!(boolean, activation, boolean::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, "", "parseFloat", toplevel::parse_float, script)?;
|
||||
function(activation, "", "escape", toplevel::escape, script)?;
|
||||
constant(mc, "", "undefined", Value::Undefined, script)?;
|
||||
constant(mc, "", "null", Value::Null, script)?;
|
||||
constant(mc, "", "NaN", f64::NAN.into(), script)?;
|
||||
constant(mc, "", "Infinity", f64::INFINITY.into(), script)?;
|
||||
constant(mc, "", "undefined", Value::Undefined, script, object_class)?;
|
||||
constant(mc, "", "null", Value::Null, script, object_class)?;
|
||||
constant(mc, "", "NaN", f64::NAN.into(), script, object_class)?;
|
||||
constant(
|
||||
mc,
|
||||
"",
|
||||
"Infinity",
|
||||
f64::INFINITY.into(),
|
||||
script,
|
||||
object_class,
|
||||
)?;
|
||||
|
||||
class(activation, math::create_class(mc), script)?;
|
||||
class(activation, json::create_class(mc), script)?;
|
||||
|
@ -784,6 +794,11 @@ pub fn load_player_globals<'gc>(
|
|||
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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ use crate::avm2::class::{Class, ClassAttributes};
|
|||
use crate::avm2::events::{
|
||||
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::names::{Namespace, QName};
|
||||
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(
|
||||
QName::new(Namespace::private(NS_EVENT_DISPATCHER), "target"),
|
||||
QName::new(Namespace::private(NS_RUFFLE_INTERNAL), "BareObject").into(),
|
||||
QName::new(Namespace::public(), "Object").into(),
|
||||
None,
|
||||
));
|
||||
write.define_instance_trait(Trait::from_slot(
|
||||
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,
|
||||
));
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use crate::avm2::activation::Activation;
|
||||
use crate::avm2::class::Class;
|
||||
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::value::Value;
|
||||
use crate::avm2::{Error, Object};
|
||||
|
@ -74,7 +74,12 @@ fn bytes_total<'gc>(
|
|||
if let Some(array) = data.as_bytearray() {
|
||||
return Ok(array.len().into());
|
||||
} 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 {
|
||||
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);
|
||||
|
||||
const PUBLIC_INSTANCE_SLOTS: &[(&str, &str, &str)] =
|
||||
&[("data", "", "Object"), ("dataFormat", "", "String")];
|
||||
const PUBLIC_INSTANCE_SLOTS: &[(&str, &str, &str)] = &[("dataFormat", "", "String")];
|
||||
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)];
|
||||
write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS);
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ pub fn class_init<'gc>(
|
|||
.get_defining_script(&QName::new(Namespace::public(), "Object").into())?
|
||||
.unwrap();
|
||||
|
||||
let class_class = activation.avm2().classes().class;
|
||||
let int_class = activation.avm2().classes().int;
|
||||
let int_vector_class = this.apply(activation, &[int_class.into()])?;
|
||||
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,
|
||||
int_vector_name,
|
||||
int_vector_class.into(),
|
||||
class_class,
|
||||
);
|
||||
domain.export_definition(int_vector_name, script, activation.context.gc_context)?;
|
||||
|
||||
|
@ -130,6 +132,7 @@ pub fn class_init<'gc>(
|
|||
activation.context.gc_context,
|
||||
uint_vector_name,
|
||||
uint_vector_class.into(),
|
||||
class_class,
|
||||
);
|
||||
domain.export_definition(uint_vector_name, script, activation.context.gc_context)?;
|
||||
|
||||
|
@ -145,6 +148,7 @@ pub fn class_init<'gc>(
|
|||
activation.context.gc_context,
|
||||
number_vector_name,
|
||||
number_vector_class.into(),
|
||||
class_class,
|
||||
);
|
||||
domain.export_definition(number_vector_name, script, activation.context.gc_context)?;
|
||||
|
||||
|
@ -159,6 +163,7 @@ pub fn class_init<'gc>(
|
|||
activation.context.gc_context,
|
||||
object_vector_name,
|
||||
object_vector_class.into(),
|
||||
class_class,
|
||||
);
|
||||
domain.export_definition(object_vector_name, script, activation.context.gc_context)?;
|
||||
}
|
||||
|
|
|
@ -141,9 +141,8 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
|
|||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) {
|
||||
Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => {
|
||||
self.base().get_slot(slot_id)
|
||||
}
|
||||
Some(Property::Slot { slot_id, class: _ })
|
||||
| Some(Property::ConstSlot { slot_id, class: _ }) => self.base().get_slot(slot_id),
|
||||
Some(Property::Method { disp_id }) => {
|
||||
if let Some(bound_method) = self.get_bound_method(disp_id) {
|
||||
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, '_>,
|
||||
) -> Result<(), Error> {
|
||||
match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) {
|
||||
Some(Property::Slot { slot_id }) => self
|
||||
.base_mut(activation.context.gc_context)
|
||||
.set_slot(slot_id, value, activation.context.gc_context),
|
||||
Some(Property::Slot { slot_id, mut class }) => {
|
||||
let value = class.coerce(activation, value)?;
|
||||
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::Method { .. }) => Err("Cannot assign to a method".into()),
|
||||
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, '_>,
|
||||
) -> Result<(), Error> {
|
||||
match self.vtable().and_then(|vtable| vtable.get_trait(multiname)) {
|
||||
Some(Property::Slot { slot_id }) | Some(Property::ConstSlot { slot_id }) => self
|
||||
.base_mut(activation.context.gc_context)
|
||||
.set_slot(slot_id, value, activation.context.gc_context),
|
||||
Some(Property::Slot { slot_id, mut class })
|
||||
| Some(Property::ConstSlot { slot_id, mut class }) => {
|
||||
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::Virtual { set: Some(set), .. }) => {
|
||||
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, '_>,
|
||||
) -> Result<Value<'gc>, Error> {
|
||||
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(
|
||||
activation,
|
||||
Some(multiname),
|
||||
|
@ -595,11 +606,12 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
|
|||
mc: MutationContext<'gc, '_>,
|
||||
name: QName<'gc>,
|
||||
value: Value<'gc>,
|
||||
class: ClassObject<'gc>,
|
||||
) {
|
||||
let new_slot_id = self
|
||||
.vtable()
|
||||
.unwrap()
|
||||
.install_const_trait_late(mc, name, value);
|
||||
.install_const_trait_late(mc, name, value, class);
|
||||
self.base_mut(mc)
|
||||
.install_const_slot_late(new_slot_id, value);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use crate::avm2::scope::{Scope, ScopeChain};
|
|||
use crate::avm2::value::Value;
|
||||
use crate::avm2::vtable::{ClassBoundMethod, VTable};
|
||||
use crate::avm2::Error;
|
||||
use crate::avm2::TranslationUnit;
|
||||
use crate::string::AvmString;
|
||||
use fnv::FnvHashMap;
|
||||
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> {
|
||||
self.0.read().instance_vtable
|
||||
}
|
||||
|
|
|
@ -1,17 +1,160 @@
|
|||
//! 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)]
|
||||
#[collect(require_static)]
|
||||
pub enum Property {
|
||||
Virtual { get: Option<u32>, set: Option<u32> },
|
||||
Method { disp_id: u32 },
|
||||
Slot { slot_id: u32 },
|
||||
ConstSlot { slot_id: u32 },
|
||||
#[collect(no_drop)]
|
||||
pub enum Property<'gc> {
|
||||
Virtual {
|
||||
get: Option<u32>,
|
||||
set: Option<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 {
|
||||
Property::Method { disp_id }
|
||||
}
|
||||
|
@ -30,11 +173,11 @@ impl Property {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_slot(slot_id: u32) -> Self {
|
||||
Property::Slot { slot_id }
|
||||
pub fn new_slot(slot_id: u32, class: PropertyClass<'gc>) -> Self {
|
||||
Property::Slot { slot_id, class }
|
||||
}
|
||||
|
||||
pub fn new_const_slot(slot_id: u32) -> Self {
|
||||
Property::ConstSlot { slot_id }
|
||||
pub fn new_const_slot(slot_id: u32, class: PropertyClass<'gc>) -> Self {
|
||||
Property::ConstSlot { slot_id, class }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ use crate::avm2::activation::Activation;
|
|||
use crate::avm2::class::Class;
|
||||
use crate::avm2::domain::Domain;
|
||||
use crate::avm2::method::{BytecodeMethod, Method};
|
||||
use crate::avm2::names::Multiname;
|
||||
use crate::avm2::object::{Object, TObject};
|
||||
use crate::avm2::property_map::PropertyMap;
|
||||
use crate::avm2::scope::ScopeChain;
|
||||
use crate::avm2::traits::Trait;
|
||||
use crate::avm2::value::Value;
|
||||
|
@ -55,6 +57,23 @@ pub struct TranslationUnitData<'gc> {
|
|||
/// All strings loaded from the ABC's strings list.
|
||||
/// They're lazy loaded and offset by 1, with the 0th element being always None.
|
||||
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> {
|
||||
|
@ -65,6 +84,7 @@ impl<'gc> TranslationUnit<'gc> {
|
|||
let methods = vec![None; abc.methods.len()];
|
||||
let scripts = vec![None; abc.scripts.len()];
|
||||
let strings = vec![None; abc.constant_pool.strings.len() + 1];
|
||||
let private_trait_scripts = PropertyMap::new();
|
||||
|
||||
Self(GcCell::allocate(
|
||||
mc,
|
||||
|
@ -75,15 +95,28 @@ impl<'gc> TranslationUnit<'gc> {
|
|||
methods,
|
||||
scripts,
|
||||
strings,
|
||||
private_trait_scripts,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn domain(self) -> Domain<'gc> {
|
||||
self.0.read().domain
|
||||
}
|
||||
|
||||
/// Retrieve the underlying `AbcFile` for this translation unit.
|
||||
pub fn abc(self) -> Rc<AbcFile> {
|
||||
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.
|
||||
pub fn load_method(
|
||||
self,
|
||||
|
@ -338,7 +371,13 @@ impl<'gc> Script<'gc> {
|
|||
|
||||
for abc_trait in script.traits.iter() {
|
||||
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(
|
||||
newtrait.name(),
|
||||
*self,
|
||||
|
|
|
@ -70,6 +70,7 @@ pub enum TraitKind<'gc> {
|
|||
slot_id: u32,
|
||||
type_name: Multiname<'gc>,
|
||||
default_value: Value<'gc>,
|
||||
unit: Option<TranslationUnit<'gc>>,
|
||||
},
|
||||
|
||||
/// A method on an object that can be called.
|
||||
|
@ -97,6 +98,7 @@ pub enum TraitKind<'gc> {
|
|||
slot_id: u32,
|
||||
type_name: Multiname<'gc>,
|
||||
default_value: Value<'gc>,
|
||||
unit: Option<TranslationUnit<'gc>>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -158,6 +160,7 @@ impl<'gc> Trait<'gc> {
|
|||
slot_id: 0,
|
||||
default_value: default_value.unwrap_or_else(|| default_value_for_type(&type_name)),
|
||||
type_name,
|
||||
unit: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +177,7 @@ impl<'gc> Trait<'gc> {
|
|||
slot_id: 0,
|
||||
default_value: default_value.unwrap_or_else(|| default_value_for_type(&type_name)),
|
||||
type_name,
|
||||
unit: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -206,6 +210,7 @@ impl<'gc> Trait<'gc> {
|
|||
slot_id: *slot_id,
|
||||
type_name,
|
||||
default_value,
|
||||
unit: Some(unit),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -267,6 +272,7 @@ impl<'gc> Trait<'gc> {
|
|||
slot_id: *slot_id,
|
||||
type_name,
|
||||
default_value,
|
||||
unit: Some(unit),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! AVM2 values
|
||||
|
||||
use crate::avm2::activation::Activation;
|
||||
use crate::avm2::globals::NS_VECTOR;
|
||||
use crate::avm2::names::{Multiname, Namespace, QName};
|
||||
use crate::avm2::object::{ClassObject, NamespaceObject, Object, PrimitiveObject, TObject};
|
||||
use crate::avm2::script::TranslationUnit;
|
||||
|
@ -687,15 +688,27 @@ impl<'gc> Value<'gc> {
|
|||
if object.is_of_type(class, activation)? {
|
||||
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();
|
||||
Err(format!(
|
||||
"Cannot coerce {:?} to an {:?}",
|
||||
self,
|
||||
static_class.read().name()
|
||||
)
|
||||
.into())
|
||||
let name = class.inner_class_definition().read().name();
|
||||
|
||||
Err(format!("Cannot coerce {:?} to an {:?}", self, name).into())
|
||||
}
|
||||
|
||||
/// Determine if this value is any kind of number.
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::avm2::activation::Activation;
|
|||
use crate::avm2::method::Method;
|
||||
use crate::avm2::names::{Multiname, Namespace, QName};
|
||||
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::scope::ScopeChain;
|
||||
use crate::avm2::traits::{Trait, TraitKind};
|
||||
|
@ -27,7 +27,7 @@ pub struct VTableData<'gc> {
|
|||
|
||||
protected_namespace: Option<Namespace<'gc>>,
|
||||
|
||||
resolved_traits: PropertyMap<'gc, Property>,
|
||||
resolved_traits: PropertyMap<'gc, Property<'gc>>,
|
||||
|
||||
method_table: Vec<ClassBoundMethod<'gc>>,
|
||||
|
||||
|
@ -64,7 +64,7 @@ impl<'gc> VTable<'gc> {
|
|||
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
|
||||
.read()
|
||||
.resolved_traits
|
||||
|
@ -300,12 +300,26 @@ impl<'gc> VTable<'gc> {
|
|||
};
|
||||
|
||||
let new_prop = match trait_data.kind() {
|
||||
TraitKind::Slot { .. } | TraitKind::Function { .. } => {
|
||||
Property::new_slot(new_slot_id)
|
||||
}
|
||||
TraitKind::Const { .. } | TraitKind::Class { .. } => {
|
||||
Property::new_const_slot(new_slot_id)
|
||||
}
|
||||
TraitKind::Slot {
|
||||
type_name, unit, ..
|
||||
} => Property::new_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!(),
|
||||
};
|
||||
|
||||
|
@ -359,14 +373,16 @@ impl<'gc> VTable<'gc> {
|
|||
mc: MutationContext<'gc, '_>,
|
||||
name: QName<'gc>,
|
||||
value: Value<'gc>,
|
||||
class: ClassObject<'gc>,
|
||||
) -> u32 {
|
||||
let mut write = self.0.write(mc);
|
||||
|
||||
write.default_slots.push(Some(value));
|
||||
let new_slot_id = write.default_slots.len() as u32 - 1;
|
||||
write
|
||||
.resolved_traits
|
||||
.insert(name, Property::new_slot(new_slot_id));
|
||||
write.resolved_traits.insert(
|
||||
name,
|
||||
Property::new_slot(new_slot_id, PropertyClass::Class(class)),
|
||||
);
|
||||
|
||||
new_slot_id
|
||||
}
|
||||
|
|
|
@ -188,6 +188,7 @@ swf_tests! {
|
|||
(as3_class_to_string, "avm2/class_to_string", 1),
|
||||
(as3_class_value_of, "avm2/class_value_of", 1),
|
||||
(as3_closures, "avm2/closures", 1),
|
||||
(as3_coerce_property, "avm2/coerce_property", 1),
|
||||
(as3_coerce_string, "avm2/coerce_string", 1),
|
||||
(as3_constructor_call, "avm2/constructor_call", 1),
|
||||
(as3_control_flow_bool, "avm2/control_flow_bool", 1),
|
||||
|
|
|
@ -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");
|
|
@ -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.
Loading…
Reference in New Issue