diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 4dd2a1452..8efb24c57 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1,6 +1,5 @@ use crate::avm1::function::Avm1Function; use crate::avm1::globals::create_globals; -use crate::avm1::object::Object; use crate::backend::navigator::NavigationMethod; use crate::context::UpdateContext; use crate::prelude::*; @@ -17,11 +16,12 @@ use crate::tag_utils::SwfSlice; mod activation; mod fscommand; mod function; -mod globals; -pub mod movie_clip; +pub mod globals; pub mod object; +mod property; mod return_value; mod scope; +pub mod script_object; mod value; #[cfg(test)] @@ -31,7 +31,10 @@ mod test_utils; mod tests; use activation::Activation; +pub use globals::SystemPrototypes; +pub use object::{Object, ObjectCell}; use scope::Scope; +pub use script_object::ScriptObject; pub use value::Value; pub struct Avm1<'gc> { @@ -42,7 +45,10 @@ pub struct Avm1<'gc> { constant_pool: Vec, /// The global object. - globals: GcCell<'gc, Object<'gc>>, + globals: ObjectCell<'gc>, + + /// System builtins that we use internally to construct new objects. + prototypes: globals::SystemPrototypes<'gc>, /// All activation records for the current execution context. stack_frames: Vec>>, @@ -59,8 +65,13 @@ unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> { #[inline] fn trace(&self, cc: gc_arena::CollectionContext) { self.globals.trace(cc); + self.prototypes.trace(cc); self.stack_frames.trace(cc); self.stack.trace(cc); + + for register in &self.registers { + register.trace(cc); + } } } @@ -68,10 +79,13 @@ type Error = Box; impl<'gc> Avm1<'gc> { pub fn new(gc_context: MutationContext<'gc, '_>, player_version: u8) -> Self { + let (prototypes, globals) = create_globals(gc_context); + Self { player_version, constant_pool: vec![], - globals: GcCell::allocate(gc_context, create_globals(gc_context)), + globals: GcCell::allocate(gc_context, globals), + prototypes, stack_frames: vec![], stack: vec![], registers: [ @@ -390,6 +404,7 @@ impl<'gc> Avm1<'gc> { Action::Increment => self.action_increment(context), Action::InitArray => self.action_init_array(context), Action::InitObject => self.action_init_object(context), + Action::InstanceOf => self.action_instance_of(context), Action::Jump { offset } => self.action_jump(context, offset, reader), Action::Less => self.action_less(context), Action::Less2 => self.action_less_2(context), @@ -799,7 +814,19 @@ impl<'gc> Avm1<'gc> { context.gc_context, ); let func = Avm1Function::from_df1(swf_version, func_data, name, params, scope); - let func_obj = GcCell::allocate(context.gc_context, Object::action_function(func)); + let prototype = GcCell::allocate( + context.gc_context, + Box::new(ScriptObject::object( + context.gc_context, + Some(self.prototypes.object), + )) as Box>, + ); + let func_obj = ScriptObject::function( + context.gc_context, + func, + Some(self.prototypes.function), + Some(prototype), + ); if name == "" { self.push(func_obj); } else { @@ -830,7 +857,19 @@ impl<'gc> Avm1<'gc> { context.gc_context, ); let func = Avm1Function::from_df2(swf_version, func_data, action_func, scope); - let func_obj = GcCell::allocate(context.gc_context, Object::action_function(func)); + let prototype = GcCell::allocate( + context.gc_context, + Box::new(ScriptObject::object( + context.gc_context, + Some(self.prototypes.object), + )) as Box>, + ); + let func_obj = ScriptObject::function( + context.gc_context, + func, + Some(self.prototypes.function), + Some(prototype), + ); if action_func.name == "" { self.push(func_obj); } else { @@ -1062,10 +1101,15 @@ impl<'gc> Avm1<'gc> { } /// Obtain a reference to `_global`. - pub fn global_object_cell(&self) -> GcCell<'gc, Object<'gc>> { + pub fn global_object_cell(&self) -> ObjectCell<'gc> { self.globals } + /// Obtain system built-in prototypes for this instance. + pub fn prototypes(&self) -> &globals::SystemPrototypes<'gc> { + &self.prototypes + } + fn action_get_variable( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -1275,15 +1319,54 @@ impl<'gc> Avm1<'gc> { Err("Unimplemented action: InitArray".into()) } - fn action_init_object(&mut self, _context: &mut UpdateContext) -> Result<(), Error> { + fn action_init_object( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { let num_props = self.pop()?.as_i64()?; + let mut object = ScriptObject::object(context.gc_context, Some(self.prototypes.object)); for _ in 0..num_props { - let _value = self.pop()?; - let _name = self.pop()?; + let value = self.pop()?; + let name = self.pop()?.into_string(); + let this = self.current_stack_frame().unwrap().read().this_cell(); + + object.set(&name, value, self, context, this)?; } - // TODO(Herschel) - Err("Unimplemented action: InitObject".into()) + self.push(Value::Object(GcCell::allocate( + context.gc_context, + Box::new(object), + ))); + + Ok(()) + } + + fn action_instance_of( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + let constr = self.pop()?.as_object()?; + let obj = self.pop()?.as_object()?; + + //TODO: Interface detection on SWF7 + let prototype = constr + .read() + .get("prototype", self, context, constr)? + .resolve(self, context)? + .as_object()?; + let mut proto = obj.read().proto(); + + while let Some(this_proto) = proto { + if GcCell::ptr_eq(this_proto, prototype) { + self.push(true); + return Ok(()); + } + + proto = this_proto.read().proto(); + } + + self.push(false); + Ok(()) } fn action_jump( @@ -1400,24 +1483,72 @@ impl<'gc> Avm1<'gc> { Ok(()) } - fn action_new_method(&mut self, _context: &mut UpdateContext) -> Result<(), Error> { - let _name = self.pop()?.as_string()?; - let _object = self.pop()?.as_object()?; - let _num_args = self.pop()?.as_i64()?; - self.push(Value::Undefined); - // TODO(Herschel) - Err("Unimplemented action: NewMethod".into()) + fn action_new_method(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> { + let method_name = self.pop()?; + let object = self.pop()?.as_object()?; + let num_args = self.pop()?.as_i64()?; + let mut args = Vec::new(); + for _ in 0..num_args { + args.push(self.pop()?); + } + + let constructor = object + .read() + .get(&method_name.as_string()?, self, context, object.to_owned())? + .resolve(self, context)? + .as_object()?; + let prototype = constructor + .read() + .get("prototype", self, context, constructor)? + .resolve(self, context)? + .as_object()?; + + let this = prototype.read().new(self, context, prototype, &args)?; + + //TODO: What happens if you `ActionNewMethod` without a method name? + constructor + .read() + .call(self, context, this, &args)? + .resolve(self, context)?; + + self.push(this); + + Ok(()) } - fn action_new_object(&mut self, _context: &mut UpdateContext) -> Result<(), Error> { - let _object = self.pop()?.as_string()?; + fn action_new_object(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> { + let fn_name = self.pop()?; let num_args = self.pop()?.as_i64()?; + let mut args = Vec::new(); for _ in 0..num_args { - let _arg = self.pop()?; + args.push(self.pop()?); } - self.push(Value::Undefined); - // TODO(Herschel) - Err("Unimplemented action: NewObject".into()) + + let constructor = self + .stack_frames + .last() + .unwrap() + .clone() + .read() + .resolve(fn_name.as_string()?, self, context)? + .resolve(self, context)? + .as_object()?; + let prototype = constructor + .read() + .get("prototype", self, context, constructor)? + .resolve(self, context)? + .as_object()?; + + let this = prototype.read().new(self, context, prototype, &args)?; + + constructor + .read() + .call(self, context, this, &args)? + .resolve(self, context)?; + + self.push(this); + + Ok(()) } fn action_or(&mut self, _context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> { @@ -1528,7 +1659,7 @@ impl<'gc> Avm1<'gc> { fn action_set_member(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> { let value = self.pop()?; let name_val = self.pop()?; - let name = name_val.as_string()?; + let name = name_val.into_string(); let object = self.pop()?.as_object()?; let this = self.current_stack_frame().unwrap().read().this_cell(); diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index c69399a42..e9e917698 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -1,9 +1,8 @@ //! Activation records -use crate::avm1::object::Object; use crate::avm1::return_value::ReturnValue; use crate::avm1::scope::Scope; -use crate::avm1::{Avm1, Error, Value}; +use crate::avm1::{Avm1, Error, ObjectCell, Value}; use crate::context::UpdateContext; use crate::tag_utils::SwfSlice; use gc_arena::{GcCell, MutationContext}; @@ -68,10 +67,10 @@ pub struct Activation<'gc> { scope: GcCell<'gc, Scope<'gc>>, /// The immutable value of `this`. - this: GcCell<'gc, Object<'gc>>, + this: ObjectCell<'gc>, /// The arguments this function was called by. - arguments: Option>>, + arguments: Option>, /// The return value of the activation. return_value: Option>, @@ -115,8 +114,8 @@ impl<'gc> Activation<'gc> { swf_version: u8, code: SwfSlice, scope: GcCell<'gc, Scope<'gc>>, - this: GcCell<'gc, Object<'gc>>, - arguments: Option>>, + this: ObjectCell<'gc>, + arguments: Option>, ) -> Activation<'gc> { Activation { swf_version, @@ -136,8 +135,8 @@ impl<'gc> Activation<'gc> { swf_version: u8, code: SwfSlice, scope: GcCell<'gc, Scope<'gc>>, - this: GcCell<'gc, Object<'gc>>, - arguments: Option>>, + this: ObjectCell<'gc>, + arguments: Option>, ) -> Activation<'gc> { Activation { swf_version, @@ -161,7 +160,7 @@ impl<'gc> Activation<'gc> { /// it. pub fn from_nothing( swf_version: u8, - globals: GcCell<'gc, Object<'gc>>, + globals: ObjectCell<'gc>, mc: MutationContext<'gc, '_>, ) -> Activation<'gc> { let global_scope = GcCell::allocate(mc, Scope::from_global_object(globals)); @@ -297,7 +296,7 @@ impl<'gc> Activation<'gc> { } /// Returns value of `this` as a reference. - pub fn this_cell(&self) -> GcCell<'gc, Object<'gc>> { + pub fn this_cell(&self) -> ObjectCell<'gc> { self.this } diff --git a/core/src/avm1/function.rs b/core/src/avm1/function.rs index 12b3412d7..5fcd3771f 100644 --- a/core/src/avm1/function.rs +++ b/core/src/avm1/function.rs @@ -1,13 +1,13 @@ //! Code relating to executable functions + calling conventions. use crate::avm1::activation::Activation; -use crate::avm1::object::{Attribute::*, Object}; +use crate::avm1::property::Attribute::*; use crate::avm1::return_value::ReturnValue; use crate::avm1::scope::Scope; use crate::avm1::value::Value; -use crate::avm1::{Avm1, Error, UpdateContext}; +use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext}; use crate::tag_utils::SwfSlice; -use gc_arena::GcCell; +use gc_arena::{Collect, CollectionContext, GcCell}; use swf::avm1::types::FunctionParam; /// Represents a function defined in Ruffle's code. @@ -27,13 +27,14 @@ use swf::avm1::types::FunctionParam; pub type NativeFunction<'gc> = fn( &mut Avm1<'gc>, &mut UpdateContext<'_, 'gc, '_>, - GcCell<'gc, Object<'gc>>, + ObjectCell<'gc>, &[Value<'gc>], ) -> Result, Error>; /// Represents a function defined in the AVM1 runtime, either through /// `DefineFunction` or `DefineFunction2`. -#[derive(Clone)] +#[derive(Clone, Collect)] +#[collect(no_drop)] pub struct Avm1Function<'gc> { /// The file format version of the SWF that generated this function. swf_version: u8, @@ -171,6 +172,15 @@ pub enum Executable<'gc> { Action(Avm1Function<'gc>), } +unsafe impl<'gc> Collect for Executable<'gc> { + fn trace(&self, cc: CollectionContext) { + match self { + Self::Native(_) => {} + Self::Action(af) => af.trace(cc), + } + } +} + impl<'gc> Executable<'gc> { /// Execute the given code. /// @@ -182,7 +192,7 @@ impl<'gc> Executable<'gc> { &self, avm: &mut Avm1<'gc>, ac: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, + this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { match self { @@ -192,29 +202,29 @@ impl<'gc> Executable<'gc> { ac.gc_context, Scope::new_local_scope(af.scope(), ac.gc_context), ); - let mut arguments = Object::object(ac.gc_context); + let mut arguments = + ScriptObject::object(ac.gc_context, Some(avm.prototypes().object)); if !af.suppress_arguments { for i in 0..args.len() { - arguments.force_set( + arguments.define_value( &format!("{}", i), args.get(i).unwrap().clone(), - DontDelete, + DontDelete.into(), ) } - arguments.force_set( - "length", - Value::Number(args.len() as f64), - DontDelete | DontEnum, - ); + arguments.define_value("length", args.len().into(), DontDelete | DontEnum); } - let argcell = GcCell::allocate(ac.gc_context, arguments); + let argcell = GcCell::allocate( + ac.gc_context, + Box::new(arguments) as Box + 'gc>, + ); let effective_ver = if avm.current_swf_version() > 5 { af.swf_version() } else { this.read() - .display_node() + .as_display_node() .map(|dn| dn.read().swf_version()) .unwrap_or(ac.player_version) }; @@ -301,3 +311,15 @@ impl<'gc> Executable<'gc> { } } } + +impl<'gc> From> for Executable<'gc> { + fn from(nf: NativeFunction<'gc>) -> Self { + Executable::Native(nf) + } +} + +impl<'gc> From> for Executable<'gc> { + fn from(af: Avm1Function<'gc>) -> Self { + Executable::Action(af) + } +} diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index dd46708b6..ba05b472d 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -1,18 +1,22 @@ use crate::avm1::fscommand; +use crate::avm1::function::Executable; use crate::avm1::return_value::ReturnValue; -use crate::avm1::{Avm1, Error, Object, UpdateContext, Value}; +use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value}; use crate::backend::navigator::NavigationMethod; use enumset::EnumSet; -use gc_arena::{GcCell, MutationContext}; +use gc_arena::MutationContext; use rand::Rng; +mod function; mod math; +mod movie_clip; +mod object; #[allow(non_snake_case, unused_must_use)] //can't use errors yet pub fn getURL<'a, 'gc>( avm: &mut Avm1<'gc>, context: &mut UpdateContext<'a, 'gc, '_>, - _this: GcCell<'gc, Object<'gc>>, + _this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { //TODO: Error behavior if no arguments are present @@ -40,7 +44,7 @@ pub fn getURL<'a, 'gc>( pub fn random<'gc>( _avm: &mut Avm1<'gc>, action_context: &mut UpdateContext<'_, 'gc, '_>, - _this: GcCell<'gc, Object<'gc>>, + _this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { match args.get(0) { @@ -52,7 +56,7 @@ pub fn random<'gc>( pub fn boolean<'gc>( avm: &mut Avm1<'gc>, _action_context: &mut UpdateContext<'_, 'gc, '_>, - _this: GcCell<'gc, Object<'gc>>, + _this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { if let Some(val) = args.get(0) { @@ -65,7 +69,7 @@ pub fn boolean<'gc>( pub fn number<'gc>( _avm: &mut Avm1<'gc>, _action_context: &mut UpdateContext<'_, 'gc, '_>, - _this: GcCell<'gc, Object<'gc>>, + _this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { if let Some(val) = args.get(0) { @@ -78,7 +82,7 @@ pub fn number<'gc>( pub fn is_nan<'gc>( _avm: &mut Avm1<'gc>, _action_context: &mut UpdateContext<'_, 'gc, '_>, - _this: GcCell<'gc, Object<'gc>>, + _this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { if let Some(val) = args.get(0) { @@ -88,24 +92,121 @@ pub fn is_nan<'gc>( } } -pub fn create_globals<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> { - let mut globals = Object::object(gc_context); +/// This structure represents all system builtins that are used regardless of +/// whatever the hell happens to `_global`. These are, of course, +/// user-modifiable. +#[derive(Clone)] +pub struct SystemPrototypes<'gc> { + pub object: ObjectCell<'gc>, + pub function: ObjectCell<'gc>, + pub movie_clip: ObjectCell<'gc>, +} - globals.force_set_function("isNaN", is_nan, gc_context, EnumSet::empty()); - globals.force_set_function("Boolean", boolean, gc_context, EnumSet::empty()); - globals.force_set("Math", math::create(gc_context), EnumSet::empty()); - globals.force_set_function("getURL", getURL, gc_context, EnumSet::empty()); - globals.force_set_function("Number", number, gc_context, EnumSet::empty()); - globals.force_set_function("random", random, gc_context, EnumSet::empty()); +unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> { + #[inline] + fn trace(&self, cc: gc_arena::CollectionContext) { + self.object.trace(cc); + self.function.trace(cc); + self.movie_clip.trace(cc); + } +} - globals.force_set("NaN", Value::Number(std::f64::NAN), EnumSet::empty()); - globals.force_set( +/// Initialize default global scope and builtins for an AVM1 instance. +pub fn create_globals<'gc>( + gc_context: MutationContext<'gc, '_>, +) -> (SystemPrototypes<'gc>, Box + 'gc>) { + let object_proto = ScriptObject::object_cell(gc_context, None); + let function_proto = function::create_proto(gc_context, object_proto); + + object::fill_proto(gc_context, object_proto, function_proto); + + let movie_clip_proto: ObjectCell<'gc> = + movie_clip::create_proto(gc_context, object_proto, function_proto); + + //TODO: These need to be constructors and should also set `.prototype` on each one + let object = ScriptObject::function( + gc_context, + Executable::Native(object::constructor), + Some(function_proto), + Some(object_proto), + ); + + let function = ScriptObject::function( + gc_context, + Executable::Native(function::constructor), + Some(function_proto), + Some(function_proto), + ); + let movie_clip = ScriptObject::function( + gc_context, + Executable::Native(movie_clip::constructor), + Some(function_proto), + Some(movie_clip_proto), + ); + + let mut globals = ScriptObject::object(gc_context, Some(object_proto)); + globals.define_value("Object", object.into(), EnumSet::empty()); + globals.define_value("Function", function.into(), EnumSet::empty()); + globals.define_value("MovieClip", movie_clip.into(), EnumSet::empty()); + globals.force_set_function( + "Number", + number, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.force_set_function( + "Boolean", + boolean, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.define_value( + "Math", + Value::Object(math::create( + gc_context, + Some(object_proto), + Some(function_proto), + )), + EnumSet::empty(), + ); + globals.force_set_function( + "isNaN", + is_nan, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.force_set_function( + "getURL", + getURL, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.force_set_function( + "random", + random, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.define_value("NaN", Value::Number(std::f64::NAN), EnumSet::empty()); + globals.define_value( "Infinity", Value::Number(std::f64::INFINITY), EnumSet::empty(), ); - globals + ( + SystemPrototypes { + object: object_proto, + function: function_proto, + movie_clip: movie_clip_proto, + }, + Box::new(globals), + ) } #[cfg(test)] diff --git a/core/src/avm1/globals/function.rs b/core/src/avm1/globals/function.rs new file mode 100644 index 000000000..344af39a1 --- /dev/null +++ b/core/src/avm1/globals/function.rs @@ -0,0 +1,73 @@ +//! Function prototype + +use crate::avm1::return_value::ReturnValue; +use crate::avm1::{Avm1, Error, ObjectCell, ScriptObject, UpdateContext, Value}; +use enumset::EnumSet; +use gc_arena::MutationContext; + +/// Implements `Function` +pub fn constructor<'gc>( + _avm: &mut Avm1<'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: ObjectCell<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined.into()) +} + +pub fn call<'gc>( + _avm: &mut Avm1<'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: ObjectCell<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined.into()) +} + +pub fn apply<'gc>( + _avm: &mut Avm1<'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: ObjectCell<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined.into()) +} + +/// Partially construct `Function.prototype`. +/// +/// `__proto__` and other cross-linked properties of this object will *not* +/// be defined here. The caller of this function is responsible for linking +/// them in order to obtain a valid ECMAScript `Function` prototype. The +/// returned object is also a bare object, which will need to be linked into +/// the prototype of `Object`. +pub fn create_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: ObjectCell<'gc>, +) -> ObjectCell<'gc> { + let function_proto = ScriptObject::object_cell(gc_context, Some(proto)); + + function_proto + .write(gc_context) + .as_script_object_mut() + .unwrap() + .force_set_function( + "call", + call, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + function_proto + .write(gc_context) + .as_script_object_mut() + .unwrap() + .force_set_function( + "apply", + apply, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + + function_proto +} diff --git a/core/src/avm1/globals/math.rs b/core/src/avm1/globals/math.rs index 88dc475b8..1b42ff255 100644 --- a/core/src/avm1/globals/math.rs +++ b/core/src/avm1/globals/math.rs @@ -1,12 +1,12 @@ -use crate::avm1::object::Attribute::*; +use crate::avm1::property::Attribute::*; use crate::avm1::return_value::ReturnValue; -use crate::avm1::{Avm1, Error, Object, UpdateContext, Value}; +use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value}; use gc_arena::{GcCell, MutationContext}; use rand::Rng; use std::f64::NAN; macro_rules! wrap_std { - ( $object: ident, $gc_context: ident, $($name:expr => $std:path),* ) => {{ + ( $object: ident, $gc_context: ident, $proto: ident, $($name:expr => $std:path),* ) => {{ $( $object.force_set_function( $name, @@ -19,6 +19,7 @@ macro_rules! wrap_std { }, $gc_context, DontDelete | ReadOnly | DontEnum, + $proto ); )* }}; @@ -27,7 +28,7 @@ macro_rules! wrap_std { fn atan2<'gc>( _avm: &mut Avm1<'gc>, _context: &mut UpdateContext<'_, 'gc, '_>, - _this: GcCell<'gc, Object<'gc>>, + _this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { if let Some(y) = args.get(0) { @@ -43,57 +44,61 @@ fn atan2<'gc>( pub fn random<'gc>( _avm: &mut Avm1<'gc>, action_context: &mut UpdateContext<'_, 'gc, '_>, - _this: GcCell<'gc, Object<'gc>>, + _this: ObjectCell<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { Ok(action_context.rng.gen_range(0.0f64, 1.0f64).into()) } -pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'gc>> { - let mut math = Object::object(gc_context); +pub fn create<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: Option>, + fn_proto: Option>, +) -> ObjectCell<'gc> { + let mut math = ScriptObject::object(gc_context, proto); - math.force_set( + math.define_value( "E", - Value::Number(std::f64::consts::E), + std::f64::consts::E.into(), DontDelete | ReadOnly | DontEnum, ); - math.force_set( + math.define_value( "LN10", - Value::Number(std::f64::consts::LN_10), + std::f64::consts::LN_10.into(), DontDelete | ReadOnly | DontEnum, ); - math.force_set( + math.define_value( "LN2", - Value::Number(std::f64::consts::LN_2), + std::f64::consts::LN_2.into(), DontDelete | ReadOnly | DontEnum, ); - math.force_set( + math.define_value( "LOG10E", - Value::Number(std::f64::consts::LOG10_E), + std::f64::consts::LOG10_E.into(), DontDelete | ReadOnly | DontEnum, ); - math.force_set( + math.define_value( "LOG2E", - Value::Number(std::f64::consts::LOG2_E), + std::f64::consts::LOG2_E.into(), DontDelete | ReadOnly | DontEnum, ); - math.force_set( + math.define_value( "PI", - Value::Number(std::f64::consts::PI), + std::f64::consts::PI.into(), DontDelete | ReadOnly | DontEnum, ); - math.force_set( + math.define_value( "SQRT1_2", - Value::Number(std::f64::consts::FRAC_1_SQRT_2), + std::f64::consts::FRAC_1_SQRT_2.into(), DontDelete | ReadOnly | DontEnum, ); - math.force_set( + math.define_value( "SQRT2", - Value::Number(std::f64::consts::SQRT_2), + std::f64::consts::SQRT_2.into(), DontDelete | ReadOnly | DontEnum, ); - wrap_std!(math, gc_context, + wrap_std!(math, gc_context, fn_proto, "abs" => f64::abs, "acos" => f64::acos, "asin" => f64::asin, @@ -108,15 +113,22 @@ pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<' "tan" => f64::tan ); - math.force_set_function("atan2", atan2, gc_context, DontDelete | ReadOnly | DontEnum); + math.force_set_function( + "atan2", + atan2, + gc_context, + DontDelete | ReadOnly | DontEnum, + fn_proto, + ); math.force_set_function( "random", random, gc_context, DontDelete | ReadOnly | DontEnum, + fn_proto, ); - GcCell::allocate(gc_context, math) + GcCell::allocate(gc_context, Box::new(math)) } #[cfg(test)] @@ -130,7 +142,7 @@ mod tests { #[test] fn $test() -> Result<(), Error> { with_avm(19, |avm, context, _root| { - let math = create(context.gc_context); + let math = create(context.gc_context, Some(avm.prototypes().object), Some(avm.prototypes().function)); let function = math.read().get($name, avm, context, math)?.unwrap_immediate(); $( @@ -236,7 +248,15 @@ mod tests { #[test] fn test_atan2_nan() { with_avm(19, |avm, context, _root| { - let math = GcCell::allocate(context.gc_context, create(context.gc_context)); + let math = GcCell::allocate( + context.gc_context, + create( + context.gc_context, + Some(avm.prototypes().object), + Some(avm.prototypes().function), + ), + ); + assert_eq!(atan2(avm, context, *math.read(), &[]).unwrap(), NAN.into()); assert_eq!( atan2(avm, context, *math.read(), &[1.0.into(), Value::Null]).unwrap(), @@ -256,7 +276,15 @@ mod tests { #[test] fn test_atan2_valid() { with_avm(19, |avm, context, _root| { - let math = GcCell::allocate(context.gc_context, create(context.gc_context)); + let math = GcCell::allocate( + context.gc_context, + create( + context.gc_context, + Some(avm.prototypes().object), + Some(avm.prototypes().function), + ), + ); + assert_eq!( atan2(avm, context, *math.read(), &[10.0.into()]).unwrap(), std::f64::consts::FRAC_PI_2.into() diff --git a/core/src/avm1/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs similarity index 75% rename from core/src/avm1/movie_clip.rs rename to core/src/avm1/globals/movie_clip.rs index 7e7505e5f..60e7ca471 100644 --- a/core/src/avm1/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -1,18 +1,30 @@ +//! MovieClip prototype + use crate::avm1::function::Executable; -use crate::avm1::object::{Attribute::*, Object}; +use crate::avm1::property::Attribute::*; use crate::avm1::return_value::ReturnValue; -use crate::avm1::{Avm1, Error, UpdateContext, Value}; +use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value}; use crate::display_object::{DisplayNode, DisplayObject, MovieClip}; use enumset::EnumSet; use gc_arena::{GcCell, MutationContext}; +/// Implements `MovieClip` +pub fn constructor<'gc>( + _avm: &mut Avm1<'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: ObjectCell<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined.into()) +} + macro_rules! with_movie_clip { - ( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{ + ( $gc_context: ident, $object:ident, $fn_proto: expr, $($name:expr => $fn:expr),* ) => {{ $( $object.force_set_function( $name, |_avm, _context, this, args| -> Result, Error> { - if let Some(display_object) = this.read().display_node() { + if let Some(display_object) = this.read().as_display_node() { if let Some(movie_clip) = display_object.read().as_movie_clip() { return Ok($fn(movie_clip, args)); } @@ -21,18 +33,19 @@ macro_rules! with_movie_clip { }, $gc_context, DontDelete | ReadOnly | DontEnum, + $fn_proto ); )* }}; } macro_rules! with_movie_clip_mut { - ( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{ + ( $gc_context: ident, $object:ident, $fn_proto: expr, $($name:expr => $fn:expr),* ) => {{ $( $object.force_set_function( $name, |_avm, context: &mut UpdateContext<'_, 'gc, '_>, this, args| -> Result, Error> { - if let Some(display_object) = this.read().display_node() { + if let Some(display_object) = this.read().as_display_node() { if let Some(movie_clip) = display_object.write(context.gc_context).as_movie_clip_mut() { return Ok($fn(movie_clip, context, display_object, args).into()); } @@ -41,6 +54,7 @@ macro_rules! with_movie_clip_mut { } as crate::avm1::function::NativeFunction<'gc>, $gc_context, DontDelete | ReadOnly | DontEnum, + $fn_proto ); )* }}; @@ -49,7 +63,7 @@ macro_rules! with_movie_clip_mut { pub fn overwrite_root<'gc>( _avm: &mut Avm1<'gc>, ac: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, + this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { let new_val = args @@ -57,7 +71,7 @@ pub fn overwrite_root<'gc>( .map(|v| v.to_owned()) .unwrap_or(Value::Undefined); this.write(ac.gc_context) - .force_set("_root", new_val, EnumSet::new()); + .define_value("_root", new_val, EnumSet::new()); Ok(Value::Undefined.into()) } @@ -65,7 +79,7 @@ pub fn overwrite_root<'gc>( pub fn overwrite_global<'gc>( _avm: &mut Avm1<'gc>, ac: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, + this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { let new_val = args @@ -73,17 +87,22 @@ pub fn overwrite_global<'gc>( .map(|v| v.to_owned()) .unwrap_or(Value::Undefined); this.write(ac.gc_context) - .force_set("_global", new_val, EnumSet::new()); + .define_value("_global", new_val, EnumSet::new()); Ok(Value::Undefined.into()) } -pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> { - let mut object = Object::object(gc_context); +pub fn create_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: ObjectCell<'gc>, + fn_proto: ObjectCell<'gc>, +) -> ObjectCell<'gc> { + let mut object = ScriptObject::object(gc_context, Some(proto)); with_movie_clip_mut!( gc_context, object, + Some(fn_proto), "nextFrame" => |movie_clip: &mut MovieClip<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _cell: DisplayNode<'gc>, _args| { movie_clip.next_frame(context); Value::Undefined @@ -105,6 +124,7 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object< with_movie_clip!( gc_context, object, + Some(fn_proto), "getBytesLoaded" => |_movie_clip: &MovieClip<'gc>, _args| { // TODO find a correct value 1.0.into() @@ -118,26 +138,26 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object< } ); - object.force_set_virtual( + object.add_property( "_global", Executable::Native(|avm, context, _this, _args| Ok(avm.global_object(context).into())), Some(Executable::Native(overwrite_global)), EnumSet::new(), ); - object.force_set_virtual( + object.add_property( "_root", Executable::Native(|avm, context, _this, _args| Ok(avm.root_object(context).into())), Some(Executable::Native(overwrite_root)), EnumSet::new(), ); - object.force_set_virtual( + object.add_property( "_parent", Executable::Native(|_avm, _context, this, _args| { Ok(this .read() - .display_node() + .as_display_node() .and_then(|mc| mc.read().parent()) .and_then(|dn| dn.read().object().as_object().ok()) .map(|o| Value::Object(o.to_owned())) @@ -148,5 +168,5 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object< EnumSet::new(), ); - object + GcCell::allocate(gc_context, Box::new(object)) } diff --git a/core/src/avm1/globals/object.rs b/core/src/avm1/globals/object.rs new file mode 100644 index 000000000..768a9b884 --- /dev/null +++ b/core/src/avm1/globals/object.rs @@ -0,0 +1,214 @@ +//! Object prototype +use crate::avm1::property::Attribute::*; +use crate::avm1::return_value::ReturnValue; +use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext, Value}; +use enumset::EnumSet; +use gc_arena::{GcCell, MutationContext}; + +/// Implements `Object` +pub fn constructor<'gc>( + _avm: &mut Avm1<'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: ObjectCell<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined.into()) +} + +/// Implements `Object.prototype.addProperty` +pub fn add_property<'gc>( + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let name = args.get(0).unwrap_or(&Value::Undefined); + let getter = args.get(1).unwrap_or(&Value::Undefined); + let setter = args.get(2).unwrap_or(&Value::Undefined); + + match (name, getter) { + (Value::String(name), Value::Object(get)) if !name.is_empty() => { + if let Some(get_func) = get.read().as_executable() { + if let Value::Object(set) = setter { + if let Some(set_func) = set.read().as_executable() { + this.write(context.gc_context).add_property( + name, + get_func, + Some(set_func), + EnumSet::empty(), + ); + } else { + return Ok(false.into()); + } + } else if let Value::Null = setter { + this.write(context.gc_context).add_property( + name, + get_func, + None, + ReadOnly.into(), + ); + } else { + return Ok(false.into()); + } + } + + Ok(false.into()) + } + _ => Ok(false.into()), + } +} + +/// Implements `Object.prototype.hasOwnProperty` +pub fn has_own_property<'gc>( + _avm: &mut Avm1<'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + match args.get(0) { + Some(Value::String(name)) => Ok(Value::Bool(this.read().has_own_property(name)).into()), + _ => Ok(Value::Bool(false).into()), + } +} + +/// Implements `Object.prototype.toString` +fn to_string<'gc>( + _: &mut Avm1<'gc>, + _: &mut UpdateContext<'_, 'gc, '_>, + _: ObjectCell<'gc>, + _: &[Value<'gc>], +) -> Result, Error> { + Ok(ReturnValue::Immediate("[object Object]".into())) +} + +/// Implements `Object.prototype.isPropertyEnumerable` +fn is_property_enumerable<'gc>( + _: &mut Avm1<'gc>, + _: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + match args.get(0) { + Some(Value::String(name)) => { + Ok(Value::Bool(this.read().is_property_enumerable(name)).into()) + } + _ => Ok(Value::Bool(false).into()), + } +} + +/// Implements `Object.prototype.isPrototypeOf` +fn is_prototype_of<'gc>( + _: &mut Avm1<'gc>, + _: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + match args.get(0) { + Some(val) => { + let ob = match val.as_object() { + Ok(ob) => ob, + Err(_) => return Ok(Value::Bool(false).into()), + }; + let mut proto = ob.read().proto(); + + while let Some(proto_ob) = proto { + if GcCell::ptr_eq(this, proto_ob) { + return Ok(Value::Bool(true).into()); + } + + proto = proto_ob.read().proto(); + } + + Ok(Value::Bool(false).into()) + } + _ => Ok(Value::Bool(false).into()), + } +} + +/// Implements `Object.prototype.valueOf` +fn value_of<'gc>( + _: &mut Avm1<'gc>, + _: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + _: &[Value<'gc>], +) -> Result, Error> { + Ok(ReturnValue::Immediate(this.into())) +} + +/// Partially construct `Object.prototype`. +/// +/// `__proto__` and other cross-linked properties of this object will *not* +/// be defined here. The caller of this function is responsible for linking +/// them in order to obtain a valid ECMAScript `Object` prototype. +/// +/// Since Object and Function are so heavily intertwined, this function does +/// not allocate an object to store either proto. Instead, you must allocate +/// bare objects for both and let this function fill Object for you. +pub fn fill_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + object_proto: ObjectCell<'gc>, + fn_proto: ObjectCell<'gc>, +) { + let mut ob_proto_write = object_proto.write(gc_context); + + ob_proto_write + .as_script_object_mut() + .unwrap() + .force_set_function( + "addProperty", + add_property, + gc_context, + DontDelete | DontEnum, + Some(fn_proto), + ); + ob_proto_write + .as_script_object_mut() + .unwrap() + .force_set_function( + "hasOwnProperty", + has_own_property, + gc_context, + DontDelete | DontEnum, + Some(fn_proto), + ); + ob_proto_write + .as_script_object_mut() + .unwrap() + .force_set_function( + "isPropertyEnumerable", + is_property_enumerable, + gc_context, + DontDelete | DontEnum, + Some(fn_proto), + ); + ob_proto_write + .as_script_object_mut() + .unwrap() + .force_set_function( + "isPrototypeOf", + is_prototype_of, + gc_context, + DontDelete | DontEnum, + Some(fn_proto), + ); + ob_proto_write + .as_script_object_mut() + .unwrap() + .force_set_function( + "toString", + to_string, + gc_context, + DontDelete | DontEnum, + Some(fn_proto), + ); + ob_proto_write + .as_script_object_mut() + .unwrap() + .force_set_function( + "valueOf", + value_of, + gc_context, + DontDelete | DontEnum, + Some(fn_proto), + ); +} diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index 385d2a801..f2565fad3 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -1,681 +1,187 @@ -use self::Attribute::*; -use crate::avm1::function::{Avm1Function, Executable, NativeFunction}; +//! Object trait to expose objects to AVM + +use crate::avm1::function::Executable; +use crate::avm1::property::Attribute; use crate::avm1::return_value::ReturnValue; -use crate::avm1::{Avm1, Error, UpdateContext, Value}; +use crate::avm1::{Avm1, Error, ScriptObject, UpdateContext, Value}; use crate::display_object::DisplayNode; -use core::fmt; -use enumset::{EnumSet, EnumSetType}; -use gc_arena::{GcCell, MutationContext}; -use std::collections::hash_map::Entry; -use std::collections::HashMap; -use std::mem::replace; +use enumset::EnumSet; +use gc_arena::{Collect, GcCell}; +use std::collections::HashSet; +use std::fmt::Debug; -pub const TYPE_OF_OBJECT: &str = "object"; -pub const TYPE_OF_FUNCTION: &str = "function"; -pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip"; +pub type ObjectCell<'gc> = GcCell<'gc, Box + 'gc>>; -fn default_to_string<'gc>( - _: &mut Avm1<'gc>, - _: &mut UpdateContext<'_, 'gc, '_>, - _: GcCell<'gc, Object<'gc>>, - _: &[Value<'gc>], -) -> Result, Error> { - Ok(ReturnValue::Immediate("[Object object]".into())) -} - -#[derive(EnumSetType, Debug)] -pub enum Attribute { - DontDelete, - DontEnum, - ReadOnly, -} - -#[derive(Clone)] -pub enum Property<'gc> { - Virtual { - get: Executable<'gc>, - set: Option>, - attributes: EnumSet, - }, - Stored { - value: Value<'gc>, - attributes: EnumSet, - }, -} - -impl<'gc> Property<'gc> { - /// Get the value of a property slot. +/// Represents an object that can be directly interacted with by the AVM +/// runtime. +pub trait Object<'gc>: 'gc + Collect + Debug { + /// Retrieve a named property from this object exclusively. /// - /// This function yields `None` if the value is being determined on the AVM - /// stack. Otherwise, if the value can be determined on the Rust stack, - /// then this function returns the value. - pub fn get( - &self, - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, - ) -> Result, Error> { - match self { - Property::Virtual { get, .. } => get.exec(avm, context, this, &[]), - Property::Stored { value, .. } => Ok(value.to_owned().into()), - } - } - - /// Set a property slot. + /// This function takes a redundant `this` parameter which should be + /// the object's own `GcCell`, so that it can pass it to user-defined + /// overrides that may need to interact with the underlying object. /// - /// This function returns `true` if the set has completed, or `false` if - /// it has not yet occured. If `false`, and you need to run code after the - /// set has occured, you must recursively execute the top-most frame via - /// `run_current_frame`. - pub fn set( - &mut self, - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, - new_value: impl Into>, - ) -> Result { - match self { - Property::Virtual { set, .. } => { - if let Some(function) = set { - let return_value = function.exec(avm, context, this, &[new_value.into()])?; - Ok(return_value.is_immediate()) - } else { - Ok(true) - } - } - Property::Stored { - value, attributes, .. - } => { - if !attributes.contains(ReadOnly) { - replace::>(value, new_value.into()); - } - - Ok(true) - } - } - } - - pub fn can_delete(&self) -> bool { - match self { - Property::Virtual { attributes, .. } => !attributes.contains(DontDelete), - Property::Stored { attributes, .. } => !attributes.contains(DontDelete), - } - } - - pub fn is_enumerable(&self) -> bool { - match self { - Property::Virtual { attributes, .. } => !attributes.contains(DontEnum), - Property::Stored { attributes, .. } => !attributes.contains(DontEnum), - } - } - - pub fn is_overwritable(&self) -> bool { - match self { - Property::Virtual { - attributes, set, .. - } => !attributes.contains(ReadOnly) && !set.is_none(), - Property::Stored { attributes, .. } => !attributes.contains(ReadOnly), - } - } -} - -unsafe impl<'gc> gc_arena::Collect for Property<'gc> { - fn trace(&self, cc: gc_arena::CollectionContext) { - match self { - Property::Virtual { get, set, .. } => { - get.trace(cc); - set.trace(cc); - } - Property::Stored { value, .. } => value.trace(cc), - } - } -} - -impl fmt::Debug for Property<'_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Property::Virtual { - get: _, - set, - attributes, - } => f - .debug_struct("Property::Virtual") - .field("get", &true) - .field("set", &set.is_some()) - .field("attributes", &attributes) - .finish(), - Property::Stored { value, attributes } => f - .debug_struct("Property::Stored") - .field("value", &value) - .field("attributes", &attributes) - .finish(), - } - } -} - -#[derive(Clone)] -pub struct Object<'gc> { - display_node: Option>, - values: HashMap>, - function: Option>, - type_of: &'static str, -} - -unsafe impl<'gc> gc_arena::Collect for Object<'gc> { - fn trace(&self, cc: gc_arena::CollectionContext) { - self.display_node.trace(cc); - self.values.trace(cc); - } -} - -impl fmt::Debug for Object<'_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Object") - .field("display_node", &self.display_node) - .field("values", &self.values) - .field("function", &self.function.is_some()) - .finish() - } -} - -impl<'gc> Object<'gc> { - pub fn object(gc_context: MutationContext<'gc, '_>) -> Self { - let mut result = Self { - type_of: TYPE_OF_OBJECT, - display_node: None, - values: HashMap::new(), - function: None, - }; - - result.force_set_function( - "toString", - default_to_string, - gc_context, - DontDelete | DontEnum, - ); - - result - } - - /// Constructs an object with no values, not even builtins. - /// - /// Intended for constructing scope chains, since they exclusively use the - /// object values, but can't just have a hashmap because of `with` and - /// friends. - pub fn bare_object() -> Self { - Self { - type_of: TYPE_OF_OBJECT, - display_node: None, - values: HashMap::new(), - function: None, - } - } - - pub fn native_function(function: NativeFunction<'gc>) -> Self { - Self { - type_of: TYPE_OF_FUNCTION, - function: Some(Executable::Native(function)), - display_node: None, - values: HashMap::new(), - } - } - - pub fn action_function(func: Avm1Function<'gc>) -> Self { - Self { - type_of: TYPE_OF_FUNCTION, - function: Some(Executable::Action(func)), - display_node: None, - values: HashMap::new(), - } - } - - pub fn set_display_node(&mut self, display_node: DisplayNode<'gc>) { - self.display_node = Some(display_node); - } - - pub fn display_node(&self) -> Option> { - self.display_node - } - - pub fn set( - &mut self, - name: &str, - value: impl Into>, - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, - ) -> Result<(), Error> { - match self.values.entry(name.to_owned()) { - Entry::Occupied(mut entry) => { - entry.get_mut().set(avm, context, this, value)?; - Ok(()) - } - Entry::Vacant(entry) => { - entry.insert(Property::Stored { - value: value.into(), - attributes: Default::default(), - }); - Ok(()) - } - } - } - - pub fn force_set_virtual( - &mut self, - name: &str, - get: Executable<'gc>, - set: Option>, - attributes: A, - ) where - A: Into>, - { - self.values.insert( - name.to_owned(), - Property::Virtual { - get, - set, - attributes: attributes.into(), - }, - ); - } - - pub fn force_set(&mut self, name: &str, value: impl Into>, attributes: A) - where - A: Into>, - { - self.values.insert( - name.to_string(), - Property::Stored { - value: value.into(), - attributes: attributes.into(), - }, - ); - } - - pub fn force_set_function( - &mut self, - name: &str, - function: NativeFunction<'gc>, - gc_context: MutationContext<'gc, '_>, - attributes: A, - ) where - A: Into>, - { - self.force_set( - name, - GcCell::allocate(gc_context, Object::native_function(function)), - attributes, - ) - } - - /// Get the value of a particular property on this object. - /// - /// The `avm`, `context`, and `this` parameters exist so that this object - /// can call virtual properties. Likewise, this function returns a - /// `ReturnValue` which allows pulling data from the return values of user - /// functions. - pub fn get( + /// This function should not inspect prototype chains. Instead, use `get` + /// to do ordinary property look-up and resolution. + fn get_local( &self, name: &str, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, - ) -> Result, Error> { - if let Some(value) = self.values.get(name) { - return value.get(avm, context, this); - } + this: ObjectCell<'gc>, + ) -> Result, Error>; - Ok(Value::Undefined.into()) - } - - /// Delete a given value off the object. - pub fn delete(&mut self, name: &str) -> bool { - if let Some(prop) = self.values.get(name) { - if prop.can_delete() { - self.values.remove(name); - return true; - } - } - - false - } - - pub fn has_property(&self, name: &str) -> bool { - self.values.contains_key(name) - } - - pub fn has_own_property(&self, name: &str) -> bool { - self.values.contains_key(name) - } - - pub fn is_property_overwritable(&self, name: &str) -> bool { - self.values - .get(name) - .map(|p| p.is_overwritable()) - .unwrap_or(false) - } - - pub fn get_keys(&self) -> Vec { - self.values - .iter() - .filter_map(|(k, p)| { - if p.is_enumerable() { - Some(k.to_string()) - } else { - None - } - }) - .collect() - } - - pub fn call( + /// Retrieve a named property from the object, or it's prototype. + fn get( &self, + name: &str, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, - args: &[Value<'gc>], + this: ObjectCell<'gc>, ) -> Result, Error> { - if let Some(function) = &self.function { - function.exec(avm, context, this, args) + if self.has_own_property(name) { + self.get_local(name, avm, context, this) } else { + let mut depth = 0; + let mut proto = self.proto(); + + while proto.is_some() { + if depth == 255 { + return Err("Encountered an excessively deep prototype chain.".into()); + } + + if proto.unwrap().read().has_own_property(name) { + return proto.unwrap().read().get_local(name, avm, context, this); + } + + proto = proto.unwrap().read().proto(); + depth += 1; + } + Ok(Value::Undefined.into()) } } - pub fn as_string(&self) -> String { - if self.function.is_some() { - "[type Function]".to_string() - } else { - "[object Object]".to_string() - } - } + /// Set a named property on this object, or it's prototype. + /// + /// This function takes a redundant `this` parameter which should be + /// the object's own `GcCell`, so that it can pass it to user-defined + /// overrides that may need to interact with the underlying object. + fn set( + &mut self, + name: &str, + value: Value<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + ) -> Result<(), Error>; - pub fn set_type_of(&mut self, type_of: &'static str) { - self.type_of = type_of; - } + /// Call the underlying object. + /// + /// This function takes a redundant `this` parameter which should be + /// the object's own `GcCell`, so that it can pass it to user-defined + /// overrides that may need to interact with the underlying object. + fn call( + &self, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + args: &[Value<'gc>], + ) -> Result, Error>; - pub fn type_of(&self) -> &'static str { - self.type_of - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::avm1::activation::Activation; - use crate::backend::audio::NullAudioBackend; - use crate::backend::navigator::NullNavigatorBackend; - use crate::backend::render::NullRenderer; - use crate::display_object::{DisplayObject, MovieClip}; - use crate::library::Library; - use crate::prelude::*; - use gc_arena::rootless_arena; - use rand::{rngs::SmallRng, SeedableRng}; - use std::sync::Arc; - - fn with_object(swf_version: u8, test: F) -> R - where - F: for<'a, 'gc> FnOnce( - &mut Avm1<'gc>, - &mut UpdateContext<'a, 'gc, '_>, - GcCell<'gc, Object<'gc>>, - ) -> R, - { - rootless_arena(|gc_context| { - let mut avm = Avm1::new(gc_context, swf_version); - let movie_clip: Box = - Box::new(MovieClip::new(swf_version, gc_context)); - let root = GcCell::allocate(gc_context, movie_clip); - let mut context = UpdateContext { - gc_context, - global_time: 0, - player_version: 32, - swf_version, - root, - start_clip: root, - active_clip: root, - target_clip: Some(root), - target_path: Value::Undefined, - rng: &mut SmallRng::from_seed([0u8; 16]), - action_queue: &mut crate::context::ActionQueue::new(), - audio: &mut NullAudioBackend::new(), - background_color: &mut Color { - r: 0, - g: 0, - b: 0, - a: 0, - }, - library: &mut Library::new(), - navigator: &mut NullNavigatorBackend::new(), - renderer: &mut NullRenderer::new(), - swf_data: &mut Arc::new(vec![]), - }; - - let object = GcCell::allocate(gc_context, Object::object(gc_context)); - - let globals = avm.global_object_cell(); - avm.insert_stack_frame(GcCell::allocate( - gc_context, - Activation::from_nothing(swf_version, globals, gc_context), - )); - - test(&mut avm, &mut context, object) - }) - } - - #[test] - fn test_get_undefined() { - with_object(0, |avm, context, object| { - assert_eq!( - object - .read() - .get("not_defined", avm, context, object) - .unwrap(), - Value::Undefined.into() - ); - }) - } - - #[test] - fn test_set_get() { - with_object(0, |avm, context, object| { - object - .write(context.gc_context) - .force_set("forced", "forced", EnumSet::empty()); - object - .write(context.gc_context) - .set("natural", "natural", avm, context, object) - .unwrap(); - - assert_eq!( - object.read().get("forced", avm, context, object).unwrap(), - ReturnValue::Immediate("forced".into()) - ); - assert_eq!( - object.read().get("natural", avm, context, object).unwrap(), - ReturnValue::Immediate("natural".into()) - ); - }) - } - - #[test] - fn test_set_readonly() { - with_object(0, |avm, context, object| { - object - .write(context.gc_context) - .force_set("normal", "initial", EnumSet::empty()); - object - .write(context.gc_context) - .force_set("readonly", "initial", ReadOnly); - - object - .write(context.gc_context) - .set("normal", "replaced", avm, context, object) - .unwrap(); - object - .write(context.gc_context) - .set("readonly", "replaced", avm, context, object) - .unwrap(); - - assert_eq!( - object.read().get("normal", avm, context, object).unwrap(), - ReturnValue::Immediate("replaced".into()) - ); - assert_eq!( - object.read().get("readonly", avm, context, object).unwrap(), - ReturnValue::Immediate("initial".into()) - ); - }) - } - - #[test] - fn test_deletable_not_readonly() { - with_object(0, |avm, context, object| { - object - .write(context.gc_context) - .force_set("test", "initial", DontDelete); - - assert_eq!(object.write(context.gc_context).delete("test"), false); - assert_eq!( - object.read().get("test", avm, context, object).unwrap(), - ReturnValue::Immediate("initial".into()) - ); - - object - .write(context.gc_context) - .set("test", "replaced", avm, context, object) - .unwrap(); - - assert_eq!(object.write(context.gc_context).delete("test"), false); - assert_eq!( - object.read().get("test", avm, context, object).unwrap(), - ReturnValue::Immediate("replaced".into()) - ); - }) - } - - #[test] - fn test_virtual_get() { - with_object(0, |avm, context, object| { - let getter = Executable::Native(|_avm, _context, _this, _args| { - Ok(ReturnValue::Immediate("Virtual!".into())) - }); - - object.write(context.gc_context).force_set_virtual( - "test", - getter, - None, - EnumSet::empty(), - ); - - assert_eq!( - object.read().get("test", avm, context, object).unwrap(), - ReturnValue::Immediate("Virtual!".into()) - ); - - // This set should do nothing - object - .write(context.gc_context) - .set("test", "Ignored!", avm, context, object) - .unwrap(); - assert_eq!( - object.read().get("test", avm, context, object).unwrap(), - ReturnValue::Immediate("Virtual!".into()) - ); - }) - } - - #[test] - fn test_delete() { - with_object(0, |avm, context, object| { - let getter = Executable::Native(|_avm, _context, _this, _args| { - Ok(ReturnValue::Immediate("Virtual!".into())) - }); - - object.write(context.gc_context).force_set_virtual( - "virtual", - getter.clone(), - None, - EnumSet::empty(), - ); - object.write(context.gc_context).force_set_virtual( - "virtual_un", - getter, - None, - DontDelete, - ); - object - .write(context.gc_context) - .force_set("stored", "Stored!", EnumSet::empty()); - object - .write(context.gc_context) - .force_set("stored_un", "Stored!", DontDelete); - - assert_eq!(object.write(context.gc_context).delete("virtual"), true); - assert_eq!(object.write(context.gc_context).delete("virtual_un"), false); - assert_eq!(object.write(context.gc_context).delete("stored"), true); - assert_eq!(object.write(context.gc_context).delete("stored_un"), false); - assert_eq!( - object.write(context.gc_context).delete("non_existent"), - false - ); - - assert_eq!( - object.read().get("virtual", avm, context, object).unwrap(), - Value::Undefined.into() - ); - assert_eq!( - object - .read() - .get("virtual_un", avm, context, object) - .unwrap(), - ReturnValue::Immediate("Virtual!".into()) - ); - assert_eq!( - object.read().get("stored", avm, context, object).unwrap(), - Value::Undefined.into() - ); - assert_eq!( - object - .read() - .get("stored_un", avm, context, object) - .unwrap(), - ReturnValue::Immediate("Stored!".into()) - ); - }) - } - - #[test] - fn test_iter_values() { - with_object(0, |_avm, context, object| { - let getter = Executable::Native(|_avm, _context, _this, _args| Ok(Value::Null.into())); - - object - .write(context.gc_context) - .force_set("stored", Value::Null, EnumSet::empty()); - object - .write(context.gc_context) - .force_set("stored_hidden", Value::Null, DontEnum); - object.write(context.gc_context).force_set_virtual( - "virtual", - getter.clone(), - None, - EnumSet::empty(), - ); - object.write(context.gc_context).force_set_virtual( - "virtual_hidden", - getter, - None, - DontEnum, - ); - - let keys = object.read().get_keys(); - assert_eq!(keys.len(), 2); - assert_eq!(keys.contains(&"stored".to_string()), true); - assert_eq!(keys.contains(&"stored_hidden".to_string()), false); - assert_eq!(keys.contains(&"virtual".to_string()), true); - assert_eq!(keys.contains(&"virtual_hidden".to_string()), false); - }) - } + /// Construct a host object of some kind and return it's cell. + /// + /// As the first step in object construction, the `new` method is called on + /// the prototype to initialize an object. The prototype may construct any + /// object implementation it wants, with itself as the new object's proto. + /// Then, the constructor is `call`ed with the new object as `this` to + /// initialize the object. + /// + /// The arguments passed to the constructor are provided here; however, all + /// object construction should happen in `call`, not `new`. `new` exists + /// purely so that host objects can be constructed by the VM. + fn new( + &self, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + args: &[Value<'gc>], + ) -> Result, Error>; + + /// Delete a named property from the object. + /// + /// Returns false if the property cannot be deleted. + fn delete(&mut self, name: &str) -> bool; + + /// Retrieve the `__proto__` of a given object. + /// + /// The proto is another object used to resolve methods across a class of + /// multiple objects. It should also be accessible as `__proto__` from + /// `get`. + fn proto(&self) -> Option>; + + /// Define a value on an object. + /// + /// Unlike setting a value, this function is intended to replace any + /// existing virtual or built-in properties already installed on a given + /// object. As such, this should not run any setters; the resulting name + /// slot should either be completely replaced with the value or completely + /// untouched. + /// + /// It is not guaranteed that all objects accept value definitions, + /// especially if a property name conflicts with a built-in property, such + /// as `__proto__`. + fn define_value(&mut self, name: &str, value: Value<'gc>, attributes: EnumSet); + + /// Define a virtual property onto a given object. + /// + /// A virtual property is a set of get/set functions that are called when a + /// given named property is retrieved or stored on an object. These + /// functions are then responsible for providing or accepting the value + /// that is given to or taken from the AVM. + /// + /// It is not guaranteed that all objects accept virtual properties, + /// especially if a property name conflicts with a built-in property, such + /// as `__proto__`. + fn add_property( + &mut self, + name: &str, + get: Executable<'gc>, + set: Option>, + attributes: EnumSet, + ); + + /// Checks if the object has a given named property. + fn has_property(&self, name: &str) -> bool; + + /// Checks if the object has a given named property on itself (and not, + /// say, the object's prototype or superclass) + fn has_own_property(&self, name: &str) -> bool; + + /// Checks if a named property can be overwritten. + fn is_property_overwritable(&self, name: &str) -> bool; + + /// Checks if a named property appears when enumerating the object. + fn is_property_enumerable(&self, name: &str) -> bool; + + /// Enumerate the object. + fn get_keys(&self) -> HashSet; + + /// Coerce the object into a string. + fn as_string(&self) -> String; + + /// Get the object's type string. + fn type_of(&self) -> &'static str; + + /// Get the underlying script object, if it exists. + fn as_script_object(&self) -> Option<&ScriptObject<'gc>>; + + /// Get the underlying script object, if it exists. + fn as_script_object_mut(&mut self) -> Option<&mut ScriptObject<'gc>>; + + /// Get the underlying display node for this object, if it exists. + fn as_display_node(&self) -> Option>; + + /// Get the underlying executable for this object, if it exists. + fn as_executable(&self) -> Option>; } diff --git a/core/src/avm1/property.rs b/core/src/avm1/property.rs new file mode 100644 index 000000000..7839765da --- /dev/null +++ b/core/src/avm1/property.rs @@ -0,0 +1,138 @@ +//! User-defined properties + +use self::Attribute::*; +use crate::avm1::function::Executable; +use crate::avm1::return_value::ReturnValue; +use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext, Value}; +use core::fmt; +use enumset::{EnumSet, EnumSetType}; +use std::mem::replace; + +#[derive(EnumSetType, Debug)] +pub enum Attribute { + DontDelete, + DontEnum, + ReadOnly, +} + +#[derive(Clone)] +pub enum Property<'gc> { + Virtual { + get: Executable<'gc>, + set: Option>, + attributes: EnumSet, + }, + Stored { + value: Value<'gc>, + attributes: EnumSet, + }, +} + +impl<'gc> Property<'gc> { + /// Get the value of a property slot. + /// + /// This function yields `ReturnValue` because some properties may be + /// user-defined. + pub fn get( + &self, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + ) -> Result, Error> { + match self { + Property::Virtual { get, .. } => get.exec(avm, context, this, &[]), + Property::Stored { value, .. } => Ok(value.to_owned().into()), + } + } + + /// Set a property slot. + /// + /// This function returns `true` if the set has completed, or `false` if + /// it has not yet occured. If `false`, and you need to run code after the + /// set has occured, you must recursively execute the top-most frame via + /// `run_current_frame`. + pub fn set( + &mut self, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + new_value: impl Into>, + ) -> Result { + match self { + Property::Virtual { set, .. } => { + if let Some(function) = set { + let return_value = function.exec(avm, context, this, &[new_value.into()])?; + Ok(return_value.is_immediate()) + } else { + Ok(true) + } + } + Property::Stored { + value, attributes, .. + } => { + if !attributes.contains(ReadOnly) { + replace::>(value, new_value.into()); + } + + Ok(true) + } + } + } + + pub fn can_delete(&self) -> bool { + match self { + Property::Virtual { attributes, .. } => !attributes.contains(DontDelete), + Property::Stored { attributes, .. } => !attributes.contains(DontDelete), + } + } + + pub fn is_enumerable(&self) -> bool { + match self { + Property::Virtual { attributes, .. } => !attributes.contains(DontEnum), + Property::Stored { attributes, .. } => !attributes.contains(DontEnum), + } + } + + pub fn is_overwritable(&self) -> bool { + match self { + Property::Virtual { + attributes, set, .. + } => !attributes.contains(ReadOnly) && !set.is_none(), + Property::Stored { attributes, .. } => !attributes.contains(ReadOnly), + } + } +} + +unsafe impl<'gc> gc_arena::Collect for Property<'gc> { + fn trace(&self, cc: gc_arena::CollectionContext) { + match self { + Property::Virtual { get, set, .. } => { + get.trace(cc); + set.trace(cc); + } + Property::Stored { value, .. } => value.trace(cc), + } + } +} + +impl fmt::Debug for Property<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Property::Virtual { + get: _, + set, + attributes, + } => f + .debug_struct("Property::Virtual") + .field("get", &true) + .field("set", &set.is_some()) + .field("attributes", &attributes) + .finish(), + Property::Stored { value, attributes } => f + .debug_struct("Property::Stored") + .field("value", &value) + .field("attributes", &attributes) + .finish(), + } + } +} diff --git a/core/src/avm1/return_value.rs b/core/src/avm1/return_value.rs index 69adc2ae2..8ad48666e 100644 --- a/core/src/avm1/return_value.rs +++ b/core/src/avm1/return_value.rs @@ -1,7 +1,7 @@ //! Return value enum use crate::avm1::activation::Activation; -use crate::avm1::{Avm1, Error, Object, Value}; +use crate::avm1::{Avm1, Error, ObjectCell, Value}; use crate::context::UpdateContext; use gc_arena::{Collect, GcCell}; use std::fmt; @@ -99,12 +99,6 @@ impl<'gc> ReturnValue<'gc> { } } - /// Consumes the given return value. - /// - /// This exists primarily so that users of return values can indicate that - /// they do not plan to use them. - pub fn ignore(self) {} - pub fn is_immediate(&self) -> bool { use ReturnValue::*; @@ -152,8 +146,8 @@ impl<'gc> From for ReturnValue<'gc> { } } -impl<'gc> From>> for ReturnValue<'gc> { - fn from(object: GcCell<'gc, Object<'gc>>) -> Self { +impl<'gc> From> for ReturnValue<'gc> { + fn from(object: ObjectCell<'gc>) -> Self { ReturnValue::Immediate(Value::Object(object)) } } diff --git a/core/src/avm1/scope.rs b/core/src/avm1/scope.rs index a05a9c2a2..406a69041 100644 --- a/core/src/avm1/scope.rs +++ b/core/src/avm1/scope.rs @@ -1,7 +1,7 @@ //! Represents AVM1 scope chain resolution. use crate::avm1::return_value::ReturnValue; -use crate::avm1::{Avm1, Error, Object, UpdateContext, Value}; +use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value}; use enumset::EnumSet; use gc_arena::{GcCell, MutationContext}; use std::cell::{Ref, RefMut}; @@ -30,7 +30,7 @@ pub enum ScopeClass { pub struct Scope<'gc> { parent: Option>>, class: ScopeClass, - values: GcCell<'gc, Object<'gc>>, + values: ObjectCell<'gc>, } unsafe impl<'gc> gc_arena::Collect for Scope<'gc> { @@ -43,7 +43,7 @@ unsafe impl<'gc> gc_arena::Collect for Scope<'gc> { impl<'gc> Scope<'gc> { /// Construct a global scope (one without a parent). - pub fn from_global_object(globals: GcCell<'gc, Object<'gc>>) -> Scope<'gc> { + pub fn from_global_object(globals: ObjectCell<'gc>) -> Scope<'gc> { Scope { parent: None, class: ScopeClass::Global, @@ -56,7 +56,7 @@ impl<'gc> Scope<'gc> { Scope { parent: Some(parent), class: ScopeClass::Local, - values: GcCell::allocate(mc, Object::bare_object()), + values: ScriptObject::object_cell(mc, None), } } @@ -109,7 +109,7 @@ impl<'gc> Scope<'gc> { Self { parent: None, class: ScopeClass::Global, - values: GcCell::allocate(mc, Object::bare_object()), + values: ScriptObject::object_cell(mc, None), }, ) }) @@ -119,7 +119,7 @@ impl<'gc> Scope<'gc> { /// scope has been replaced with another given object. pub fn new_target_scope( mut parent: GcCell<'gc, Self>, - clip: GcCell<'gc, Object<'gc>>, + clip: ObjectCell<'gc>, mc: MutationContext<'gc, '_>, ) -> GcCell<'gc, Self> { let mut bottom_scope = None; @@ -163,7 +163,7 @@ impl<'gc> Scope<'gc> { Self { parent: None, class: ScopeClass::Global, - values: GcCell::allocate(mc, Object::bare_object()), + values: ScriptObject::object_cell(mc, None), }, ) }) @@ -176,7 +176,7 @@ impl<'gc> Scope<'gc> { /// scope. This requires some scope chain juggling. pub fn new_with_scope( locals: GcCell<'gc, Self>, - with_object: GcCell<'gc, Object<'gc>>, + with_object: ObjectCell<'gc>, mc: MutationContext<'gc, '_>, ) -> GcCell<'gc, Self> { let parent_scope = locals.read().parent; @@ -204,7 +204,7 @@ impl<'gc> Scope<'gc> { pub fn new( parent: GcCell<'gc, Self>, class: ScopeClass, - with_object: GcCell<'gc, Object<'gc>>, + with_object: ObjectCell<'gc>, ) -> Scope<'gc> { Scope { parent: Some(parent), @@ -214,17 +214,17 @@ impl<'gc> Scope<'gc> { } /// Returns a reference to the current local scope object. - pub fn locals(&self) -> Ref> { + pub fn locals(&self) -> Ref>> { self.values.read() } /// Returns a gc cell of the current local scope object. - pub fn locals_cell(&self) -> GcCell<'gc, Object<'gc>> { + pub fn locals_cell(&self) -> ObjectCell<'gc> { self.values.to_owned() } /// Returns a reference to the current local scope object for mutation. - pub fn locals_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut> { + pub fn locals_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut>> { self.values.write(mc) } @@ -246,7 +246,7 @@ impl<'gc> Scope<'gc> { name: &str, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, + this: ObjectCell<'gc>, ) -> Result, Error> { if self.locals().has_property(name) { return self.locals().get(name, avm, context, this); @@ -286,7 +286,7 @@ impl<'gc> Scope<'gc> { value: Value<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, + this: ObjectCell<'gc>, ) -> Result>, Error> { if self.locals().has_property(name) && self.locals().is_property_overwritable(name) { self.locals_mut(context.gc_context) @@ -308,7 +308,8 @@ impl<'gc> Scope<'gc> { /// chain. As a result, this function always force sets a property on the /// local object and does not traverse the scope chain. pub fn define(&self, name: &str, value: impl Into>, mc: MutationContext<'gc, '_>) { - self.locals_mut(mc).force_set(name, value, EnumSet::empty()); + self.locals_mut(mc) + .define_value(name, value.into(), EnumSet::empty()); } /// Delete a value from scope diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs new file mode 100644 index 000000000..dd7691974 --- /dev/null +++ b/core/src/avm1/script_object.rs @@ -0,0 +1,707 @@ +use crate::avm1::function::{Executable, NativeFunction}; +use crate::avm1::property::{Attribute, Property}; +use crate::avm1::return_value::ReturnValue; +use crate::avm1::{Avm1, Error, Object, ObjectCell, UpdateContext, Value}; +use crate::display_object::DisplayNode; +use core::fmt; +use enumset::EnumSet; +use gc_arena::{GcCell, MutationContext}; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; + +pub const TYPE_OF_OBJECT: &str = "object"; +pub const TYPE_OF_FUNCTION: &str = "function"; +pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip"; + +#[derive(Clone)] +pub struct ScriptObject<'gc> { + prototype: Option>, + display_node: Option>, + values: HashMap>, + function: Option>, + type_of: &'static str, +} + +unsafe impl<'gc> gc_arena::Collect for ScriptObject<'gc> { + fn trace(&self, cc: gc_arena::CollectionContext) { + self.prototype.trace(cc); + self.display_node.trace(cc); + self.values.trace(cc); + self.function.trace(cc); + } +} + +impl fmt::Debug for ScriptObject<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Object") + .field("prototype", &self.prototype) + .field("display_node", &self.display_node) + .field("values", &self.values) + .field("function", &self.function.is_some()) + .finish() + } +} + +impl<'gc> ScriptObject<'gc> { + pub fn object( + _gc_context: MutationContext<'gc, '_>, + proto: Option>, + ) -> ScriptObject<'gc> { + ScriptObject { + prototype: proto, + type_of: TYPE_OF_OBJECT, + display_node: None, + values: HashMap::new(), + function: None, + } + } + + /// Constructs and allocates an empty but normal object in one go. + pub fn object_cell( + gc_context: MutationContext<'gc, '_>, + proto: Option>, + ) -> ObjectCell<'gc> { + GcCell::allocate( + gc_context, + Box::new(ScriptObject { + prototype: proto, + type_of: TYPE_OF_OBJECT, + display_node: None, + values: HashMap::new(), + function: None, + }), + ) + } + + /// Constructs an object with no values, not even builtins. + /// + /// Intended for constructing scope chains, since they exclusively use the + /// object values, but can't just have a hashmap because of `with` and + /// friends. + pub fn bare_object() -> Self { + ScriptObject { + prototype: None, + type_of: TYPE_OF_OBJECT, + display_node: None, + values: HashMap::new(), + function: None, + } + } + + /// Construct a function sans prototype. + pub fn bare_function( + function: impl Into>, + fn_proto: Option>, + ) -> Self { + ScriptObject { + prototype: fn_proto, + type_of: TYPE_OF_FUNCTION, + function: Some(function.into()), + display_node: None, + values: HashMap::new(), + } + } + + /// Construct a function from an executable and associated protos. + /// + /// Since prototypes need to link back to themselves, this function builds + /// both objects itself and returns the function to you, fully allocated. + /// + /// `fn_proto` refers to the implicit proto of the function object, and the + /// `prototype` refers to the explicit prototype of the function. If + /// provided, the function and it's prototype will be linked to each other. + pub fn function( + gc_context: MutationContext<'gc, '_>, + function: impl Into>, + fn_proto: Option>, + prototype: Option>, + ) -> ObjectCell<'gc> { + let function = GcCell::allocate( + gc_context, + Box::new(Self::bare_function(function, fn_proto)) as Box + 'gc>, + ); + + //TODO: Can we make these proper sets or no? + if let Some(p) = prototype { + p.write(gc_context).define_value( + "constructor", + Value::Object(function), + Attribute::DontEnum.into(), + ); + function + .write(gc_context) + .define_value("prototype", p.into(), EnumSet::empty()); + } + + function + } + + pub fn set_display_node(&mut self, display_node: DisplayNode<'gc>) { + self.display_node = Some(display_node); + } + + pub fn display_node(&self) -> Option> { + self.display_node + } + + /// Declare a native function on the current object. + /// + /// This is intended for use with defining host object prototypes. Notably, + /// this creates a function object without an explicit `prototype`, which + /// is only possible when defining host functions. User-defined functions + /// always get a fresh explicit prototype, so you should never force set a + /// user-defined function. + pub fn force_set_function( + &mut self, + name: &str, + function: NativeFunction<'gc>, + gc_context: MutationContext<'gc, '_>, + attributes: A, + fn_proto: Option>, + ) where + A: Into>, + { + self.define_value( + name, + Value::Object(ScriptObject::function(gc_context, function, fn_proto, None)), + attributes.into(), + ) + } + + pub fn set_prototype(&mut self, prototype: ObjectCell<'gc>) { + self.prototype = Some(prototype); + } + + pub fn set_type_of(&mut self, type_of: &'static str) { + self.type_of = type_of; + } +} + +impl<'gc> Object<'gc> for ScriptObject<'gc> { + /// Get the value of a particular property on this object. + /// + /// The `avm`, `context`, and `this` parameters exist so that this object + /// can call virtual properties. Furthermore, since some virtual properties + /// may resolve on the AVM stack, this function may return `None` instead + /// of a `Value`. *This is not equivalent to `undefined`.* Instead, it is a + /// signal that your value will be returned on the ActionScript stack, and + /// that you should register a stack continuation in order to get it. + fn get_local( + &self, + name: &str, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + ) -> Result, Error> { + if name == "__proto__" { + return Ok(self + .prototype + .map_or(Value::Undefined, Value::Object) + .into()); + } + + if let Some(value) = self.values.get(name) { + return value.get(avm, context, this); + } + + Ok(Value::Undefined.into()) + } + + /// Set a named property on the object. + /// + /// This function takes a redundant `this` parameter which should be + /// the object's own `GcCell`, so that it can pass it to user-defined + /// overrides that may need to interact with the underlying object. + fn set( + &mut self, + name: &str, + value: Value<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + ) -> Result<(), Error> { + if name == "__proto__" { + self.prototype = value.as_object().ok().to_owned(); + } else { + match self.values.entry(name.to_owned()) { + Entry::Occupied(mut entry) => { + entry.get_mut().set(avm, context, this, value)?; + } + Entry::Vacant(entry) => { + entry.insert(Property::Stored { + value, + attributes: Default::default(), + }); + } + } + } + + Ok(()) + } + + /// Call the underlying object. + /// + /// This function takes a redundant `this` parameter which should be + /// the object's own `GcCell`, so that it can pass it to user-defined + /// overrides that may need to interact with the underlying object. + fn call( + &self, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + args: &[Value<'gc>], + ) -> Result, Error> { + if let Some(function) = &self.function { + function.exec(avm, context, this, args) + } else { + Ok(Value::Undefined.into()) + } + } + + #[allow(clippy::new_ret_no_self)] + fn new( + &self, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: ObjectCell<'gc>, + _args: &[Value<'gc>], + ) -> Result, Error> { + Ok(GcCell::allocate( + context.gc_context, + Box::new(ScriptObject::object(context.gc_context, Some(this))) as Box>, + )) + } + + /// Delete a named property from the object. + /// + /// Returns false if the property cannot be deleted. + fn delete(&mut self, name: &str) -> bool { + if let Some(prop) = self.values.get(name) { + if prop.can_delete() { + self.values.remove(name); + return true; + } + } + + false + } + + fn add_property( + &mut self, + name: &str, + get: Executable<'gc>, + set: Option>, + attributes: EnumSet, + ) { + self.values.insert( + name.to_owned(), + Property::Virtual { + get, + set, + attributes, + }, + ); + } + + fn define_value(&mut self, name: &str, value: Value<'gc>, attributes: EnumSet) { + self.values + .insert(name.to_string(), Property::Stored { value, attributes }); + } + + fn proto(&self) -> Option> { + self.prototype + } + + /// Checks if the object has a given named property. + fn has_property(&self, name: &str) -> bool { + self.has_own_property(name) + || self + .prototype + .as_ref() + .map_or(false, |p| p.read().has_property(name)) + } + + /// Checks if the object has a given named property on itself (and not, + /// say, the object's prototype or superclass) + fn has_own_property(&self, name: &str) -> bool { + if name == "__proto__" { + return true; + } + self.values.contains_key(name) + } + + fn is_property_overwritable(&self, name: &str) -> bool { + self.values + .get(name) + .map(|p| p.is_overwritable()) + .unwrap_or(false) + } + + /// Checks if a named property appears when enumerating the object. + fn is_property_enumerable(&self, name: &str) -> bool { + if let Some(prop) = self.values.get(name) { + prop.is_enumerable() + } else { + false + } + } + + /// Enumerate the object. + fn get_keys(&self) -> HashSet { + let mut result = self + .prototype + .map_or_else(HashSet::new, |p| p.read().get_keys()); + + self.values + .iter() + .filter_map(|(k, p)| { + if p.is_enumerable() { + Some(k.to_string()) + } else { + None + } + }) + .for_each(|k| { + result.insert(k); + }); + + result + } + + fn as_string(&self) -> String { + if self.function.is_some() { + "[type Function]".to_string() + } else { + "[object Object]".to_string() + } + } + + fn type_of(&self) -> &'static str { + self.type_of + } + + fn as_script_object(&self) -> Option<&ScriptObject<'gc>> { + Some(self) + } + + fn as_script_object_mut(&mut self) -> Option<&mut ScriptObject<'gc>> { + Some(self) + } + + /// Get the underlying display node for this object, if it exists. + fn as_display_node(&self) -> Option> { + self.display_node + } + + /// Returns a copy of a given function. + /// + /// TODO: We have to clone here because of how executables are stored on + /// objects directly. This might not be a good idea for performance. + fn as_executable(&self) -> Option> { + self.function.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::avm1::activation::Activation; + use crate::avm1::property::Attribute::*; + use crate::backend::audio::NullAudioBackend; + use crate::backend::navigator::NullNavigatorBackend; + use crate::backend::render::NullRenderer; + use crate::display_object::{DisplayObject, MovieClip}; + use crate::library::Library; + use crate::prelude::*; + use gc_arena::rootless_arena; + use rand::{rngs::SmallRng, SeedableRng}; + use std::sync::Arc; + + fn with_object(swf_version: u8, test: F) -> R + where + F: for<'a, 'gc> FnOnce( + &mut Avm1<'gc>, + &mut UpdateContext<'a, 'gc, '_>, + ObjectCell<'gc>, + ) -> R, + { + rootless_arena(|gc_context| { + let mut avm = Avm1::new(gc_context, swf_version); + let movie_clip: Box = + Box::new(MovieClip::new(swf_version, gc_context)); + let root = GcCell::allocate(gc_context, movie_clip); + let mut context = UpdateContext { + gc_context, + global_time: 0, + player_version: 32, + swf_version, + root, + start_clip: root, + active_clip: root, + target_clip: Some(root), + target_path: Value::Undefined, + rng: &mut SmallRng::from_seed([0u8; 16]), + action_queue: &mut crate::context::ActionQueue::new(), + audio: &mut NullAudioBackend::new(), + background_color: &mut Color { + r: 0, + g: 0, + b: 0, + a: 0, + }, + library: &mut Library::new(), + navigator: &mut NullNavigatorBackend::new(), + renderer: &mut NullRenderer::new(), + swf_data: &mut Arc::new(vec![]), + system_prototypes: avm.prototypes().clone(), + }; + + let object = GcCell::allocate( + gc_context, + Box::new(ScriptObject::object( + gc_context, + Some(avm.prototypes().object), + )) as Box>, + ); + + let globals = avm.global_object_cell(); + avm.insert_stack_frame(GcCell::allocate( + gc_context, + Activation::from_nothing(swf_version, globals, gc_context), + )); + + test(&mut avm, &mut context, object) + }) + } + + #[test] + fn test_get_undefined() { + with_object(0, |avm, context, object| { + assert_eq!( + object + .read() + .get("not_defined", avm, context, object) + .unwrap(), + ReturnValue::Immediate(Value::Undefined) + ); + }) + } + + #[test] + fn test_set_get() { + with_object(0, |avm, context, object| { + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("forced", "forced".into(), EnumSet::empty()); + object + .write(context.gc_context) + .set("natural", "natural".into(), avm, context, object) + .unwrap(); + + assert_eq!( + object.read().get("forced", avm, context, object).unwrap(), + ReturnValue::Immediate("forced".into()) + ); + assert_eq!( + object.read().get("natural", avm, context, object).unwrap(), + ReturnValue::Immediate("natural".into()) + ); + }) + } + + #[test] + fn test_set_readonly() { + with_object(0, |avm, context, object| { + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("normal", "initial".into(), EnumSet::empty()); + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("readonly", "initial".into(), ReadOnly.into()); + + object + .write(context.gc_context) + .set("normal", "replaced".into(), avm, context, object) + .unwrap(); + object + .write(context.gc_context) + .set("readonly", "replaced".into(), avm, context, object) + .unwrap(); + + assert_eq!( + object.read().get("normal", avm, context, object).unwrap(), + ReturnValue::Immediate("replaced".into()) + ); + assert_eq!( + object.read().get("readonly", avm, context, object).unwrap(), + ReturnValue::Immediate("initial".into()) + ); + }) + } + + #[test] + fn test_deletable_not_readonly() { + with_object(0, |avm, context, object| { + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("test", "initial".into(), DontDelete.into()); + + assert_eq!(object.write(context.gc_context).delete("test"), false); + assert_eq!( + object.read().get("test", avm, context, object).unwrap(), + ReturnValue::Immediate("initial".into()) + ); + + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .set("test", "replaced".into(), avm, context, object) + .unwrap(); + + assert_eq!(object.write(context.gc_context).delete("test"), false); + assert_eq!( + object.read().get("test", avm, context, object).unwrap(), + ReturnValue::Immediate("replaced".into()) + ); + }) + } + + #[test] + fn test_virtual_get() { + with_object(0, |avm, context, object| { + let getter = Executable::Native(|_avm, _context, _this, _args| { + Ok(ReturnValue::Immediate("Virtual!".into())) + }); + + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .add_property("test", getter, None, EnumSet::empty()); + + assert_eq!( + object.read().get("test", avm, context, object).unwrap(), + ReturnValue::Immediate("Virtual!".into()) + ); + + // This set should do nothing + object + .write(context.gc_context) + .set("test", "Ignored!".into(), avm, context, object) + .unwrap(); + assert_eq!( + object.read().get("test", avm, context, object).unwrap(), + ReturnValue::Immediate("Virtual!".into()) + ); + }) + } + + #[test] + fn test_delete() { + with_object(0, |avm, context, object| { + let getter = Executable::Native(|_avm, _context, _this, _args| { + Ok(ReturnValue::Immediate("Virtual!".into())) + }); + + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .add_property("virtual", getter.clone(), None, EnumSet::empty()); + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .add_property("virtual_un", getter, None, DontDelete.into()); + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("stored", "Stored!".into(), EnumSet::empty()); + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("stored_un", "Stored!".into(), DontDelete.into()); + + assert_eq!(object.write(context.gc_context).delete("virtual"), true); + assert_eq!(object.write(context.gc_context).delete("virtual_un"), false); + assert_eq!(object.write(context.gc_context).delete("stored"), true); + assert_eq!(object.write(context.gc_context).delete("stored_un"), false); + assert_eq!( + object.write(context.gc_context).delete("non_existent"), + false + ); + + assert_eq!( + object.read().get("virtual", avm, context, object).unwrap(), + ReturnValue::Immediate(Value::Undefined) + ); + assert_eq!( + object + .read() + .get("virtual_un", avm, context, object) + .unwrap(), + ReturnValue::Immediate("Virtual!".into()) + ); + assert_eq!( + object.read().get("stored", avm, context, object).unwrap(), + ReturnValue::Immediate(Value::Undefined) + ); + assert_eq!( + object + .read() + .get("stored_un", avm, context, object) + .unwrap(), + ReturnValue::Immediate("Stored!".into()) + ); + }) + } + + #[test] + fn test_iter_values() { + with_object(0, |_avm, context, object| { + let getter = Executable::Native(|_avm, _context, _this, _args| { + Ok(ReturnValue::Immediate(Value::Null)) + }); + + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("stored", Value::Null, EnumSet::empty()); + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .define_value("stored_hidden", Value::Null, DontEnum.into()); + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .add_property("virtual", getter.clone(), None, EnumSet::empty()); + object + .write(context.gc_context) + .as_script_object_mut() + .unwrap() + .add_property("virtual_hidden", getter, None, DontEnum.into()); + + let keys = object.read().get_keys(); + assert_eq!(keys.len(), 2); + assert_eq!(keys.contains(&"stored".to_string()), true); + assert_eq!(keys.contains(&"stored_hidden".to_string()), false); + assert_eq!(keys.contains(&"virtual".to_string()), true); + assert_eq!(keys.contains(&"virtual_hidden".to_string()), false); + }) + } +} diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index a702aee59..6af3540ea 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -1,5 +1,5 @@ use crate::avm1::activation::Activation; -use crate::avm1::{Avm1, Object, UpdateContext, Value}; +use crate::avm1::{Avm1, ObjectCell, UpdateContext, Value}; use crate::backend::audio::NullAudioBackend; use crate::backend::navigator::NullNavigatorBackend; use crate::backend::render::NullRenderer; @@ -7,19 +7,18 @@ use crate::context::ActionQueue; use crate::display_object::{DisplayObject, MovieClip}; use crate::library::Library; use crate::prelude::*; -use gc_arena::{rootless_arena, GcCell}; +use gc_arena::{rootless_arena, GcCell, MutationContext}; use rand::{rngs::SmallRng, SeedableRng}; use std::sync::Arc; pub fn with_avm(swf_version: u8, test: F) -> R where - F: for<'a, 'gc> FnOnce( - &mut Avm1<'gc>, - &mut UpdateContext<'a, 'gc, '_>, - GcCell<'gc, Object<'gc>>, - ) -> R, + F: for<'a, 'gc> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, ObjectCell<'gc>) -> R, { - rootless_arena(|gc_context| { + fn in_the_arena<'gc, F, R>(swf_version: u8, test: F, gc_context: MutationContext<'gc, '_>) -> R + where + F: for<'a> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, ObjectCell<'gc>) -> R, + { let mut avm = Avm1::new(gc_context, swf_version); let movie_clip: Box = Box::new(MovieClip::new(swf_version, gc_context)); let root = GcCell::allocate(gc_context, movie_clip); @@ -46,6 +45,7 @@ where navigator: &mut NullNavigatorBackend::new(), renderer: &mut NullRenderer::new(), swf_data: &mut Arc::new(vec![]), + system_prototypes: avm.prototypes().clone(), }; let globals = avm.global_object_cell(); @@ -57,5 +57,7 @@ where let this = root.read().object().as_object().unwrap().to_owned(); test(&mut avm, &mut context, this) - }) + } + + rootless_arena(|gc_context| in_the_arena(swf_version, test, gc_context)) } diff --git a/core/src/avm1/tests.rs b/core/src/avm1/tests.rs index 1dcb60e96..725a1a4eb 100644 --- a/core/src/avm1/tests.rs +++ b/core/src/avm1/tests.rs @@ -11,11 +11,11 @@ fn locals_into_form_values() { my_locals .write(context.gc_context) - .set("value1", "string", avm, context, my_locals) + .set("value1", "string".into(), avm, context, my_locals) .unwrap(); my_locals .write(context.gc_context) - .set("value2", 2.0, avm, context, my_locals) + .set("value2", 2.0.into(), avm, context, my_locals) .unwrap(); avm.insert_stack_frame(GcCell::allocate(context.gc_context, my_activation)); diff --git a/core/src/avm1/value.rs b/core/src/avm1/value.rs index 00a4a8949..9602ca2d9 100644 --- a/core/src/avm1/value.rs +++ b/core/src/avm1/value.rs @@ -1,7 +1,5 @@ -use crate::avm1::object::Object; use crate::avm1::return_value::ReturnValue; -use crate::avm1::{Avm1, Error, UpdateContext}; -use gc_arena::GcCell; +use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext}; #[derive(Clone, Debug)] #[allow(dead_code)] @@ -11,7 +9,7 @@ pub enum Value<'gc> { Bool(bool), Number(f64), String(String), - Object(GcCell<'gc, Object<'gc>>), + Object(ObjectCell<'gc>), } impl<'gc> From for Value<'gc> { @@ -32,8 +30,8 @@ impl<'gc> From for Value<'gc> { } } -impl<'gc> From>> for Value<'gc> { - fn from(object: GcCell<'gc, Object<'gc>>) -> Self { +impl<'gc> From> for Value<'gc> { + fn from(object: ObjectCell<'gc>) -> Self { Value::Object(object) } } @@ -68,6 +66,12 @@ impl<'gc> From for Value<'gc> { } } +impl<'gc> From for Value<'gc> { + fn from(value: usize) -> Self { + Value::Number(value as f64) + } +} + unsafe impl<'gc> gc_arena::Collect for Value<'gc> { fn trace(&self, cc: gc_arena::CollectionContext) { if let Value::Object(object) = self { @@ -230,6 +234,10 @@ impl<'gc> Value<'gc> { self.as_f64().map(|n| n as i64) } + pub fn as_usize(&self) -> Result { + self.as_f64().map(|n| n as usize) + } + pub fn as_f64(&self) -> Result { match *self { Value::Number(v) => Ok(v), @@ -244,7 +252,7 @@ impl<'gc> Value<'gc> { } } - pub fn as_object(&self) -> Result>, Error> { + pub fn as_object(&self) -> Result, Error> { if let Value::Object(object) = self { Ok(object.to_owned()) } else { @@ -256,7 +264,7 @@ impl<'gc> Value<'gc> { &self, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, - this: GcCell<'gc, Object<'gc>>, + this: ObjectCell<'gc>, args: &[Value<'gc>], ) -> Result, Error> { if let Value::Object(object) = self { diff --git a/core/src/context.rs b/core/src/context.rs index bcaf9d612..76e122c12 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -81,6 +81,10 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// request, but this requires us to implement auto-generated /// _names ("instanceN" etc. for unnamed clips). pub target_path: avm1::Value<'gc>, + + /// The current set of system-specified prototypes to use when constructing + /// new built-in objects. + pub system_prototypes: avm1::SystemPrototypes<'gc>, } /// A queued ActionScript call. diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 488b5325b..6d623906b 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -1,4 +1,4 @@ -use crate::avm1::Value; +use crate::avm1::{ObjectCell, Value}; use crate::context::{RenderContext, UpdateContext}; use crate::player::NEWEST_PLAYER_VERSION; use crate::prelude::*; @@ -259,6 +259,7 @@ pub trait DisplayObject<'gc>: 'gc + Collect + Debug { &mut self, _gc_context: MutationContext<'gc, '_>, _display_object: DisplayNode<'gc>, + _proto: ObjectCell<'gc>, ) { } diff --git a/core/src/display_object/button.rs b/core/src/display_object/button.rs index b77b6df51..e2c9652ad 100644 --- a/core/src/display_object/button.rs +++ b/core/src/display_object/button.rs @@ -73,10 +73,11 @@ impl<'gc> Button<'gc> { self.children.clear(); for record in &self.static_data.records { if record.states.contains(&swf_state) { - if let Ok(child) = context - .library - .instantiate_display_object(record.id, context.gc_context) - { + if let Ok(child) = context.library.instantiate_display_object( + record.id, + context.gc_context, + &context.system_prototypes, + ) { child .write(context.gc_context) .set_parent(Some(context.active_clip)); @@ -161,10 +162,11 @@ impl<'gc> DisplayObject<'gc> for Button<'gc> { for record in &self.static_data.records { if record.states.contains(&swf::ButtonState::HitTest) { - match context - .library - .instantiate_display_object(record.id, context.gc_context) - { + match context.library.instantiate_display_object( + record.id, + context.gc_context, + &context.system_prototypes, + ) { Ok(child) => { { let mut child = child.write(context.gc_context); diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 58f3d8934..6c6ec18bb 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -1,7 +1,6 @@ //! `MovieClip` display object and support code. -use crate::avm1::movie_clip::create_movie_object; -use crate::avm1::object::{Object, TYPE_OF_MOVIE_CLIP}; -use crate::avm1::Value; +use crate::avm1::script_object::TYPE_OF_MOVIE_CLIP; +use crate::avm1::{ObjectCell, ScriptObject, Value}; use crate::backend::audio::AudioStreamHandle; use crate::character::Character; use crate::context::{RenderContext, UpdateContext}; @@ -34,7 +33,7 @@ pub struct MovieClip<'gc> { current_frame: FrameNumber, audio_stream: Option, children: BTreeMap>, - object: GcCell<'gc, Object<'gc>>, + object: ObjectCell<'gc>, } impl<'gc> MovieClip<'gc> { @@ -48,7 +47,7 @@ impl<'gc> MovieClip<'gc> { current_frame: 0, audio_stream: None, children: BTreeMap::new(), - object: GcCell::allocate(gc_context, create_movie_object(gc_context)), + object: GcCell::allocate(gc_context, Box::new(ScriptObject::bare_object())), } } @@ -79,7 +78,7 @@ impl<'gc> MovieClip<'gc> { current_frame: 0, audio_stream: None, children: BTreeMap::new(), - object: GcCell::allocate(gc_context, create_movie_object(gc_context)), + object: GcCell::allocate(gc_context, Box::new(ScriptObject::bare_object())), } } @@ -303,10 +302,11 @@ impl<'gc> MovieClip<'gc> { depth: Depth, copy_previous_properties: bool, ) -> Option> { - if let Ok(child_cell) = context - .library - .instantiate_display_object(id, context.gc_context) - { + if let Ok(child_cell) = context.library.instantiate_display_object( + id, + context.gc_context, + &context.system_prototypes, + ) { // Remove previous child from children list, // and add new childonto front of the list. let prev_child = self.children.insert(depth, child_cell); @@ -626,10 +626,18 @@ impl<'gc> DisplayObject<'gc> for MovieClip<'gc> { &mut self, gc_context: MutationContext<'gc, '_>, display_object: DisplayNode<'gc>, + proto: ObjectCell<'gc>, ) { let mut object = self.object.write(gc_context); - object.set_display_node(display_object); - object.set_type_of(TYPE_OF_MOVIE_CLIP); + object + .as_script_object_mut() + .unwrap() + .set_display_node(display_object); + object + .as_script_object_mut() + .unwrap() + .set_type_of(TYPE_OF_MOVIE_CLIP); + object.as_script_object_mut().unwrap().set_prototype(proto); } fn object(&self) -> Value<'gc> { diff --git a/core/src/library.rs b/core/src/library.rs index 8af150c8b..af34ba94d 100644 --- a/core/src/library.rs +++ b/core/src/library.rs @@ -1,3 +1,5 @@ +use crate::avm1::globals::SystemPrototypes; +use crate::avm1::ObjectCell; use crate::backend::audio::SoundHandle; use crate::character::Character; use crate::display_object::DisplayObject; @@ -47,15 +49,19 @@ impl<'gc> Library<'gc> { &self, id: CharacterId, gc_context: MutationContext<'gc, '_>, + prototypes: &SystemPrototypes<'gc>, ) -> Result, Box> { - let obj: Box> = match self.characters.get(&id) { - Some(Character::Bitmap(bitmap)) => bitmap.clone(), - Some(Character::EditText(edit_text)) => edit_text.clone(), - Some(Character::Graphic(graphic)) => graphic.clone(), - Some(Character::MorphShape(morph_shape)) => morph_shape.clone(), - Some(Character::MovieClip(movie_clip)) => movie_clip.clone(), - Some(Character::Button(button)) => button.clone(), - Some(Character::Text(text)) => text.clone(), + let (obj, proto): (Box>, ObjectCell<'gc>) = match self + .characters + .get(&id) + { + Some(Character::Bitmap(bitmap)) => (bitmap.clone(), prototypes.object), + Some(Character::EditText(edit_text)) => (edit_text.clone(), prototypes.object), + Some(Character::Graphic(graphic)) => (graphic.clone(), prototypes.object), + Some(Character::MorphShape(morph_shape)) => (morph_shape.clone(), prototypes.object), + Some(Character::MovieClip(movie_clip)) => (movie_clip.clone(), prototypes.movie_clip), + Some(Character::Button(button)) => (button.clone(), prototypes.object), + Some(Character::Text(text)) => (text.clone(), prototypes.object), Some(_) => return Err("Not a DisplayObject".into()), None => { log::error!("Tried to instantiate non-registered character ID {}", id); @@ -65,7 +71,7 @@ impl<'gc> Library<'gc> { let result = GcCell::allocate(gc_context, obj); result .write(gc_context) - .post_instantiation(gc_context, result); + .post_instantiation(gc_context, result, proto); Ok(result) } diff --git a/core/src/player.rs b/core/src/player.rs index 95332db72..90c0ae35f 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1,4 +1,4 @@ -use crate::avm1::{self, Avm1}; +use crate::avm1::{Avm1, Value}; use crate::backend::{ audio::AudioBackend, navigator::NavigatorBackend, render::Letterbox, render::RenderBackend, }; @@ -188,10 +188,11 @@ impl }; player.gc_arena.mutate(|gc_context, gc_root| { - gc_root - .root - .write(gc_context) - .post_instantiation(gc_context, gc_root.root) + gc_root.root.write(gc_context).post_instantiation( + gc_context, + gc_root.root, + gc_root.avm.read().prototypes().movie_clip, + ) }); player.build_matrices(); @@ -336,7 +337,8 @@ impl start_clip: gc_root.root, target_clip: Some(gc_root.root), root: gc_root.root, - target_path: avm1::Value::Undefined, + target_path: Value::Undefined, + system_prototypes: gc_root.avm.read().prototypes().clone(), }; if let Some(node) = &*gc_root.mouse_hover_node.read() { @@ -414,7 +416,8 @@ impl start_clip: gc_root.root, target_clip: Some(gc_root.root), root: gc_root.root, - target_path: avm1::Value::Undefined, + target_path: Value::Undefined, + system_prototypes: gc_root.avm.read().prototypes().clone(), }; // RollOut of previous node. @@ -484,7 +487,8 @@ impl start_clip: gc_root.root, target_clip: Some(gc_root.root), root: gc_root.root, - target_path: avm1::Value::Undefined, + target_path: Value::Undefined, + system_prototypes: gc_root.avm.read().prototypes().clone(), }; let mut morph_shapes = fnv::FnvHashMap::default(); @@ -547,7 +551,8 @@ impl start_clip: gc_root.root, target_clip: Some(gc_root.root), root: gc_root.root, - target_path: avm1::Value::Undefined, + target_path: Value::Undefined, + system_prototypes: gc_root.avm.read().prototypes().clone(), }; gc_root diff --git a/core/src/tag_utils.rs b/core/src/tag_utils.rs index bbb706da8..4b68fd253 100644 --- a/core/src/tag_utils.rs +++ b/core/src/tag_utils.rs @@ -1,3 +1,4 @@ +use gc_arena::Collect; use std::sync::Arc; use swf::TagCode; @@ -5,7 +6,8 @@ pub type DecodeResult = Result<(), Box>; pub type SwfStream = swf::read::Reader>; /// A shared-ownership reference to some portion of an immutable datastream. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Collect)] +#[collect(no_drop)] pub struct SwfSlice { pub data: Arc>, pub start: usize, diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 26d6c050d..c7bb0164e 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -60,14 +60,49 @@ swf_tests! { (timeline_function_def, "avm1/timeline_function_def", 3), (root_global_parent, "avm1/root_global_parent", 3), (register_underflow, "avm1/register_underflow", 1), + (object_prototypes, "avm1/object_prototypes", 1), + (movieclip_prototype_extension, "avm1/movieclip_prototype_extension", 1), + (recursive_prototypes, "avm1/recursive_prototypes", 1), + (has_own_property, "avm1/has_own_property", 1), + #[ignore] (extends_chain, "avm1/extends_chain", 1), + (is_prototype_of, "avm1/is_prototype_of", 1), +} + +#[test] +fn test_prototype_enumerate() -> Result<(), Error> { + let trace_log = run_swf("tests/swfs/avm1/prototype_enumerate/test.swf", 1)?; + let mut actual: Vec = trace_log.lines().map(|s| s.to_string()).collect(); + let mut expected = vec!["a", "b", "c", "d", "e"]; + + actual.sort(); + expected.sort(); + + assert_eq!(actual, expected); + Ok(()) } /// Loads an SWF and runs it through the Ruffle core for a number of frames. /// Tests that the trace output matches the given expected output. fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Result<(), Error> { - let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info)); let expected_output = std::fs::read_to_string(expected_output_path)?.replace("\r\n", "\n"); + let trace_log = run_swf(swf_path, num_frames)?; + if trace_log != expected_output { + println!( + "Ruffle output:\n{}\nExpected output:\n{}", + trace_log, expected_output + ); + panic!("Ruffle output did not match expected output."); + } + + Ok(()) +} + +/// Loads an SWF and runs it through the Ruffle core for a number of frames. +/// Tests that the trace output matches the given expected output. +fn run_swf(swf_path: &str, num_frames: u32) -> Result { + let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info)); + let swf_data = std::fs::read(swf_path)?; let mut player = Player::new( NullRenderer, @@ -80,16 +115,7 @@ fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Resu player.run_frame(); } - let trace_log = trace_log(); - if trace_log != expected_output { - println!( - "Ruffle output:\n{}\nExpected output:\n{}", - trace_log, expected_output - ); - panic!("Ruffle output did not match expected output."); - } - - Ok(()) + Ok(trace_log()) } thread_local! { diff --git a/core/tests/swfs/avm1/extends_chain/Blue.as b/core/tests/swfs/avm1/extends_chain/Blue.as new file mode 100644 index 000000000..e9d814be2 --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/Blue.as @@ -0,0 +1,3 @@ +interface Blue { + +} diff --git a/core/tests/swfs/avm1/extends_chain/ChildA.as b/core/tests/swfs/avm1/extends_chain/ChildA.as new file mode 100644 index 000000000..a91ea3477 --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/ChildA.as @@ -0,0 +1,11 @@ +class ChildA extends Super { + function ChildA() { + super(); + trace("ChildA constructor"); + } + + function work() { + super.work(); + trace("ChildA work"); + } +} \ No newline at end of file diff --git a/core/tests/swfs/avm1/extends_chain/ChildB.as b/core/tests/swfs/avm1/extends_chain/ChildB.as new file mode 100644 index 000000000..f8236a2b7 --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/ChildB.as @@ -0,0 +1,11 @@ +class ChildB extends Super implements Blue { + function ChildB() { + super(); + trace("ChildA constructor"); + } + + function work() { + super.work(); + trace("ChildA work"); + } +} diff --git a/core/tests/swfs/avm1/extends_chain/GrandchildBA.as b/core/tests/swfs/avm1/extends_chain/GrandchildBA.as new file mode 100644 index 000000000..228dd2dcb --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/GrandchildBA.as @@ -0,0 +1,11 @@ +class GrandchildBA extends ChildB { + function GrandchildBA() { + super(); + trace("GrandchildBA constructor"); + } + + function work() { + super.work(); + trace("GrandchildBA work"); + } +} diff --git a/core/tests/swfs/avm1/extends_chain/GrandchildBB.as b/core/tests/swfs/avm1/extends_chain/GrandchildBB.as new file mode 100644 index 000000000..1ef172a88 --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/GrandchildBB.as @@ -0,0 +1,11 @@ +class GrandchildBB extends ChildB implements Pink { + function GrandchildBB() { + super(); + trace("GrandchildBB constructor"); + } + + function work() { + super.work(); + trace("GrandchildBB work"); + } +} diff --git a/core/tests/swfs/avm1/extends_chain/Pink.as b/core/tests/swfs/avm1/extends_chain/Pink.as new file mode 100644 index 000000000..9ed847acc --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/Pink.as @@ -0,0 +1,3 @@ +interface Pink extends Red { + +} diff --git a/core/tests/swfs/avm1/extends_chain/Red.as b/core/tests/swfs/avm1/extends_chain/Red.as new file mode 100644 index 000000000..d23bea1ac --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/Red.as @@ -0,0 +1,3 @@ +interface Red { + +} diff --git a/core/tests/swfs/avm1/extends_chain/Super.as b/core/tests/swfs/avm1/extends_chain/Super.as new file mode 100644 index 000000000..ca2c78ea2 --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/Super.as @@ -0,0 +1,9 @@ +class Super { + function Super() { + trace("Super constructor"); + } + + function work() { + trace("Super work"); + } +} \ No newline at end of file diff --git a/core/tests/swfs/avm1/extends_chain/output.txt b/core/tests/swfs/avm1/extends_chain/output.txt new file mode 100644 index 000000000..f7821a22f --- /dev/null +++ b/core/tests/swfs/avm1/extends_chain/output.txt @@ -0,0 +1,134 @@ +// ChildA constructor +Super constructor +ChildA constructor + +// ChildA work +Super work +ChildA work + +// ChildA instanceof Super +true + +// ChildA instanceof ChildA +true + +// ChildA instanceof ChildB +false + +// ChildA instanceof GrandchildBA +false + +// ChildA instanceof GrandchildBB +false + +// ChildA instanceof Red +false + +// ChildA instanceof Blue +false + +// ChildA instanceof Pink +false + + +// ChildB constructor +Super constructor +ChildA constructor + +// ChildB work +Super work +ChildA work + +// ChildB instanceof Super +true + +// ChildB instanceof ChildA +false + +// ChildB instanceof ChildB +true + +// ChildB instanceof GrandchildBA +false + +// ChildB instanceof GrandchildBB +false + +// ChildB instanceof Red +false + +// ChildB instanceof Blue +true + +// ChildB instanceof Pink +false + + +// GrandchildBA constructor +Super constructor +ChildA constructor +GrandchildBA constructor + +// GrandchildBA work +Super work +ChildA work +GrandchildBA work + +// GrandchildBA instanceof Super +true + +// GrandchildBA instanceof ChildA +false + +// GrandchildBA instanceof ChildB +true + +// GrandchildBA instanceof GrandchildBA +true + +// GrandchildBA instanceof GrandchildBB +false + +// GrandchildBA instanceof Red +false + +// GrandchildBA instanceof Blue +true + +// GrandchildBA instanceof Pink +false + + +// GrandchildBB constructor +Super constructor +ChildA constructor +GrandchildBB constructor + +// GrandchildBB work +Super work +ChildA work +GrandchildBB work + +// GrandchildBB instanceof Super +true + +// GrandchildBB instanceof ChildA +false + +// GrandchildBB instanceof ChildB +true + +// GrandchildBB instanceof GrandchildBA +false + +// GrandchildBB instanceof GrandchildBB +true + +// GrandchildBB instanceof Red +true + +// GrandchildBB instanceof Blue +true + +// GrandchildBB instanceof Pink +true diff --git a/core/tests/swfs/avm1/extends_chain/test.fla b/core/tests/swfs/avm1/extends_chain/test.fla new file mode 100644 index 000000000..920c0f92a Binary files /dev/null and b/core/tests/swfs/avm1/extends_chain/test.fla differ diff --git a/core/tests/swfs/avm1/extends_chain/test.swf b/core/tests/swfs/avm1/extends_chain/test.swf new file mode 100644 index 000000000..4534ac4db Binary files /dev/null and b/core/tests/swfs/avm1/extends_chain/test.swf differ diff --git a/core/tests/swfs/avm1/has_own_property/output.txt b/core/tests/swfs/avm1/has_own_property/output.txt new file mode 100644 index 000000000..f8cde2dd9 --- /dev/null +++ b/core/tests/swfs/avm1/has_own_property/output.txt @@ -0,0 +1,18 @@ +// base.hasOwnProperty("name") +true + +// child.hasOwnProperty("name") +false + +// base.hasOwnProperty("__proto__") +true + +// child.hasOwnProperty("__proto__") +true + +// base.hasOwnProperty("prototype") +false + +// child.hasOwnProperty("prototype") +false + diff --git a/core/tests/swfs/avm1/has_own_property/test.fla b/core/tests/swfs/avm1/has_own_property/test.fla new file mode 100644 index 000000000..25182211d Binary files /dev/null and b/core/tests/swfs/avm1/has_own_property/test.fla differ diff --git a/core/tests/swfs/avm1/has_own_property/test.swf b/core/tests/swfs/avm1/has_own_property/test.swf new file mode 100644 index 000000000..64dc2961a Binary files /dev/null and b/core/tests/swfs/avm1/has_own_property/test.swf differ diff --git a/core/tests/swfs/avm1/is_prototype_of/output.txt b/core/tests/swfs/avm1/is_prototype_of/output.txt new file mode 100644 index 000000000..383062d62 --- /dev/null +++ b/core/tests/swfs/avm1/is_prototype_of/output.txt @@ -0,0 +1,90 @@ +// a.isPrototypeOf(a) +false + +// a.isPrototypeOf(b) +true + +// a.isPrototypeOf(c) +false + +// a.isPrototypeOf(d) +false + +// a.isPrototypeOf(Fun) +false + +// b.isPrototypeOf(a) +false + +// b.isPrototypeOf(b) +false + +// b.isPrototypeOf(c) +false + +// b.isPrototypeOf(d) +false + +// b.isPrototypeOf(Fun) +false + +// c.isPrototypeOf(a) +false + +// c.isPrototypeOf(b) +false + +// c.isPrototypeOf(c) +false + +// c.isPrototypeOf(d) +true + +// c.isPrototypeOf(Fun) +false + +// d.isPrototypeOf(a) +false + +// d.isPrototypeOf(b) +false + +// d.isPrototypeOf(c) +false + +// d.isPrototypeOf(d) +false + +// d.isPrototypeOf(Fun) +false + +// Fun.prototype.isPrototypeOf(a) +false + +// Fun.prototype.isPrototypeOf(b) +false + +// Fun.prototype.isPrototypeOf(c) +true + +// Fun.prototype.isPrototypeOf(d) +true + +// Fun.prototype.isPrototypeOf(Fun) +false + +// Object.prototype.isPrototypeOf(a) +true + +// Object.prototype.isPrototypeOf(b) +true + +// Object.prototype.isPrototypeOf(c) +true + +// Object.prototype.isPrototypeOf(d) +true + +// Object.prototype.isPrototypeOf(Fun) +true + diff --git a/core/tests/swfs/avm1/is_prototype_of/test.fla b/core/tests/swfs/avm1/is_prototype_of/test.fla new file mode 100644 index 000000000..30fbdf0ac Binary files /dev/null and b/core/tests/swfs/avm1/is_prototype_of/test.fla differ diff --git a/core/tests/swfs/avm1/is_prototype_of/test.swf b/core/tests/swfs/avm1/is_prototype_of/test.swf new file mode 100644 index 000000000..a790c5bc2 Binary files /dev/null and b/core/tests/swfs/avm1/is_prototype_of/test.swf differ diff --git a/core/tests/swfs/avm1/movieclip_prototype_extension/output.txt b/core/tests/swfs/avm1/movieclip_prototype_extension/output.txt new file mode 100644 index 000000000..906fef7ea --- /dev/null +++ b/core/tests/swfs/avm1/movieclip_prototype_extension/output.txt @@ -0,0 +1,6 @@ +// this.three +undefined + +// this.three +3 + diff --git a/core/tests/swfs/avm1/movieclip_prototype_extension/test.fla b/core/tests/swfs/avm1/movieclip_prototype_extension/test.fla new file mode 100644 index 000000000..c90db3569 Binary files /dev/null and b/core/tests/swfs/avm1/movieclip_prototype_extension/test.fla differ diff --git a/core/tests/swfs/avm1/movieclip_prototype_extension/test.swf b/core/tests/swfs/avm1/movieclip_prototype_extension/test.swf new file mode 100644 index 000000000..4db645798 Binary files /dev/null and b/core/tests/swfs/avm1/movieclip_prototype_extension/test.swf differ diff --git a/core/tests/swfs/avm1/object_prototypes/output.txt b/core/tests/swfs/avm1/object_prototypes/output.txt new file mode 100644 index 000000000..c4898fe6a --- /dev/null +++ b/core/tests/swfs/avm1/object_prototypes/output.txt @@ -0,0 +1,23 @@ +Base constructed! +Base constructed! +// a.name +foo + +// b.name +foo + +// c.name +foo + + +// a.name = "bar" + +// a.name +bar + +// b.name +bar + +// c.name +foo + diff --git a/core/tests/swfs/avm1/object_prototypes/test.fla b/core/tests/swfs/avm1/object_prototypes/test.fla new file mode 100644 index 000000000..ec74aa669 Binary files /dev/null and b/core/tests/swfs/avm1/object_prototypes/test.fla differ diff --git a/core/tests/swfs/avm1/object_prototypes/test.swf b/core/tests/swfs/avm1/object_prototypes/test.swf new file mode 100644 index 000000000..aeb135912 Binary files /dev/null and b/core/tests/swfs/avm1/object_prototypes/test.swf differ diff --git a/core/tests/swfs/avm1/prototype_enumerate/test.fla b/core/tests/swfs/avm1/prototype_enumerate/test.fla new file mode 100644 index 000000000..087042549 Binary files /dev/null and b/core/tests/swfs/avm1/prototype_enumerate/test.fla differ diff --git a/core/tests/swfs/avm1/prototype_enumerate/test.swf b/core/tests/swfs/avm1/prototype_enumerate/test.swf new file mode 100644 index 000000000..5ff91daf6 Binary files /dev/null and b/core/tests/swfs/avm1/prototype_enumerate/test.swf differ diff --git a/core/tests/swfs/avm1/recursive_prototypes/output.txt b/core/tests/swfs/avm1/recursive_prototypes/output.txt new file mode 100644 index 000000000..e69de29bb diff --git a/core/tests/swfs/avm1/recursive_prototypes/test.fla b/core/tests/swfs/avm1/recursive_prototypes/test.fla new file mode 100644 index 000000000..904edaf4b Binary files /dev/null and b/core/tests/swfs/avm1/recursive_prototypes/test.fla differ diff --git a/core/tests/swfs/avm1/recursive_prototypes/test.swf b/core/tests/swfs/avm1/recursive_prototypes/test.swf new file mode 100644 index 000000000..46c961122 Binary files /dev/null and b/core/tests/swfs/avm1/recursive_prototypes/test.swf differ