diff --git a/core/src/avm2/class.rs b/core/src/avm2/class.rs index 43f544648..920ca45c8 100644 --- a/core/src/avm2/class.rs +++ b/core/src/avm2/class.rs @@ -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, diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index 7f80c5a0d..54c653198 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -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>, 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(()) } diff --git a/core/src/avm2/globals/flash/events/eventdispatcher.rs b/core/src/avm2/globals/flash/events/eventdispatcher.rs index 1bf5beea7..ff58bd744 100644 --- a/core/src/avm2/globals/flash/events/eventdispatcher.rs +++ b/core/src/avm2/globals/flash/events/eventdispatcher.rs @@ -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, )); diff --git a/core/src/avm2/globals/flash/net/url_loader.rs b/core/src/avm2/globals/flash/net/url_loader.rs index 345352bdb..8a904afe6 100644 --- a/core/src/avm2/globals/flash/net/url_loader.rs +++ b/core/src/avm2/globals/flash/net/url_loader.rs @@ -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); diff --git a/core/src/avm2/globals/vector.rs b/core/src/avm2/globals/vector.rs index 1e860addc..242f7a857 100644 --- a/core/src/avm2/globals/vector.rs +++ b/core/src/avm2/globals/vector.rs @@ -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)?; } diff --git a/core/src/avm2/object.rs b/core/src/avm2/object.rs index b816f0575..194c15bab 100644 --- a/core/src/avm2/object.rs +++ b/core/src/avm2/object.rs @@ -141,9 +141,8 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + 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().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> + 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> + 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> + 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 }) => { + 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> + 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); } diff --git a/core/src/avm2/object/class_object.rs b/core/src/avm2/object/class_object.rs index defa46be6..ce9b835f8 100644 --- a/core/src/avm2/object/class_object.rs +++ b/core/src/avm2/object/class_object.rs @@ -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> { + 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 } diff --git a/core/src/avm2/property.rs b/core/src/avm2/property.rs index 2a6ba9c37..a7f5c553b 100644 --- a/core/src/avm2/property.rs +++ b/core/src/avm2/property.rs @@ -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, set: Option }, - Method { disp_id: u32 }, - Slot { slot_id: u32 }, - ConstSlot { slot_id: u32 }, +#[collect(no_drop)] +pub enum Property<'gc> { + Virtual { + get: Option, + set: Option, + }, + 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>)>), +} + +impl<'gc> PropertyClass<'gc> { + pub fn name( + activation: &mut Activation<'_, 'gc, '_>, + name: Multiname<'gc>, + unit: Option>, + ) -> Self { + PropertyClass::Name(Gc::allocate(activation.context.gc_context, (name, unit))) + } + pub fn coerce( + &mut self, + activation: &mut Activation<'_, 'gc, '_>, + value: Value<'gc>, + ) -> Result, 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>, + activation: &mut Activation<'_, 'gc, '_>, +) -> Result>, 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 } } } diff --git a/core/src/avm2/script.rs b/core/src/avm2/script.rs index 1cb621f4e..e2bd35560 100644 --- a/core/src/avm2/script.rs +++ b/core/src/avm2/script.rs @@ -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>>, + + /// 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 { self.0.read().abc.clone() } + pub fn get_loaded_private_trait_script(self, name: &Multiname<'gc>) -> Option> { + 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, diff --git a/core/src/avm2/traits.rs b/core/src/avm2/traits.rs index b4cbfc966..9086478a0 100644 --- a/core/src/avm2/traits.rs +++ b/core/src/avm2/traits.rs @@ -70,6 +70,7 @@ pub enum TraitKind<'gc> { slot_id: u32, type_name: Multiname<'gc>, default_value: Value<'gc>, + unit: Option>, }, /// 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>, }, } @@ -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), }, } } diff --git a/core/src/avm2/value.rs b/core/src/avm2/value.rs index c08aaeb52..8533a8899 100644 --- a/core/src/avm2/value.rs +++ b/core/src/avm2/value.rs @@ -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. diff --git a/core/src/avm2/vtable.rs b/core/src/avm2/vtable.rs index d0b64443f..45f3fea2d 100644 --- a/core/src/avm2/vtable.rs +++ b/core/src/avm2/vtable.rs @@ -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>, - resolved_traits: PropertyMap<'gc, Property>, + resolved_traits: PropertyMap<'gc, Property<'gc>>, method_table: Vec>, @@ -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 { + pub fn get_trait(self, name: &Multiname<'gc>) -> Option> { 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 } diff --git a/tests/tests/regression_tests.rs b/tests/tests/regression_tests.rs index 6cd9cfa31..ff5113e36 100644 --- a/tests/tests/regression_tests.rs +++ b/tests/tests/regression_tests.rs @@ -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), diff --git a/tests/tests/swfs/avm2/coerce_property/Test.as b/tests/tests/swfs/avm2/coerce_property/Test.as new file mode 100644 index 000000000..8dacf70e6 --- /dev/null +++ b/tests/tests/swfs/avm2/coerce_property/Test.as @@ -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"); \ No newline at end of file diff --git a/tests/tests/swfs/avm2/coerce_property/output.txt b/tests/tests/swfs/avm2/coerce_property/output.txt new file mode 100644 index 000000000..70ecb3ab2 --- /dev/null +++ b/tests/tests/swfs/avm2/coerce_property/output.txt @@ -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(); diff --git a/tests/tests/swfs/avm2/coerce_property/test.fla b/tests/tests/swfs/avm2/coerce_property/test.fla new file mode 100644 index 000000000..b93b0f420 Binary files /dev/null and b/tests/tests/swfs/avm2/coerce_property/test.fla differ diff --git a/tests/tests/swfs/avm2/coerce_property/test.swf b/tests/tests/swfs/avm2/coerce_property/test.swf new file mode 100644 index 000000000..d747a21e4 Binary files /dev/null and b/tests/tests/swfs/avm2/coerce_property/test.swf differ