diff --git a/core/src/avm1/string.rs b/core/src/avm1/string.rs index 8b2e27de2141..391e0069e1c2 100644 --- a/core/src/avm1/string.rs +++ b/core/src/avm1/string.rs @@ -1,15 +1,17 @@ use gc_arena::{Collect, Gc, MutationContext}; +use std::cmp::{Eq, Ord, Ordering, PartialOrd}; use std::fmt; +use std::hash::{Hash, Hasher}; use std::ops::Deref; -#[derive(Debug, Clone, Collect)] +#[derive(Debug, Clone, Copy, Collect)] #[collect(no_drop)] enum Source<'gc> { Owned(Gc<'gc, String>), Static(&'static str), } -#[derive(Debug, Clone, Collect)] +#[derive(Debug, Clone, Copy, Collect)] #[collect(no_drop)] pub struct AvmString<'gc> { source: Source<'gc>, @@ -78,6 +80,29 @@ impl<'gc> PartialEq> for AvmString<'gc> { } } +impl<'gc> Eq for AvmString<'gc> {} + +impl<'gc> PartialOrd> for AvmString<'gc> { + fn partial_cmp(&self, other: &AvmString<'gc>) -> Option { + self.as_ref().partial_cmp(other.as_ref()) + } +} + +impl<'gc> Ord for AvmString<'gc> { + fn cmp(&self, other: &AvmString<'gc>) -> Ordering { + self.as_ref().cmp(other.as_ref()) + } +} + +impl<'gc> Hash for AvmString<'gc> { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.as_ref().hash(state) + } +} + macro_rules! impl_eq { ($lhs:ty, $rhs: ty) => { #[allow(unused_lifetimes)] diff --git a/core/src/avm2.rs b/core/src/avm2.rs new file mode 100644 index 000000000000..31d797234679 --- /dev/null +++ b/core/src/avm2.rs @@ -0,0 +1,164 @@ +//! ActionScript Virtual Machine 2 (AS3) support + +use crate::avm2::activation::Activation; +use crate::avm2::globals::SystemPrototypes; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::scope::Scope; +use crate::avm2::script::Script; +use crate::avm2::script::TranslationUnit; +use crate::avm2::value::Value; +use crate::context::UpdateContext; +use crate::tag_utils::SwfSlice; +use gc_arena::{Collect, GcCell, MutationContext}; +use std::rc::Rc; +use swf::avm2::read::Reader; + +#[macro_export] +macro_rules! avm_debug { + ($($arg:tt)*) => ( + #[cfg(feature = "avm_debug")] + log::debug!($($arg)*) + ) +} + +mod activation; +mod class; +mod function; +mod globals; +mod method; +mod names; +mod object; +mod property; +mod property_map; +mod return_value; +mod scope; +mod script; +mod script_object; +mod slot; +mod string; +mod r#trait; +mod value; + +/// Boxed error alias. +/// +/// As AVM2 is a far stricter VM than AVM1, this may eventually be replaced +/// with a proper Avm2Error enum. +type Error = Box; + +/// The state of an AVM2 interpreter. +#[derive(Collect)] +#[collect(no_drop)] +pub struct Avm2<'gc> { + /// Values currently present on the operand stack. + stack: Vec>, + + /// Global scope object. + globals: Object<'gc>, + + /// System prototypes. + system_prototypes: SystemPrototypes<'gc>, +} + +impl<'gc> Avm2<'gc> { + /// Construct a new AVM interpreter. + pub fn new(mc: MutationContext<'gc, '_>) -> Self { + let (globals, system_prototypes) = globals::construct_global_scope(mc); + + Self { + stack: Vec::new(), + globals, + system_prototypes, + } + } + + /// Return the current set of system prototypes. + pub fn prototypes(&self) -> &SystemPrototypes<'gc> { + &self.system_prototypes + } + + /// Run a script's initializer method. + pub fn run_script_initializer( + &mut self, + script: GcCell<'gc, Script<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + let mut init_activation = Activation::from_script(self, context, script, self.globals)?; + + init_activation.run_stack_frame_for_script(context, script) + } + + /// Load an ABC file embedded in a `SwfSlice`. + /// + /// The `SwfSlice` must resolve to the contents of an ABC file. + pub fn load_abc( + &mut self, + abc: SwfSlice, + _abc_name: &str, + _lazy_init: bool, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + let mut read = Reader::new(abc.as_ref()); + + let abc_file = Rc::new(read.read()?); + let tunit = TranslationUnit::from_abc(abc_file.clone(), context.gc_context); + + for i in (0..abc_file.scripts.len()).rev() { + let script = tunit.load_script(i as u32, context.gc_context)?; + let mut globals = self.globals(); + let scope = Scope::push_scope(None, globals, context.gc_context); + let mut null_activation = Activation::from_nothing(self, context); + + // TODO: Lazyinit means we shouldn't do this until traits are + // actually mentioned... + for trait_entry in script.read().traits()?.iter() { + globals.install_foreign_trait( + &mut null_activation, + context, + trait_entry.clone(), + Some(scope), + globals, + )?; + } + + drop(null_activation); + + self.run_script_initializer(script, context)?; + } + + Ok(()) + } + + pub fn globals(&self) -> Object<'gc> { + self.globals + } + + /// Push a value onto the operand stack. + fn push(&mut self, value: impl Into>) { + let value = value.into(); + avm_debug!("Stack push {}: {:?}", self.stack.len(), value); + self.stack.push(value); + } + + /// Retrieve the top-most value on the operand stack. + #[allow(clippy::let_and_return)] + fn pop(&mut self) -> Value<'gc> { + let value = self.stack.pop().unwrap_or_else(|| { + log::warn!("Avm1::pop: Stack underflow"); + Value::Undefined + }); + + avm_debug!("Stack pop {}: {:?}", self.stack.len(), value); + + value + } + + fn pop_args(&mut self, arg_count: u32) -> Vec> { + let mut args = Vec::with_capacity(arg_count as usize); + args.resize(arg_count as usize, Value::Undefined); + for arg in args.iter_mut().rev() { + *arg = self.pop(); + } + + args + } +} diff --git a/core/src/avm2/activation.rs b/core/src/avm2/activation.rs new file mode 100644 index 000000000000..cc8a04f2995c --- /dev/null +++ b/core/src/avm2/activation.rs @@ -0,0 +1,1660 @@ +//! Activation frames + +use crate::avm2::class::Class; +use crate::avm2::function::FunctionObject; +use crate::avm2::method::BytecodeMethod; +use crate::avm2::names::{Multiname, Namespace, QName}; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::scope::Scope; +use crate::avm2::script::Script; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::string::AvmString; +use crate::avm2::value::Value; +use crate::avm2::{value, Avm2, Error}; +use crate::context::UpdateContext; +use gc_arena::{Collect, Gc, GcCell, MutationContext}; +use smallvec::SmallVec; +use std::io::Cursor; +use swf::avm2::read::Reader; +use swf::avm2::types::{ + Class as AbcClass, Index, Method as AbcMethod, Multiname as AbcMultiname, + Namespace as AbcNamespace, Op, +}; + +/// Represents a particular register set. +/// +/// This type exists primarily because SmallVec isn't garbage-collectable. +#[derive(Clone)] +pub struct RegisterSet<'gc>(SmallVec<[Value<'gc>; 8]>); + +unsafe impl<'gc> gc_arena::Collect for RegisterSet<'gc> { + #[inline] + fn trace(&self, cc: gc_arena::CollectionContext) { + for register in &self.0 { + register.trace(cc); + } + } +} + +impl<'gc> RegisterSet<'gc> { + /// Create a new register set with a given number of specified registers. + /// + /// The given registers will be set to `undefined`. + pub fn new(num: u32) -> Self { + Self(smallvec![Value::Undefined; num as usize]) + } + + /// Return a reference to a given register, if it exists. + pub fn get(&self, num: u32) -> Option<&Value<'gc>> { + self.0.get(num as usize) + } + + /// Return a mutable reference to a given register, if it exists. + pub fn get_mut(&mut self, num: u32) -> Option<&mut Value<'gc>> { + self.0.get_mut(num as usize) + } +} + +#[derive(Debug, Clone)] +enum FrameControl<'gc> { + Continue, + Return(Value<'gc>), +} + +/// Represents a single activation of a given AVM2 function or keyframe. +#[derive(Collect)] +#[collect(no_drop)] +pub struct Activation<'a, 'gc: 'a> { + /// The AVM2 instance we execute under. + avm2: &'a mut Avm2<'gc>, + + /// The immutable value of `this`. + this: Option>, + + /// The arguments this function was called by. + arguments: Option>, + + /// Flags that the current activation frame is being executed and has a + /// reader object copied from it. Taking out two readers on the same + /// activation frame is a programming error. + is_executing: bool, + + /// Local registers. + /// + /// All activations have local registers, but it is possible for multiple + /// activations (such as a rescope) to execute from the same register set. + local_registers: GcCell<'gc, RegisterSet<'gc>>, + + /// What was returned from the function. + /// + /// A return value of `None` indicates that the called function is still + /// executing. Functions that do not return instead return `Undefined`. + return_value: Option>, + + /// The current local scope, implemented as a bare object. + local_scope: Object<'gc>, + + /// The current scope stack. + /// + /// A `scope` of `None` indicates that the scope stack is empty. + scope: Option>>, + + /// The base prototype of `this`. + /// + /// This will not be available if this is not a method call. + base_proto: Option>, +} + +impl<'a, 'gc: 'a> Activation<'a, 'gc> { + /// Construct an activation that does not represent any particular scope. + /// + /// This exists primarily for non-AVM2 related manipulations of the + /// interpreter environment that require an activation. For example, + /// loading traits into an object, or running tests. + /// + /// It is a logic error to attempt to run AVM2 code in a nothing + /// `Activation`. + pub fn from_nothing(avm2: &'a mut Avm2<'gc>, context: &mut UpdateContext<'_, 'gc, '_>) -> Self { + let local_registers = GcCell::allocate(context.gc_context, RegisterSet::new(0)); + + Self { + avm2, + this: None, + arguments: None, + is_executing: false, + local_registers, + return_value: None, + local_scope: ScriptObject::bare_object(context.gc_context), + scope: None, + base_proto: None, + } + } + + /// Construct an activation for the execution of a particular script's + /// initializer method. + pub fn from_script( + avm2: &'a mut Avm2<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + script: GcCell<'gc, Script<'gc>>, + global: Object<'gc>, + ) -> Result { + let method = script.read().init().into_bytecode()?; + let scope = Some(Scope::push_scope(None, global, context.gc_context)); + let body: Result<_, Error> = method + .body() + .ok_or_else(|| "Cannot execute non-native method (for script) without body".into()); + let num_locals = body?.num_locals; + let local_registers = + GcCell::allocate(context.gc_context, RegisterSet::new(num_locals + 1)); + + *local_registers + .write(context.gc_context) + .get_mut(0) + .unwrap() = global.into(); + + Ok(Self { + avm2, + this: Some(global), + arguments: None, + is_executing: false, + local_registers, + return_value: None, + local_scope: ScriptObject::bare_object(context.gc_context), + scope, + base_proto: None, + }) + } + + /// Construct an activation for the execution of a particular bytecode + /// method. + pub fn from_method( + avm2: &'a mut Avm2<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + method: Gc<'gc, BytecodeMethod<'gc>>, + scope: Option>>, + this: Option>, + arguments: &[Value<'gc>], + base_proto: Option>, + ) -> Result { + let body: Result<_, Error> = method + .body() + .ok_or_else(|| "Cannot execute non-native method without body".into()); + let num_locals = body?.num_locals; + let num_declared_arguments = method.method().params.len() as u32; + let local_registers = GcCell::allocate( + context.gc_context, + RegisterSet::new(num_locals + num_declared_arguments + 1), + ); + + { + let mut write = local_registers.write(context.gc_context); + *write.get_mut(0).unwrap() = this.map(|t| t.into()).unwrap_or(Value::Null); + + for i in 0..num_declared_arguments { + *write.get_mut(1 + i).unwrap() = arguments + .get(i as usize) + .cloned() + .unwrap_or(Value::Undefined); + } + } + + Ok(Self { + avm2, + this, + arguments: None, + is_executing: false, + local_registers, + return_value: None, + local_scope: ScriptObject::bare_object(context.gc_context), + scope, + base_proto, + }) + } + + /// Execute a script initializer. + pub fn run_stack_frame_for_script( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + script: GcCell<'gc, Script<'gc>>, + ) -> Result<(), Error> { + let init = script.read().init().into_bytecode()?; + + self.run_actions(init, context)?; + + Ok(()) + } + + /// Attempts to lock the activation frame for execution. + /// + /// If this frame is already executing, that is an error condition. + pub fn lock(&mut self) -> Result<(), Error> { + if self.is_executing { + return Err("Attempted to execute the same frame twice".into()); + } + + self.is_executing = true; + + Ok(()) + } + + /// Unlock the activation object. This allows future execution to run on it + /// again. + pub fn unlock_execution(&mut self) { + self.is_executing = false; + } + + /// Retrieve a local register. + pub fn local_register(&self, id: u32) -> Result, Error> { + self.local_registers + .read() + .get(id) + .cloned() + .ok_or_else(|| format!("Out of bounds register read: {}", id).into()) + } + + /// Get the current scope stack. + pub fn scope(&self) -> Option>> { + self.scope + } + + /// Set a new scope stack. + pub fn set_scope(&mut self, new_scope: Option>>) { + self.scope = new_scope; + } + + /// Set a local register. + /// + /// Returns `true` if the set was successful; `false` otherwise + pub fn set_local_register( + &mut self, + id: u32, + value: impl Into>, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + if let Some(r) = self.local_registers.write(mc).get_mut(id) { + *r = value.into(); + + Ok(()) + } else { + Err(format!("Out of bounds register write: {}", id).into()) + } + } + + pub fn avm2(&mut self) -> &mut Avm2<'gc> { + self.avm2 + } + + /// Set the return value. + pub fn set_return_value(&mut self, value: Value<'gc>) { + self.return_value = Some(value); + } + + /// Get the base prototype of the object that the currently executing + /// method was retrieved from, if one exists. + pub fn base_proto(&self) -> Option> { + self.base_proto + } + + /// Retrieve a int from the current constant pool. + fn pool_int( + &self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + ) -> Result { + value::abc_int(method.translation_unit(), index) + } + + /// Retrieve a int from the current constant pool. + fn pool_uint( + &self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + ) -> Result { + value::abc_uint(method.translation_unit(), index) + } + + /// Retrieve a double from the current constant pool. + fn pool_double( + &self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + ) -> Result { + value::abc_double(method.translation_unit(), index) + } + + /// Retrieve a string from the current constant pool. + fn pool_string<'b>( + &self, + method: &'b BytecodeMethod<'gc>, + index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + method.translation_unit().pool_string(index.0, mc) + } + + /// Retrieve a namespace from the current constant pool. + fn pool_namespace( + &self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + Namespace::from_abc_namespace(method.translation_unit(), index, mc) + } + + /// Retrieve a multiname from the current constant pool. + fn pool_multiname( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + Multiname::from_abc_multiname(method.translation_unit(), index, self.avm2, mc) + } + + /// Retrieve a static, or non-runtime, multiname from the current constant + /// pool. + fn pool_multiname_static( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + Multiname::from_abc_multiname_static(method.translation_unit(), index, mc) + } + + /// Retrieve a method entry from the current ABC file's method table. + fn table_method( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result>, Error> { + BytecodeMethod::from_method_index(method.translation_unit(), index.clone(), mc) + .ok_or_else(|| format!("Method index {} does not exist", index.0).into()) + } + + /// Retrieve a class entry from the current ABC file's method table. + fn table_class( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result>, Error> { + method + .translation_unit() + .load_class(index.0, context.gc_context) + } + + pub fn run_actions( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let body: Result<_, Error> = method + .body() + .ok_or_else(|| "Cannot execute non-native method without body".into()); + let mut read = Reader::new(Cursor::new(body?.code.as_ref())); + + loop { + let result = self.do_next_opcode(method, context, &mut read); + match result { + Ok(FrameControl::Return(value)) => break Ok(value), + Ok(FrameControl::Continue) => {} + Err(e) => break Err(e), + } + } + } + + /// Run a single action from a given action reader. + fn do_next_opcode( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + reader: &mut Reader>, + ) -> Result, Error> { + let op = reader.read_op(); + if let Ok(Some(op)) = op { + avm_debug!("Opcode: {:?}", op); + + let result = match op { + Op::PushByte { value } => self.op_push_byte(value), + Op::PushDouble { value } => self.op_push_double(method, value), + Op::PushFalse => self.op_push_false(), + Op::PushInt { value } => self.op_push_int(method, value), + Op::PushNamespace { value } => self.op_push_namespace(context, method, value), + Op::PushNaN => self.op_push_nan(), + Op::PushNull => self.op_push_null(), + Op::PushShort { value } => self.op_push_short(value), + Op::PushString { value } => self.op_push_string(context, method, value), + Op::PushTrue => self.op_push_true(), + Op::PushUint { value } => self.op_push_uint(method, value), + Op::PushUndefined => self.op_push_undefined(), + Op::Pop => self.op_pop(), + Op::Dup => self.op_dup(), + Op::GetLocal { index } => self.op_get_local(index), + Op::SetLocal { index } => self.op_set_local(context, index), + Op::Kill { index } => self.op_kill(context, index), + Op::Call { num_args } => self.op_call(context, num_args), + Op::CallMethod { index, num_args } => self.op_call_method(context, index, num_args), + Op::CallProperty { index, num_args } => { + self.op_call_property(method, context, index, num_args) + } + Op::CallPropLex { index, num_args } => { + self.op_call_prop_lex(method, context, index, num_args) + } + Op::CallPropVoid { index, num_args } => { + self.op_call_prop_void(method, context, index, num_args) + } + Op::CallStatic { index, num_args } => { + self.op_call_static(method, context, index, num_args) + } + Op::CallSuper { index, num_args } => { + self.op_call_super(method, context, index, num_args) + } + Op::CallSuperVoid { index, num_args } => { + self.op_call_super_void(method, context, index, num_args) + } + Op::ReturnValue => self.op_return_value(), + Op::ReturnVoid => self.op_return_void(), + Op::GetProperty { index } => self.op_get_property(method, context, index), + Op::SetProperty { index } => self.op_set_property(method, context, index), + Op::InitProperty { index } => self.op_init_property(method, context, index), + Op::DeleteProperty { index } => self.op_delete_property(method, context, index), + Op::GetSuper { index } => self.op_get_super(method, context, index), + Op::SetSuper { index } => self.op_set_super(method, context, index), + Op::PushScope => self.op_push_scope(context), + Op::PushWith => self.op_push_with(context), + Op::PopScope => self.op_pop_scope(), + Op::GetScopeObject { index } => self.op_get_scope_object(index), + Op::GetGlobalScope => self.op_get_global_scope(), + Op::FindProperty { index } => self.op_find_property(method, context, index), + Op::FindPropStrict { index } => self.op_find_prop_strict(method, context, index), + Op::GetLex { index } => self.op_get_lex(method, context, index), + Op::GetSlot { index } => self.op_get_slot(index), + Op::SetSlot { index } => self.op_set_slot(context, index), + Op::GetGlobalSlot { index } => self.op_get_global_slot(index), + Op::SetGlobalSlot { index } => self.op_set_global_slot(context, index), + Op::Construct { num_args } => self.op_construct(context, num_args), + Op::ConstructProp { index, num_args } => { + self.op_construct_prop(method, context, index, num_args) + } + Op::ConstructSuper { num_args } => self.op_construct_super(context, num_args), + Op::NewActivation => self.op_new_activation(context), + Op::NewObject { num_args } => self.op_new_object(context, num_args), + Op::NewFunction { index } => self.op_new_function(method, context, index), + Op::NewClass { index } => self.op_new_class(method, context, index), + Op::CoerceA => self.op_coerce_a(), + Op::Jump { offset } => self.op_jump(offset, reader), + Op::IfTrue { offset } => self.op_if_true(offset, reader), + Op::IfFalse { offset } => self.op_if_false(offset, reader), + Op::IfStrictEq { offset } => self.op_if_strict_eq(offset, reader), + Op::IfStrictNe { offset } => self.op_if_strict_ne(offset, reader), + Op::StrictEquals => self.op_strict_equals(), + Op::HasNext => self.op_has_next(), + Op::HasNext2 { + object_register, + index_register, + } => self.op_has_next_2(context, object_register, index_register), + Op::NextName => self.op_next_name(), + Op::NextValue => self.op_next_value(context), + Op::IsType { index } => self.op_is_type(method, context, index), + Op::IsTypeLate => self.op_is_type_late(context), + Op::InstanceOf => self.op_instance_of(context), + Op::Label => Ok(FrameControl::Continue), + Op::Debug { + is_local_register, + register_name, + register, + } => self.op_debug(method, is_local_register, register_name, register), + Op::DebugFile { file_name } => self.op_debug_file(method, file_name), + Op::DebugLine { line_num } => self.op_debug_line(line_num), + _ => self.unknown_op(op), + }; + + if let Err(e) = result { + log::error!("AVM2 error: {}", e); + return Err(e); + } + result + } else if let Ok(None) = op { + log::error!("Unknown opcode!"); + Err("Unknown opcode!".into()) + } else if let Err(e) = op { + log::error!("Parse error: {:?}", e); + Err(e.into()) + } else { + unreachable!(); + } + } + + fn unknown_op(&mut self, op: swf::avm2::types::Op) -> Result, Error> { + log::error!("Unknown AVM2 opcode: {:?}", op); + Err("Unknown op".into()) + } + + fn op_push_byte(&mut self, value: u8) -> Result, Error> { + self.avm2.push(value); + Ok(FrameControl::Continue) + } + + fn op_push_double( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + value: Index, + ) -> Result, Error> { + self.avm2.push(self.pool_double(method, value)?); + Ok(FrameControl::Continue) + } + + fn op_push_false(&mut self) -> Result, Error> { + self.avm2.push(false); + Ok(FrameControl::Continue) + } + + fn op_push_int( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + value: Index, + ) -> Result, Error> { + self.avm2.push(self.pool_int(method, value)?); + Ok(FrameControl::Continue) + } + + fn op_push_namespace( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + method: Gc<'gc, BytecodeMethod<'gc>>, + value: Index, + ) -> Result, Error> { + self.avm2 + .push(self.pool_namespace(method, value, context.gc_context)?); + Ok(FrameControl::Continue) + } + + fn op_push_nan(&mut self) -> Result, Error> { + self.avm2.push(std::f64::NAN); + Ok(FrameControl::Continue) + } + + fn op_push_null(&mut self) -> Result, Error> { + self.avm2.push(Value::Null); + Ok(FrameControl::Continue) + } + + fn op_push_short(&mut self, value: u32) -> Result, Error> { + self.avm2.push(value); + Ok(FrameControl::Continue) + } + + fn op_push_string( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + method: Gc<'gc, BytecodeMethod<'gc>>, + value: Index, + ) -> Result, Error> { + self.avm2 + .push(self.pool_string(&method, value, context.gc_context)?); + Ok(FrameControl::Continue) + } + + fn op_push_true(&mut self) -> Result, Error> { + self.avm2.push(true); + Ok(FrameControl::Continue) + } + + fn op_push_uint( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + value: Index, + ) -> Result, Error> { + self.avm2.push(self.pool_uint(method, value)?); + Ok(FrameControl::Continue) + } + + fn op_push_undefined(&mut self) -> Result, Error> { + self.avm2.push(Value::Undefined); + Ok(FrameControl::Continue) + } + + fn op_pop(&mut self) -> Result, Error> { + self.avm2.pop(); + + Ok(FrameControl::Continue) + } + + fn op_dup(&mut self) -> Result, Error> { + self.avm2 + .push(self.avm2.stack.last().cloned().unwrap_or(Value::Undefined)); + + Ok(FrameControl::Continue) + } + + fn op_get_local(&mut self, register_index: u32) -> Result, Error> { + self.avm2.push(self.local_register(register_index)?); + Ok(FrameControl::Continue) + } + + fn op_set_local( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + register_index: u32, + ) -> Result, Error> { + let value = self.avm2.pop(); + + self.set_local_register(register_index, value, context.gc_context)?; + + Ok(FrameControl::Continue) + } + + fn op_kill( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + register_index: u32, + ) -> Result, Error> { + self.set_local_register(register_index, Value::Undefined, context.gc_context)?; + + Ok(FrameControl::Continue) + } + + fn op_call( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let receiver = self.avm2.pop().as_object().ok(); + let function = self.avm2.pop().as_object()?; + let base_proto = receiver.and_then(|r| r.proto()); + let value = function.call(receiver, &args, self, context, base_proto)?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_call_method( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let receiver = self.avm2.pop().as_object()?; + let function: Result, Error> = receiver + .get_method(index.0) + .ok_or_else(|| format!("Object method {} does not exist", index.0).into()); + let base_proto = receiver.proto(); + let value = function?.call(Some(receiver), &args, self, context, base_proto)?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_call_property( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let mut receiver = self.avm2.pop().as_object()?; + let name: Result = receiver + .resolve_multiname(&multiname)? + .ok_or_else(|| format!("Could not find method {:?}", multiname.local_name()).into()); + let name = name?; + let base_proto = receiver.get_base_proto(&name)?; + let function = receiver + .get_property(receiver, &name, self, context)? + .as_object()?; + let value = function.call(Some(receiver), &args, self, context, base_proto)?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_call_prop_lex( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let mut receiver = self.avm2.pop().as_object()?; + let name: Result = receiver + .resolve_multiname(&multiname)? + .ok_or_else(|| format!("Could not find method {:?}", multiname.local_name()).into()); + let function = receiver + .get_property(receiver, &name?, self, context)? + .as_object()?; + let value = function.call(None, &args, self, context, None)?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_call_prop_void( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let mut receiver = self.avm2.pop().as_object()?; + let name: Result = receiver + .resolve_multiname(&multiname)? + .ok_or_else(|| format!("Could not find method {:?}", multiname.local_name()).into()); + let name = name?; + let base_proto = receiver.get_base_proto(&name)?; + let function = receiver + .get_property(receiver, &name, self, context)? + .as_object()?; + + function.call(Some(receiver), &args, self, context, base_proto)?; + + Ok(FrameControl::Continue) + } + + fn op_call_static( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let receiver = self.avm2.pop().as_object()?; + let method = self.table_method(method, index, context.gc_context)?; + let scope = self.scope(); //TODO: Is this correct? + let function = FunctionObject::from_method( + context.gc_context, + method.into(), + scope, + self.avm2.prototypes().function, + None, + ); + let value = function.call(Some(receiver), &args, self, context, receiver.proto())?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_call_super( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let receiver = self.avm2.pop().as_object()?; + let name: Result = receiver + .resolve_multiname(&multiname)? + .ok_or_else(|| format!("Could not find method {:?}", multiname.local_name()).into()); + let base_proto: Result, Error> = + self.base_proto().and_then(|bp| bp.proto()).ok_or_else(|| { + "Attempted to call super method without a superclass." + .to_string() + .into() + }); + let base_proto = base_proto?; + let mut base = base_proto.construct(self, context, &[])?; //TODO: very hacky workaround + + let function = base + .get_property(receiver, &name?, self, context)? + .as_object()?; + + let value = function.call(Some(receiver), &args, self, context, Some(base_proto))?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_call_super_void( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let receiver = self.avm2.pop().as_object()?; + let name: Result = receiver + .resolve_multiname(&multiname)? + .ok_or_else(|| format!("Could not find method {:?}", multiname.local_name()).into()); + let base_proto: Result, Error> = + self.base_proto().and_then(|bp| bp.proto()).ok_or_else(|| { + "Attempted to call super method without a superclass." + .to_string() + .into() + }); + let base_proto = base_proto?; + let mut base = base_proto.construct(self, context, &[])?; //TODO: very hacky workaround + + let function = base + .get_property(receiver, &name?, self, context)? + .as_object()?; + + function.call(Some(receiver), &args, self, context, Some(base_proto))?; + + Ok(FrameControl::Continue) + } + + fn op_return_value(&mut self) -> Result, Error> { + let return_value = self.avm2.pop(); + + Ok(FrameControl::Return(return_value)) + } + + fn op_return_void(&mut self) -> Result, Error> { + Ok(FrameControl::Return(Value::Undefined)) + } + + fn op_get_property( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let mut object = self.avm2.pop().as_object()?; + + let name: Result = object.resolve_multiname(&multiname)?.ok_or_else(|| { + format!("Could not resolve property {:?}", multiname.local_name()).into() + }); + + let value = object.get_property(object, &name?, self, context)?; + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_set_property( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let value = self.avm2.pop(); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let mut object = self.avm2.pop().as_object()?; + + if let Some(name) = object.resolve_multiname(&multiname)? { + object.set_property(object, &name, value, self, context)?; + } else { + //TODO: Non-dynamic objects should fail + //TODO: This should only work if the public namespace is present + let local_name: Result, Error> = multiname + .local_name() + .ok_or_else(|| "Cannot set property using any name".into()); + let name = QName::dynamic_name(local_name?); + object.set_property(object, &name, value, self, context)?; + } + + Ok(FrameControl::Continue) + } + + fn op_init_property( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let value = self.avm2.pop(); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let mut object = self.avm2.pop().as_object()?; + + if let Some(name) = object.resolve_multiname(&multiname)? { + object.init_property(object, &name, value, self, context)?; + } else { + //TODO: Non-dynamic objects should fail + //TODO: This should only work if the public namespace is present + let local_name: Result, Error> = multiname + .local_name() + .ok_or_else(|| "Cannot set property using any name".into()); + let name = QName::dynamic_name(local_name?); + object.init_property(object, &name, value, self, context)?; + } + + Ok(FrameControl::Continue) + } + + fn op_delete_property( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let object = self.avm2.pop().as_object()?; + + if let Some(name) = object.resolve_multiname(&multiname)? { + self.avm2 + .push(object.delete_property(context.gc_context, &name)) + } else { + self.avm2.push(false) + } + + Ok(FrameControl::Continue) + } + + fn op_get_super( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let object = self.avm2.pop().as_object()?; + let base_proto: Result, Error> = self + .base_proto() + .and_then(|p| p.proto()) + .ok_or_else(|| "Attempted to get property on non-existent super object".into()); + let base_proto = base_proto?; + let mut base = base_proto.construct(self, context, &[])?; //TODO: very hacky workaround + + let name: Result = base.resolve_multiname(&multiname)?.ok_or_else(|| { + format!( + "Could not resolve {:?} as super property", + multiname.local_name() + ) + .into() + }); + + let value = base.get_property(object, &name?, self, context)?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_set_super( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let value = self.avm2.pop(); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let object = self.avm2.pop().as_object()?; + let base_proto: Result, Error> = self + .base_proto() + .and_then(|p| p.proto()) + .ok_or_else(|| "Attempted to get property on non-existent super object".into()); + let base_proto = base_proto?; + let mut base = base_proto.construct(self, context, &[])?; //TODO: very hacky workaround + + let name: Result = base.resolve_multiname(&multiname)?.ok_or_else(|| { + format!( + "Could not resolve {:?} as super property", + multiname.local_name() + ) + .into() + }); + + base.set_property(object, &name?, value, self, context)?; + + Ok(FrameControl::Continue) + } + + fn op_push_scope( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let object = self.avm2.pop().as_object()?; + let scope_stack = self.scope(); + let new_scope = Scope::push_scope(scope_stack, object, context.gc_context); + + self.set_scope(Some(new_scope)); + + Ok(FrameControl::Continue) + } + + fn op_push_with( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let object = self.avm2.pop().as_object()?; + let scope_stack = self.scope(); + let new_scope = Scope::push_with(scope_stack, object, context.gc_context); + + self.set_scope(Some(new_scope)); + + Ok(FrameControl::Continue) + } + + fn op_pop_scope(&mut self) -> Result, Error> { + let scope_stack = self.scope(); + let new_scope = scope_stack.and_then(|s| s.read().pop_scope()); + + self.set_scope(new_scope); + + Ok(FrameControl::Continue) + } + + fn op_get_scope_object(&mut self, mut index: u8) -> Result, Error> { + let mut scope = self.scope(); + + while index > 0 { + if let Some(child_scope) = scope { + scope = child_scope.read().parent_cell(); + } + + index -= 1; + } + + self.avm2.push( + scope + .map(|s| s.read().locals().clone().into()) + .unwrap_or(Value::Undefined), + ); + + Ok(FrameControl::Continue) + } + + fn op_get_global_scope(&mut self) -> Result, Error> { + let mut scope = self.scope(); + + while let Some(this_scope) = scope { + let parent = this_scope.read().parent_cell(); + if parent.is_none() { + break; + } + + scope = parent; + } + + self.avm2.push( + scope + .map(|s| s.read().locals().clone().into()) + .unwrap_or(Value::Undefined), + ); + + Ok(FrameControl::Continue) + } + + fn op_find_property( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let multiname = self.pool_multiname(method, index, context.gc_context)?; + avm_debug!("Resolving {:?}", multiname); + let result = if let Some(scope) = self.scope() { + scope.read().find(&multiname, self, context)? + } else { + None + }; + + self.avm2 + .push(result.map(|o| o.into()).unwrap_or(Value::Undefined)); + + Ok(FrameControl::Continue) + } + + fn op_find_prop_strict( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let multiname = self.pool_multiname(method, index, context.gc_context)?; + avm_debug!("Resolving {:?}", multiname); + let found: Result, Error> = if let Some(scope) = self.scope() { + scope.read().find(&multiname, self, context)? + } else { + None + } + .ok_or_else(|| format!("Property does not exist: {:?}", multiname.local_name()).into()); + let result: Value<'gc> = found?.into(); + + self.avm2.push(result); + + Ok(FrameControl::Continue) + } + + fn op_get_lex( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let multiname = self.pool_multiname_static(method, index, context.gc_context)?; + avm_debug!("Resolving {:?}", multiname); + let found: Result, Error> = if let Some(scope) = self.scope() { + scope + .write(context.gc_context) + .resolve(&multiname, self, context)? + } else { + None + } + .ok_or_else(|| format!("Property does not exist: {:?}", multiname.local_name()).into()); + let result: Value<'gc> = found?; + + self.avm2.push(result); + + Ok(FrameControl::Continue) + } + + fn op_get_slot(&mut self, index: u32) -> Result, Error> { + let object = self.avm2.pop().as_object()?; + let value = object.get_slot(index)?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_set_slot( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + index: u32, + ) -> Result, Error> { + let object = self.avm2.pop().as_object()?; + let value = self.avm2.pop(); + + object.set_slot(index, value, context.gc_context)?; + + Ok(FrameControl::Continue) + } + + fn op_get_global_slot(&mut self, index: u32) -> Result, Error> { + let value = self.avm2.globals().get_slot(index)?; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_set_global_slot( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + index: u32, + ) -> Result, Error> { + let value = self.avm2.pop(); + + self.avm2 + .globals() + .set_slot(index, value, context.gc_context)?; + + Ok(FrameControl::Continue) + } + + fn op_construct( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let mut ctor = self.avm2.pop().as_object()?; + + let proto = ctor + .get_property( + ctor, + &QName::new(Namespace::public_namespace(), "prototype"), + self, + context, + )? + .as_object()?; + + let object = proto.construct(self, context, &args)?; + ctor.call(Some(object), &args, self, context, object.proto())?; + + self.avm2.push(object); + + Ok(FrameControl::Continue) + } + + fn op_construct_prop( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let multiname = self.pool_multiname(method, index, context.gc_context)?; + let mut source = self.avm2.pop().as_object()?; + + let ctor_name: Result = + source.resolve_multiname(&multiname)?.ok_or_else(|| { + format!("Could not resolve property {:?}", multiname.local_name()).into() + }); + let mut ctor = source + .get_property(source, &ctor_name?, self, context)? + .as_object()?; + let proto = ctor + .get_property( + ctor, + &QName::new(Namespace::public_namespace(), "prototype"), + self, + context, + )? + .as_object()?; + + let object = proto.construct(self, context, &args)?; + ctor.call(Some(object), &args, self, context, Some(proto))?; + + self.avm2.push(object); + + Ok(FrameControl::Continue) + } + + fn op_construct_super( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + arg_count: u32, + ) -> Result, Error> { + let args = self.avm2.pop_args(arg_count); + let receiver = self.avm2.pop().as_object()?; + let name = QName::new(Namespace::public_namespace(), "constructor"); + let base_proto: Result, Error> = + self.base_proto().and_then(|p| p.proto()).ok_or_else(|| { + "Attempted to call super constructor without a superclass." + .to_string() + .into() + }); + let mut base_proto = base_proto?; + + let function = base_proto + .get_property(receiver, &name, self, context)? + .as_object()?; + + function.call(Some(receiver), &args, self, context, Some(base_proto))?; + + Ok(FrameControl::Continue) + } + + fn op_new_activation( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + self.avm2 + .push(ScriptObject::bare_object(context.gc_context)); + + Ok(FrameControl::Continue) + } + + fn op_new_object( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + num_args: u32, + ) -> Result, Error> { + let mut object = ScriptObject::object(context.gc_context, self.avm2.prototypes().object); + + for _ in 0..num_args { + let value = self.avm2.pop(); + let name = self.avm2.pop(); + + object.set_property( + object, + &QName::new(Namespace::public_namespace(), name.as_string()?), + value, + self, + context, + )?; + } + + self.avm2.push(object); + + Ok(FrameControl::Continue) + } + + fn op_new_function( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let method_entry = self.table_method(method, index, context.gc_context)?; + let scope = self.scope(); + + let mut new_fn = FunctionObject::from_method( + context.gc_context, + method_entry.into(), + scope, + self.avm2.prototypes().function, + None, + ); + let es3_proto = ScriptObject::object(context.gc_context, self.avm2.prototypes().object); + + new_fn.install_slot( + context.gc_context, + QName::new(Namespace::public_namespace(), "prototype"), + 0, + es3_proto.into(), + ); + + self.avm2.push(new_fn); + + Ok(FrameControl::Continue) + } + + fn op_new_class( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + index: Index, + ) -> Result, Error> { + let base_value = self.avm2.pop(); + let base_class = match base_value { + Value::Object(o) => Some(o), + Value::Null => None, + _ => return Err("Base class for new class is not Object or null.".into()), + }; + + let class_entry = self.table_class(method, index, context)?; + let scope = self.scope(); + + let (new_class, class_init) = + FunctionObject::from_class(self, context, class_entry, base_class, scope)?; + + class_init.call(Some(new_class), &[], self, context, None)?; + + self.avm2.push(new_class); + + Ok(FrameControl::Continue) + } + + fn op_coerce_a(&mut self) -> Result, Error> { + Ok(FrameControl::Continue) + } + + fn op_jump( + &mut self, + offset: i32, + reader: &mut Reader>, + ) -> Result, Error> { + reader.seek(offset as i64)?; + + Ok(FrameControl::Continue) + } + + fn op_if_true( + &mut self, + offset: i32, + reader: &mut Reader>, + ) -> Result, Error> { + let value = self.avm2.pop().as_bool()?; + + if value { + reader.seek(offset as i64)?; + } + + Ok(FrameControl::Continue) + } + + fn op_if_false( + &mut self, + offset: i32, + reader: &mut Reader>, + ) -> Result, Error> { + let value = self.avm2.pop().as_bool()?; + + if !value { + reader.seek(offset as i64)?; + } + + Ok(FrameControl::Continue) + } + + fn op_if_strict_eq( + &mut self, + offset: i32, + reader: &mut Reader>, + ) -> Result, Error> { + let value2 = self.avm2.pop(); + let value1 = self.avm2.pop(); + + if value1 == value2 { + reader.seek(offset as i64)?; + } + + Ok(FrameControl::Continue) + } + + fn op_if_strict_ne( + &mut self, + offset: i32, + reader: &mut Reader>, + ) -> Result, Error> { + let value2 = self.avm2.pop(); + let value1 = self.avm2.pop(); + + if value1 != value2 { + reader.seek(offset as i64)?; + } + + Ok(FrameControl::Continue) + } + + fn op_strict_equals(&mut self) -> Result, Error> { + let value2 = self.avm2.pop(); + let value1 = self.avm2.pop(); + + self.avm2.push(value1 == value2); + + Ok(FrameControl::Continue) + } + + fn op_has_next(&mut self) -> Result, Error> { + //TODO: After adding ints, change this to ints. + let cur_index = self.avm2.pop().as_number()?; + let object = self.avm2.pop().as_object()?; + + let next_index = cur_index as u32 + 1; + + if object.get_enumerant_name(next_index).is_some() { + self.avm2.push(next_index as f32); + } else { + self.avm2.push(0.0); + } + + Ok(FrameControl::Continue) + } + + fn op_has_next_2( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + object_register: u32, + index_register: u32, + ) -> Result, Error> { + //TODO: After adding ints, change this to ints. + let cur_index = self.local_register(index_register)?.as_number()?; + let mut object = Some(self.local_register(object_register)?.as_object()?); + + let mut next_index = cur_index as u32 + 1; + + while let Some(cur_object) = object { + if cur_object.get_enumerant_name(next_index).is_none() { + next_index = 1; + object = cur_object.proto(); + } else { + break; + } + } + + if object.is_none() { + next_index = 0; + } + + self.avm2.push(next_index != 0); + self.set_local_register(index_register, next_index, context.gc_context)?; + self.set_local_register( + object_register, + object.map(|v| v.into()).unwrap_or(Value::Null), + context.gc_context, + )?; + + Ok(FrameControl::Continue) + } + + fn op_next_name(&mut self) -> Result, Error> { + //TODO: After adding ints, change this to ints. + let cur_index = self.avm2.pop().as_number()?; + let object = self.avm2.pop().as_object()?; + + let name = object + .get_enumerant_name(cur_index as u32) + .map(|n| n.local_name().into()); + + self.avm2.push(name.unwrap_or(Value::Undefined)); + + Ok(FrameControl::Continue) + } + + fn op_next_value( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + //TODO: After adding ints, change this to ints. + let cur_index = self.avm2.pop().as_number()?; + let mut object = self.avm2.pop().as_object()?; + + let name = object.get_enumerant_name(cur_index as u32); + let value = if let Some(name) = name { + object.get_property(object, &name, self, context)? + } else { + Value::Undefined + }; + + self.avm2.push(value); + + Ok(FrameControl::Continue) + } + + fn op_is_type( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + context: &mut UpdateContext<'_, 'gc, '_>, + type_name_index: Index, + ) -> Result, Error> { + let value = self.avm2.pop().as_object()?; + + let type_name = self.pool_multiname_static(method, type_name_index, context.gc_context)?; + let type_object = if let Some(scope) = self.scope() { + scope.read().find(&type_name, self, context)? + } else { + None + }; + + if let Some(type_object) = type_object { + let is_instance_of = value.is_instance_of(self, context, type_object, true)?; + self.avm2.push(is_instance_of); + } else { + return Err(format!( + "Attempted to check against nonexistent type {:?}", + type_name + ) + .into()); + } + + Ok(FrameControl::Continue) + } + + fn op_is_type_late( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let type_object = self.avm2.pop().as_object()?; + let value = self.avm2.pop().as_object()?; + + let is_instance_of = value.is_instance_of(self, context, type_object, true)?; + + self.avm2.push(is_instance_of); + + Ok(FrameControl::Continue) + } + + fn op_instance_of( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let type_object = self.avm2.pop().as_object()?; + let value = self.avm2.pop().as_object()?; + + let is_instance_of = value.is_instance_of(self, context, type_object, false)?; + + self.avm2.push(is_instance_of); + + Ok(FrameControl::Continue) + } + + #[allow(unused_variables)] + #[cfg(avm_debug)] + fn op_debug( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + is_local_register: bool, + register_name: Index, + register: u8, + ) -> Result, Error> { + if is_local_register { + let register_name = self.pool_string(method, register_name)?; + let value = self.local_register(register as u32)?; + + avm_debug!("Debug: {} = {:?}", register_name, value); + } else { + avm_debug!("Unknown debugging mode!"); + } + + Ok(FrameControl::Continue) + } + + #[allow(unused_variables)] + #[cfg(not(avm_debug))] + fn op_debug( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + is_local_register: bool, + register_name: Index, + register: u8, + ) -> Result, Error> { + Ok(FrameControl::Continue) + } + + #[allow(unused_variables)] + #[cfg(avm_debug)] + fn op_debug_file( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + file_name: Index, + ) -> Result, Error> { + let file_name = self.pool_string(method, file_name)?; + + avm_debug!("File: {}", file_name); + + Ok(FrameControl::Continue) + } + + #[allow(unused_variables)] + #[cfg(not(avm_debug))] + fn op_debug_file( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + file_name: Index, + ) -> Result, Error> { + Ok(FrameControl::Continue) + } + + #[allow(unused_variables)] + fn op_debug_line(&mut self, line_num: u32) -> Result, Error> { + avm_debug!("Line: {}", line_num); + + Ok(FrameControl::Continue) + } +} diff --git a/core/src/avm2/class.rs b/core/src/avm2/class.rs new file mode 100644 index 000000000000..d13358b642f9 --- /dev/null +++ b/core/src/avm2/class.rs @@ -0,0 +1,324 @@ +//! AVM2 classes + +use crate::avm2::method::Method; +use crate::avm2::names::{Multiname, Namespace, QName}; +use crate::avm2::r#trait::{Trait, TraitKind}; +use crate::avm2::script::TranslationUnit; +use crate::avm2::string::AvmString; +use crate::avm2::Error; +use gc_arena::{Collect, GcCell, MutationContext}; +use swf::avm2::types::{Class as AbcClass, Instance as AbcInstance}; + +/// A loaded ABC Class which can be used to construct objects with. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct Class<'gc> { + /// The name of the class. + name: QName<'gc>, + + /// The name of this class's superclass. + super_class: Option>, + + /// If this class is sealed (dynamic property writes should fail) + is_sealed: bool, + + /// If this class is final (subclassing should fail) + is_final: bool, + + /// If this class is an interface + is_interface: bool, + + /// The namespace that protected traits of this class are stored into. + protected_namespace: Option>, + + /// The list of interfaces this class implements. + interfaces: Vec>, + + /// The instance initializer for this class. + /// + /// Must be called each time a new class instance is constructed. + instance_init: Method<'gc>, + + /// Instance traits for a given class. + /// + /// These are accessed as normal instance properties; they should not be + /// present on prototypes, but instead should shadow any prototype + /// properties that would match. + instance_traits: Vec>, + + /// The class initializer for this class. + /// + /// Must be called once prior to any use of this class. + class_init: Method<'gc>, + + /// Static traits for a given class. + /// + /// These are accessed as constructor properties. + class_traits: Vec>, + + /// Whether or not this `Class` has loaded it's traits or not. + traits_loaded: bool, +} + +/// Find traits in a list of traits matching a name. +/// +/// This function also enforces final/override bits on the traits, and will +/// raise `VerifyError`s as needed. +/// +/// TODO: This is an O(n^2) algorithm, it sucks. +fn do_trait_lookup<'gc>( + name: &QName<'gc>, + known_traits: &mut Vec>, + all_traits: &[Trait<'gc>], +) -> Result<(), Error> { + for trait_entry in all_traits { + if name == trait_entry.name() { + for known_trait in known_traits.iter() { + match (&trait_entry.kind(), &known_trait.kind()) { + (TraitKind::Getter { .. }, TraitKind::Setter { .. }) => continue, + (TraitKind::Setter { .. }, TraitKind::Getter { .. }) => continue, + _ => {} + }; + + if known_trait.is_final() { + return Err("Attempting to override a final definition".into()); + } + + if !trait_entry.is_override() { + return Err("Definition override is not marked as override".into()); + } + } + + known_traits.push(trait_entry.clone()); + } + } + + Ok(()) +} + +impl<'gc> Class<'gc> { + /// Construct a class from a `TranslationUnit` and it's class index. + /// + /// The returned class will be allocated, but no traits will be loaded. The + /// caller is responsible for storing the class in the `TranslationUnit` + /// and calling `load_traits` to complete the trait-loading process. + pub fn from_abc_index( + unit: TranslationUnit<'gc>, + class_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + let abc = unit.abc(); + let abc_class: Result<&AbcClass, Error> = abc + .classes + .get(class_index as usize) + .ok_or_else(|| "LoadError: Class index not valid".into()); + let abc_class = abc_class?; + + let abc_instance: Result<&AbcInstance, Error> = abc + .instances + .get(class_index as usize) + .ok_or_else(|| "LoadError: Instance index not valid".into()); + let abc_instance = abc_instance?; + + let name = QName::from_abc_multiname(unit, abc_instance.name.clone(), mc)?; + let super_class = if abc_instance.super_name.0 == 0 { + None + } else { + Some(Multiname::from_abc_multiname_static( + unit, + abc_instance.super_name.clone(), + mc, + )?) + }; + + let protected_namespace = if let Some(ns) = &abc_instance.protected_namespace { + Some(Namespace::from_abc_namespace(unit, ns.clone(), mc)?) + } else { + None + }; + + let mut interfaces = Vec::new(); + for interface_name in abc_instance.interfaces.iter() { + interfaces.push(Multiname::from_abc_multiname_static( + unit, + interface_name.clone(), + mc, + )?); + } + + let instance_init = unit.load_method(abc_instance.init_method.0, mc)?; + let class_init = unit.load_method(abc_class.init_method.0, mc)?; + + Ok(GcCell::allocate( + mc, + Self { + name, + super_class, + is_sealed: abc_instance.is_sealed, + is_final: abc_instance.is_final, + is_interface: abc_instance.is_interface, + protected_namespace, + interfaces, + instance_init, + instance_traits: Vec::new(), + class_init, + class_traits: Vec::new(), + traits_loaded: false, + }, + )) + } + + /// Finish the class-loading process by loading traits. + /// + /// This process must be done after the `Class` has been stored in the + /// `TranslationUnit`. Failing to do so runs the risk of runaway recursion + /// or double-borrows. It should be done before the class is actually + /// instantiated into an `Object`. + pub fn load_traits( + &mut self, + unit: TranslationUnit<'gc>, + class_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + if self.traits_loaded { + return Ok(()); + } + + self.traits_loaded = true; + + let abc = unit.abc(); + let abc_class: Result<&AbcClass, Error> = abc + .classes + .get(class_index as usize) + .ok_or_else(|| "LoadError: Class index not valid".into()); + let abc_class = abc_class?; + + let abc_instance: Result<&AbcInstance, Error> = abc + .instances + .get(class_index as usize) + .ok_or_else(|| "LoadError: Instance index not valid".into()); + let abc_instance = abc_instance?; + + for abc_trait in abc_instance.traits.iter() { + self.instance_traits + .push(Trait::from_abc_trait(unit, &abc_trait, mc)?); + } + + for abc_trait in abc_class.traits.iter() { + self.class_traits + .push(Trait::from_abc_trait(unit, &abc_trait, mc)?); + } + + Ok(()) + } + + pub fn name(&self) -> &QName<'gc> { + &self.name + } + + pub fn super_class_name(&self) -> &Option> { + &self.super_class + } + + /// Given a name, append class traits matching the name to a list of known + /// traits. + /// + /// This function adds it's result onto the list of known traits, with the + /// caveat that duplicate entries will be replaced (if allowed). As such, this + /// function should be run on the class hierarchy from top to bottom. + /// + /// If a given trait has an invalid name, attempts to override a final trait, + /// or overlaps an existing trait without being an override, then this function + /// returns an error. + pub fn lookup_class_traits( + &self, + name: &QName<'gc>, + known_traits: &mut Vec>, + ) -> Result<(), Error> { + do_trait_lookup(name, known_traits, &self.class_traits) + } + + /// Determines if this class provides a given trait on itself. + pub fn has_class_trait(&self, name: &QName<'gc>) -> bool { + for trait_entry in self.class_traits.iter() { + if name == trait_entry.name() { + return true; + } + } + + false + } + + /// Look for a class trait with a given local name, and return it's + /// namespace. + /// + /// TODO: Matching multiple namespaces with the same local name is at least + /// claimed by the AVM2 specification to be a `VerifyError`. + pub fn resolve_any_class_trait(&self, local_name: AvmString<'gc>) -> Option> { + for trait_entry in self.class_traits.iter() { + if local_name == trait_entry.name().local_name() { + return Some(trait_entry.name().namespace().clone()); + } + } + + None + } + + /// Given a name, append instance traits matching the name to a list of + /// known traits. + /// + /// This function adds it's result onto the list of known traits, with the + /// caveat that duplicate entries will be replaced (if allowed). As such, this + /// function should be run on the class hierarchy from top to bottom. + /// + /// If a given trait has an invalid name, attempts to override a final trait, + /// or overlaps an existing trait without being an override, then this function + /// returns an error. + pub fn lookup_instance_traits( + &self, + name: &QName<'gc>, + known_traits: &mut Vec>, + ) -> Result<(), Error> { + do_trait_lookup(name, known_traits, &self.instance_traits) + } + + /// Determines if this class provides a given trait on it's instances. + pub fn has_instance_trait(&self, name: &QName<'gc>) -> bool { + for trait_entry in self.instance_traits.iter() { + if name == trait_entry.name() { + return true; + } + } + + false + } + + /// Look for an instance trait with a given local name, and return it's + /// namespace. + /// + /// TODO: Matching multiple namespaces with the same local name is at least + /// claimed by the AVM2 specification to be a `VerifyError`. + pub fn resolve_any_instance_trait(&self, local_name: AvmString<'gc>) -> Option> { + for trait_entry in self.instance_traits.iter() { + if local_name == trait_entry.name().local_name() { + return Some(trait_entry.name().namespace().clone()); + } + } + + None + } + + /// Get this class's instance initializer. + pub fn instance_init(&self) -> Method<'gc> { + self.instance_init.clone() + } + + /// Get this class's class initializer. + pub fn class_init(&self) -> Method<'gc> { + self.class_init.clone() + } + + pub fn interfaces(&self) -> &[Multiname<'gc>] { + &self.interfaces + } +} diff --git a/core/src/avm2/function.rs b/core/src/avm2/function.rs new file mode 100644 index 000000000000..686a83230837 --- /dev/null +++ b/core/src/avm2/function.rs @@ -0,0 +1,663 @@ +//! AVM2 executables. + +use crate::avm2::activation::Activation; +use crate::avm2::class::Class; +use crate::avm2::method::{BytecodeMethod, Method, NativeMethod}; +use crate::avm2::names::{Namespace, QName}; +use crate::avm2::object::{Object, ObjectPtr, TObject}; +use crate::avm2::r#trait::Trait; +use crate::avm2::scope::Scope; +use crate::avm2::script_object::{ScriptObject, ScriptObjectClass, ScriptObjectData}; +use crate::avm2::string::AvmString; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::{Collect, CollectionContext, Gc, GcCell, MutationContext}; +use std::fmt; + +/// Represents code written in AVM2 bytecode that can be executed by some +/// means. +#[derive(Clone, Collect)] +#[collect(no_drop)] +pub struct BytecodeExecutable<'gc> { + /// The method code to execute from a given ABC file. + method: Gc<'gc, BytecodeMethod<'gc>>, + + /// The scope stack to pull variables from. + scope: Option>>, + + /// The reciever that this function is always called with. + /// + /// If `None`, then the reciever provided by the caller is used. A + /// `Some` value indicates a bound executable. + reciever: Option>, +} + +/// Represents code that can be executed by some means. +#[derive(Copy, Clone)] +pub enum Executable<'gc> { + /// Code defined in Ruffle's binary. + /// + /// The second parameter stores the bound reciever for this function. + Native(NativeMethod<'gc>, Option>), + + /// Code defined in a loaded ABC file. + Action(Gc<'gc, BytecodeExecutable<'gc>>), +} + +unsafe impl<'gc> Collect for Executable<'gc> { + fn trace(&self, cc: CollectionContext) { + match self { + Self::Action(be) => be.trace(cc), + Self::Native(_nf, reciever) => reciever.trace(cc), + } + } +} + +impl<'gc> Executable<'gc> { + /// Convert a method into an executable. + pub fn from_method( + method: Method<'gc>, + scope: Option>>, + reciever: Option>, + mc: MutationContext<'gc, '_>, + ) -> Self { + match method { + Method::Native(nf) => Self::Native(nf, reciever), + Method::Entry(method) => Self::Action(Gc::allocate( + mc, + BytecodeExecutable { + method, + scope, + reciever, + }, + )), + } + } + + /// Execute a method. + /// + /// The function will either be called directly if it is a Rust builtin, or + /// executed on the same AVM2 instance as the activation passed in here. + /// The value returned in either case will be provided here. + /// + /// It is a panicing logic error to attempt to execute user code while any + /// reachable object is currently under a GcCell write lock. + pub fn exec( + &self, + unbound_reciever: Option>, + arguments: &[Value<'gc>], + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + base_proto: Option>, + ) -> Result, Error> { + match self { + Executable::Native(nf, reciever) => nf( + activation, + context, + reciever.or(unbound_reciever), + arguments, + ), + Executable::Action(bm) => { + let reciever = bm.reciever.or(unbound_reciever); + let mut activation = Activation::from_method( + activation.avm2(), + context, + bm.method, + bm.scope, + reciever, + arguments, + base_proto, + )?; + + activation.run_actions(bm.method, context) + } + } + } +} + +impl<'gc> fmt::Debug for Executable<'gc> { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Action(be) => fmt + .debug_struct("Executable::Action") + .field("method", &be.method) + .field("scope", &be.scope) + .field("reciever", &be.reciever) + .finish(), + Self::Native(nf, reciever) => fmt + .debug_tuple("Executable::Native") + .field(&format!("{:p}", nf)) + .field(reciever) + .finish(), + } + } +} + +/// An Object which can be called to execute it's function code. +#[derive(Collect, Debug, Clone, Copy)] +#[collect(no_drop)] +pub struct FunctionObject<'gc>(GcCell<'gc, FunctionObjectData<'gc>>); + +#[derive(Collect, Debug, Clone)] +#[collect(no_drop)] +pub struct FunctionObjectData<'gc> { + /// Base script object + base: ScriptObjectData<'gc>, + + /// Executable code + exec: Option>, +} + +impl<'gc> FunctionObject<'gc> { + /// Construct a class. + /// + /// This function returns both the class itself, and the static class + /// initializer method that you should call before interacting with the + /// class. The latter should be called using the former as a reciever. + /// + /// `base_class` is allowed to be `None`, corresponding to a `null` value + /// in the VM. This corresponds to no base class, and in practice appears + /// to be limited to interfaces (at least by the AS3 compiler in Animate + /// CC 2020.) + pub fn from_class( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + class: GcCell<'gc, Class<'gc>>, + base_class: Option>, + scope: Option>>, + ) -> Result<(Object<'gc>, Object<'gc>), Error> { + let class_read = class.read(); + let mut class_proto = if let Some(mut base_class) = base_class { + let super_proto: Result<_, Error> = base_class + .get_property( + base_class, + &QName::new(Namespace::public_namespace(), "prototype"), + activation, + context, + )? + .as_object() + .map_err(|_| { + format!( + "Could not resolve superclass prototype {:?}", + class_read + .super_class_name() + .as_ref() + .map(|p| p.local_name()) + .unwrap_or_else(|| Some("Object".into())) + ) + .into() + }); + + super_proto?.derive(activation, context, class, scope)? + } else { + ScriptObject::bare_object(context.gc_context) + }; + + let mut interfaces = Vec::new(); + let interface_names = class.read().interfaces().to_vec(); + for interface_name in interface_names { + let interface = if let Some(scope) = scope { + scope + .write(context.gc_context) + .resolve(&interface_name, activation, context)? + } else { + None + }; + + if interface.is_none() { + return Err(format!("Could not resolve interface {:?}", interface_name).into()); + } + + let mut interface = interface.unwrap().as_object()?; + let iface_proto = interface + .get_property( + interface, + &QName::new(Namespace::public_namespace(), "prototype"), + activation, + context, + )? + .as_object()?; + + interfaces.push(iface_proto); + } + + if !interfaces.is_empty() { + class_proto.set_interfaces(context.gc_context, interfaces); + } + + let fn_proto = activation.avm2().prototypes().function; + let class_constr_proto = activation.avm2().prototypes().class; + + let initializer = class_read.instance_init(); + + let mut constr: Object<'gc> = FunctionObject(GcCell::allocate( + context.gc_context, + FunctionObjectData { + base: ScriptObjectData::base_new( + Some(fn_proto), + ScriptObjectClass::ClassConstructor(class, scope), + ), + exec: Some(Executable::from_method( + initializer, + scope, + None, + context.gc_context, + )), + }, + )) + .into(); + + constr.install_dynamic_property( + context.gc_context, + QName::new(Namespace::public_namespace(), "prototype"), + class_proto.into(), + )?; + class_proto.install_dynamic_property( + context.gc_context, + QName::new(Namespace::public_namespace(), "constructor"), + constr.into(), + )?; + + let class_initializer = class_read.class_init(); + let class_constr = FunctionObject::from_method( + context.gc_context, + class_initializer, + scope, + class_constr_proto, + None, + ); + + Ok((constr, class_constr)) + } + + /// Construct a function from an ABC method and the current closure scope. + /// + /// The given `reciever`, if supplied, will override any user-specified + /// `this` parameter. + pub fn from_method( + mc: MutationContext<'gc, '_>, + method: Method<'gc>, + scope: Option>>, + fn_proto: Object<'gc>, + reciever: Option>, + ) -> Object<'gc> { + let exec = Some(Executable::from_method(method, scope, reciever, mc)); + + FunctionObject(GcCell::allocate( + mc, + FunctionObjectData { + base: ScriptObjectData::base_new(Some(fn_proto), ScriptObjectClass::NoClass), + exec, + }, + )) + .into() + } + + /// Construct a builtin function object from a Rust function. + pub fn from_builtin( + mc: MutationContext<'gc, '_>, + nf: NativeMethod<'gc>, + fn_proto: Object<'gc>, + ) -> Object<'gc> { + FunctionObject(GcCell::allocate( + mc, + FunctionObjectData { + base: ScriptObjectData::base_new(Some(fn_proto), ScriptObjectClass::NoClass), + exec: Some(Executable::from_method(nf.into(), None, None, mc)), + }, + )) + .into() + } + + /// Construct a builtin type from a Rust constructor and prototype. + pub fn from_builtin_constr( + mc: MutationContext<'gc, '_>, + constr: NativeMethod<'gc>, + mut prototype: Object<'gc>, + fn_proto: Object<'gc>, + ) -> Result, Error> { + let mut base: Object<'gc> = FunctionObject(GcCell::allocate( + mc, + FunctionObjectData { + base: ScriptObjectData::base_new(Some(fn_proto), ScriptObjectClass::NoClass), + exec: Some(Executable::from_method(constr.into(), None, None, mc)), + }, + )) + .into(); + + base.install_dynamic_property( + mc, + QName::new(Namespace::public_namespace(), "prototype"), + prototype.into(), + )?; + prototype.install_dynamic_property( + mc, + QName::new(Namespace::public_namespace(), "constructor"), + base.into(), + )?; + + Ok(base) + } +} + +impl<'gc> TObject<'gc> for FunctionObject<'gc> { + fn get_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let read = self.0.read(); + let rv = read.base.get_property_local(reciever, name, activation)?; + + drop(read); + + rv.resolve(activation, context) + } + + fn set_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + let mut write = self.0.write(context.gc_context); + let rv = write + .base + .set_property_local(reciever, name, value, activation, context)?; + + drop(write); + + rv.resolve(activation, context)?; + + Ok(()) + } + + fn init_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + let mut write = self.0.write(context.gc_context); + let rv = write + .base + .init_property_local(reciever, name, value, activation, context)?; + + drop(write); + + rv.resolve(activation, context)?; + + Ok(()) + } + + fn is_property_overwritable( + self, + gc_context: MutationContext<'gc, '_>, + name: &QName<'gc>, + ) -> bool { + self.0.write(gc_context).base.is_property_overwritable(name) + } + + fn delete_property( + &self, + gc_context: MutationContext<'gc, '_>, + multiname: &QName<'gc>, + ) -> bool { + self.0.write(gc_context).base.delete_property(multiname) + } + + fn get_slot(self, id: u32) -> Result, Error> { + self.0.read().base.get_slot(id) + } + + fn set_slot( + self, + id: u32, + value: Value<'gc>, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + self.0.write(mc).base.set_slot(id, value, mc) + } + + fn init_slot( + self, + id: u32, + value: Value<'gc>, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + self.0.write(mc).base.init_slot(id, value, mc) + } + + fn get_method(self, id: u32) -> Option> { + self.0.read().base.get_method(id) + } + + fn get_trait(self, name: &QName<'gc>) -> Result>, Error> { + self.0.read().base.get_trait(name) + } + + fn get_provided_trait( + &self, + name: &QName<'gc>, + known_traits: &mut Vec>, + ) -> Result<(), Error> { + self.0.read().base.get_provided_trait(name, known_traits) + } + + fn get_scope(self) -> Option>> { + self.0.read().base.get_scope() + } + + fn resolve_any(self, local_name: AvmString<'gc>) -> Result>, Error> { + self.0.read().base.resolve_any(local_name) + } + + fn resolve_any_trait( + self, + local_name: AvmString<'gc>, + ) -> Result>, Error> { + self.0.read().base.resolve_any_trait(local_name) + } + + fn has_own_property(self, name: &QName<'gc>) -> Result { + self.0.read().base.has_own_property(name) + } + + fn has_trait(self, name: &QName<'gc>) -> Result { + self.0.read().base.has_trait(name) + } + + fn provides_trait(self, name: &QName<'gc>) -> Result { + self.0.read().base.provides_trait(name) + } + + fn has_instantiated_property(self, name: &QName<'gc>) -> bool { + self.0.read().base.has_instantiated_property(name) + } + + fn has_own_virtual_getter(self, name: &QName<'gc>) -> bool { + self.0.read().base.has_own_virtual_getter(name) + } + + fn has_own_virtual_setter(self, name: &QName<'gc>) -> bool { + self.0.read().base.has_own_virtual_setter(name) + } + + fn proto(&self) -> Option> { + self.0.read().base.proto() + } + + fn get_enumerant_name(&self, index: u32) -> Option> { + self.0.read().base.get_enumerant_name(index) + } + + fn property_is_enumerable(&self, name: &QName<'gc>) -> bool { + self.0.read().base.property_is_enumerable(name) + } + + fn set_local_property_is_enumerable( + &self, + mc: MutationContext<'gc, '_>, + name: &QName<'gc>, + is_enumerable: bool, + ) -> Result<(), Error> { + self.0 + .write(mc) + .base + .set_local_property_is_enumerable(name, is_enumerable) + } + + fn as_ptr(&self) -> *const ObjectPtr { + self.0.as_ptr() as *const ObjectPtr + } + + fn as_executable(&self) -> Option> { + self.0.read().exec + } + + fn call( + self, + reciever: Option>, + arguments: &[Value<'gc>], + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + base_proto: Option>, + ) -> Result, Error> { + if let Some(exec) = &self.0.read().exec { + exec.exec(reciever, arguments, activation, context, base_proto) + } else { + Err("Not a callable function!".into()) + } + } + + fn construct( + &self, + _activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], + ) -> Result, Error> { + let this: Object<'gc> = Object::FunctionObject(*self); + let base = ScriptObjectData::base_new(Some(this), ScriptObjectClass::NoClass); + + Ok(FunctionObject(GcCell::allocate( + context.gc_context, + FunctionObjectData { base, exec: None }, + )) + .into()) + } + + fn derive( + &self, + _activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + class: GcCell<'gc, Class<'gc>>, + scope: Option>>, + ) -> Result, Error> { + let this: Object<'gc> = Object::FunctionObject(*self); + let base = ScriptObjectData::base_new( + Some(this), + ScriptObjectClass::InstancePrototype(class, scope), + ); + + Ok(FunctionObject(GcCell::allocate( + context.gc_context, + FunctionObjectData { base, exec: None }, + )) + .into()) + } + + fn to_string(&self, mc: MutationContext<'gc, '_>) -> Result, Error> { + if let ScriptObjectClass::ClassConstructor(class, ..) = self.0.read().base.class() { + Ok(AvmString::new(mc, format!("[class {}]", class.read().name().local_name())).into()) + } else { + Ok("function Function() {}".into()) + } + } + + fn value_of(&self, _mc: MutationContext<'gc, '_>) -> Result, Error> { + Ok(Value::Object(Object::from(*self))) + } + + fn install_method( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) { + self.0 + .write(mc) + .base + .install_method(name, disp_id, function) + } + + fn install_getter( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error> { + self.0 + .write(mc) + .base + .install_getter(name, disp_id, function) + } + + fn install_setter( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error> { + self.0 + .write(mc) + .base + .install_setter(name, disp_id, function) + } + + fn install_dynamic_property( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + value: Value<'gc>, + ) -> Result<(), Error> { + self.0.write(mc).base.install_dynamic_property(name, value) + } + + fn install_slot( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + id: u32, + value: Value<'gc>, + ) { + self.0.write(mc).base.install_slot(name, id, value) + } + + fn install_const( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + id: u32, + value: Value<'gc>, + ) { + self.0.write(mc).base.install_const(name, id, value) + } + + fn interfaces(&self) -> Vec> { + self.0.read().base.interfaces() + } + + fn set_interfaces(&self, context: MutationContext<'gc, '_>, iface_list: Vec>) { + self.0.write(context).base.set_interfaces(iface_list) + } +} diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs new file mode 100644 index 000000000000..db02b0d6359c --- /dev/null +++ b/core/src/avm2/globals.rs @@ -0,0 +1,219 @@ +//! Global scope built-ins + +use crate::avm2::activation::Activation; +use crate::avm2::function::FunctionObject; +use crate::avm2::method::NativeMethod; +use crate::avm2::names::{Namespace, QName}; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::string::AvmString; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::{Collect, MutationContext}; +use std::f64::NAN; + +mod class; +mod flash; +mod function; +mod object; + +fn trace<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + args: &[Value<'gc>], +) -> Result, Error> { + if let Some(s) = args.get(0) { + log::info!(target: "avm_trace", "{}", s.clone().coerce_string()); + } + + Ok(Value::Undefined) +} + +/// This structure represents all system builtins' prototypes. +#[derive(Clone, Collect)] +#[collect(no_drop)] +pub struct SystemPrototypes<'gc> { + pub object: Object<'gc>, + pub function: Object<'gc>, + pub class: Object<'gc>, +} + +/// Add a free-function builtin to the global scope. +fn function<'gc>( + mc: MutationContext<'gc, '_>, + mut global_scope: Object<'gc>, + package: impl Into>, + name: impl Into>, + nf: NativeMethod<'gc>, + fn_proto: Object<'gc>, +) { + global_scope + .install_dynamic_property( + mc, + QName::new(Namespace::package(package), name), + FunctionObject::from_builtin(mc, nf, fn_proto).into(), + ) + .unwrap() +} + +/// Add a class builtin to the global scope. +fn class<'gc>( + mc: MutationContext<'gc, '_>, + mut global_scope: Object<'gc>, + package: impl Into>, + name: impl Into>, + constr: NativeMethod<'gc>, + proto: Object<'gc>, + fn_proto: Object<'gc>, +) { + global_scope + .install_dynamic_property( + mc, + QName::new(Namespace::package(package), name), + FunctionObject::from_builtin_constr(mc, constr, proto, fn_proto) + .unwrap() + .into(), + ) + .unwrap(); +} + +/// Add a builtin constant to the global scope. +fn constant<'gc>( + mc: MutationContext<'gc, '_>, + mut global_scope: Object<'gc>, + package: impl Into>, + name: impl Into>, + value: Value<'gc>, +) { + global_scope.install_const(mc, QName::new(Namespace::package(package), name), 0, value) +} + +/// Construct a new global scope. +/// +/// This function returns both the global scope object, as well as all builtin +/// prototypes that other parts of the VM will need to use. +pub fn construct_global_scope<'gc>( + mc: MutationContext<'gc, '_>, +) -> (Object<'gc>, SystemPrototypes<'gc>) { + let gs = ScriptObject::bare_object(mc); + + // public / root package + let object_proto = ScriptObject::bare_object(mc); + let fn_proto = function::create_proto(mc, object_proto); + let class_proto = class::create_proto(mc, object_proto, fn_proto); + + object::fill_proto(mc, object_proto, fn_proto); + + class( + mc, + gs, + "", + "Object", + object::constructor, + object_proto, + fn_proto, + ); + class( + mc, + gs, + "", + "Function", + function::constructor, + fn_proto, + fn_proto, + ); + class( + mc, + gs, + "", + "Class", + class::constructor, + class_proto, + fn_proto, + ); + function(mc, gs, "", "trace", trace, fn_proto); + constant(mc, gs, "", "undefined", Value::Undefined); + constant(mc, gs, "", "null", Value::Null); + constant(mc, gs, "", "NaN", NAN.into()); + + // package `flash.events` + let eventdispatcher_proto = + flash::events::eventdispatcher::create_proto(mc, object_proto, fn_proto); + + class( + mc, + gs, + "flash.events", + "EventDispatcher", + flash::events::eventdispatcher::constructor, + eventdispatcher_proto, + fn_proto, + ); + + // package `flash.display` + let displayobject_proto = + flash::display::displayobject::create_proto(mc, eventdispatcher_proto, fn_proto); + let interactiveobject_proto = + flash::display::interactiveobject::create_proto(mc, displayobject_proto, fn_proto); + let displayobjectcontainer_proto = + flash::display::displayobjectcontainer::create_proto(mc, interactiveobject_proto, fn_proto); + let sprite_proto = + flash::display::sprite::create_proto(mc, displayobjectcontainer_proto, fn_proto); + let movieclip_proto = flash::display::movieclip::create_proto(mc, sprite_proto, fn_proto); + + class( + mc, + gs, + "flash.display", + "DisplayObject", + flash::display::displayobject::constructor, + displayobject_proto, + fn_proto, + ); + class( + mc, + gs, + "flash.display", + "InteractiveObject", + flash::display::interactiveobject::constructor, + interactiveobject_proto, + fn_proto, + ); + class( + mc, + gs, + "flash.display", + "DisplayObjectContainer", + flash::display::displayobjectcontainer::constructor, + sprite_proto, + fn_proto, + ); + class( + mc, + gs, + "flash.display", + "Sprite", + flash::display::sprite::constructor, + sprite_proto, + fn_proto, + ); + class( + mc, + gs, + "flash.display", + "MovieClip", + flash::display::movieclip::constructor, + movieclip_proto, + fn_proto, + ); + + let system_prototypes = SystemPrototypes { + object: object_proto, + function: fn_proto, + class: class_proto, + }; + + (gs, system_prototypes) +} diff --git a/core/src/avm2/globals/class.rs b/core/src/avm2/globals/class.rs new file mode 100644 index 000000000000..c738f2403cc2 --- /dev/null +++ b/core/src/avm2/globals/class.rs @@ -0,0 +1,31 @@ +//! `Class` builtin/prototype + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `Class` +/// +/// Notably, you cannot construct new classes this way, so this returns an +/// error. +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Err("Classes cannot be constructed.".into()) +} + +/// Construct `Class.prototype`. +pub fn create_proto<'gc>( + mc: MutationContext<'gc, '_>, + super_proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + ScriptObject::object(mc, super_proto) +} diff --git a/core/src/avm2/globals/flash.rs b/core/src/avm2/globals/flash.rs new file mode 100644 index 000000000000..18e6d2b4e14a --- /dev/null +++ b/core/src/avm2/globals/flash.rs @@ -0,0 +1,4 @@ +//! `flash` namespace + +pub mod display; +pub mod events; diff --git a/core/src/avm2/globals/flash/display.rs b/core/src/avm2/globals/flash/display.rs new file mode 100644 index 000000000000..c0dc34135098 --- /dev/null +++ b/core/src/avm2/globals/flash/display.rs @@ -0,0 +1,7 @@ +//! `flash.display` namespace + +pub mod displayobject; +pub mod displayobjectcontainer; +pub mod interactiveobject; +pub mod movieclip; +pub mod sprite; diff --git a/core/src/avm2/globals/flash/display/displayobject.rs b/core/src/avm2/globals/flash/display/displayobject.rs new file mode 100644 index 000000000000..30278237d059 --- /dev/null +++ b/core/src/avm2/globals/flash/display/displayobject.rs @@ -0,0 +1,29 @@ +//! `flash.display.DisplayObject` builtin/prototype + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `flash.display.DisplayObject`'s constructor. +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Construct `DisplayObject.prototype`. +pub fn create_proto<'gc>( + mc: MutationContext<'gc, '_>, + super_proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + // TODO: Use `StageObject` here. + ScriptObject::object(mc, super_proto) +} diff --git a/core/src/avm2/globals/flash/display/displayobjectcontainer.rs b/core/src/avm2/globals/flash/display/displayobjectcontainer.rs new file mode 100644 index 000000000000..7b09b5c5ac33 --- /dev/null +++ b/core/src/avm2/globals/flash/display/displayobjectcontainer.rs @@ -0,0 +1,29 @@ +//! `flash.display.DisplayObjectContainer` builtin/prototype + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `flash.display.DisplayObjectContainer`'s constructor. +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Construct `DisplayObjectContainer.prototype`. +pub fn create_proto<'gc>( + mc: MutationContext<'gc, '_>, + super_proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + // TODO: Use `StageObject` here. + ScriptObject::object(mc, super_proto) +} diff --git a/core/src/avm2/globals/flash/display/interactiveobject.rs b/core/src/avm2/globals/flash/display/interactiveobject.rs new file mode 100644 index 000000000000..8fc2844fd620 --- /dev/null +++ b/core/src/avm2/globals/flash/display/interactiveobject.rs @@ -0,0 +1,29 @@ +//! `flash.display.InteractiveObject` builtin/prototype + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `flash.display.InteractiveObject`'s constructor. +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Construct `InteractiveObject.prototype`. +pub fn create_proto<'gc>( + mc: MutationContext<'gc, '_>, + super_proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + // TODO: Use `StageObject` here. + ScriptObject::object(mc, super_proto) +} diff --git a/core/src/avm2/globals/flash/display/movieclip.rs b/core/src/avm2/globals/flash/display/movieclip.rs new file mode 100644 index 000000000000..1a97f8ea4741 --- /dev/null +++ b/core/src/avm2/globals/flash/display/movieclip.rs @@ -0,0 +1,29 @@ +//! `flash.display.MovieClip` builtin/prototype + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `flash.display.MovieClip`'s constructor. +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Construct `MovieClip.prototype`. +pub fn create_proto<'gc>( + mc: MutationContext<'gc, '_>, + super_proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + // TODO: Use `StageObject` here. + ScriptObject::object(mc, super_proto) +} diff --git a/core/src/avm2/globals/flash/display/sprite.rs b/core/src/avm2/globals/flash/display/sprite.rs new file mode 100644 index 000000000000..8763921d627b --- /dev/null +++ b/core/src/avm2/globals/flash/display/sprite.rs @@ -0,0 +1,29 @@ +//! `flash.display.Sprite` builtin/prototype + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `flash.display.Sprite`'s constructor. +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Construct `Sprite.prototype`. +pub fn create_proto<'gc>( + mc: MutationContext<'gc, '_>, + super_proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + // TODO: Use `StageObject` here. + ScriptObject::object(mc, super_proto) +} diff --git a/core/src/avm2/globals/flash/events.rs b/core/src/avm2/globals/flash/events.rs new file mode 100644 index 000000000000..afd4e106bae3 --- /dev/null +++ b/core/src/avm2/globals/flash/events.rs @@ -0,0 +1,3 @@ +//! `flash.events` namespace + +pub mod eventdispatcher; diff --git a/core/src/avm2/globals/flash/events/eventdispatcher.rs b/core/src/avm2/globals/flash/events/eventdispatcher.rs new file mode 100644 index 000000000000..172a8dddac01 --- /dev/null +++ b/core/src/avm2/globals/flash/events/eventdispatcher.rs @@ -0,0 +1,29 @@ +//! `flash.events.EventDispatcher` builtin/prototype + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `flash.events.EventDispatcher`'s constructor. +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Construct `EventDispatcher.prototype`. +pub fn create_proto<'gc>( + mc: MutationContext<'gc, '_>, + super_proto: Object<'gc>, + _fn_proto: Object<'gc>, +) -> Object<'gc> { + // TODO: Use `StageObject` here. + ScriptObject::object(mc, super_proto) +} diff --git a/core/src/avm2/globals/function.rs b/core/src/avm2/globals/function.rs new file mode 100644 index 000000000000..7f9ffc8ffe88 --- /dev/null +++ b/core/src/avm2/globals/function.rs @@ -0,0 +1,62 @@ +//! Function builtin and prototype + +use crate::avm2::activation::Activation; +use crate::avm2::function::FunctionObject; +use crate::avm2::names::{Namespace, QName}; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `Function` +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _action_context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Implements `Function.prototype.call` +fn call<'gc>( + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + func: Option>, + args: &[Value<'gc>], +) -> Result, Error> { + let this = args.get(0).and_then(|v| v.as_object().ok()); + let base_proto = this.and_then(|that| that.proto()); + + if let Some(func) = func { + if args.len() > 1 { + Ok(func.call(this, &args[1..], activation, context, base_proto)?) + } else { + Ok(func.call(this, &[], activation, context, base_proto)?) + } + } else { + Err("Not a callable function".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: Object<'gc>) -> Object<'gc> { + let mut function_proto = ScriptObject::object(gc_context, proto); + + function_proto.install_method( + gc_context, + QName::new(Namespace::as3_namespace(), "call"), + 0, + FunctionObject::from_builtin(gc_context, call, function_proto), + ); + + function_proto +} diff --git a/core/src/avm2/globals/object.rs b/core/src/avm2/globals/object.rs new file mode 100644 index 000000000000..45d0504d1ee1 --- /dev/null +++ b/core/src/avm2/globals/object.rs @@ -0,0 +1,208 @@ +//! Object builtin and prototype + +use crate::avm2::activation::Activation; +use crate::avm2::function::FunctionObject; +use crate::avm2::names::{Namespace, QName}; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::MutationContext; + +/// Implements `Object` +pub fn constructor<'gc>( + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _this: Option>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(Value::Undefined) +} + +/// Implements `Object.prototype.toString` +fn to_string<'gc>( + _: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Option>, + _: &[Value<'gc>], +) -> Result, Error> { + Ok(this + .map(|t| t.to_string(context.gc_context)) + .unwrap_or(Ok(Value::Undefined))?) +} + +/// Implements `Object.prototype.toLocaleString` +fn to_locale_string<'gc>( + _: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Option>, + _: &[Value<'gc>], +) -> Result, Error> { + Ok(this + .map(|t| t.to_string(context.gc_context)) + .unwrap_or(Ok(Value::Undefined))?) +} + +/// Implements `Object.prototype.valueOf` +fn value_of<'gc>( + _: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Option>, + _: &[Value<'gc>], +) -> Result, Error> { + Ok(this + .map(|t| t.value_of(context.gc_context)) + .unwrap_or(Ok(Value::Undefined))?) +} + +/// `Object.prototype.hasOwnProperty` +pub fn has_own_property<'gc>( + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + this: Option>, + args: &[Value<'gc>], +) -> Result, Error> { + let this: Result, Error> = this.ok_or_else(|| "No valid this parameter".into()); + let this = this?; + let name: Result<&Value<'gc>, Error> = args.get(0).ok_or_else(|| "No name specified".into()); + let name = name?.as_string()?; + + if let Some(ns) = this.resolve_any(name)? { + if !ns.is_private() { + let qname = QName::new(ns, name); + return Ok(this.has_own_property(&qname)?.into()); + } + } + + Ok(false.into()) +} + +/// `Object.prototype.isPrototypeOf` +pub fn is_prototype_of<'gc>( + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + this: Option>, + args: &[Value<'gc>], +) -> Result, Error> { + let search_proto: Result, Error> = + this.ok_or_else(|| "No valid this parameter".into()); + let search_proto = search_proto?; + let mut target_proto = args.get(0).cloned().unwrap_or(Value::Undefined); + + while let Value::Object(proto) = target_proto { + if Object::ptr_eq(search_proto, proto) { + return Ok(true.into()); + } + + target_proto = proto.proto().map(|o| o.into()).unwrap_or(Value::Undefined); + } + + Ok(false.into()) +} + +/// `Object.prototype.propertyIsEnumerable` +pub fn property_is_enumerable<'gc>( + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + this: Option>, + args: &[Value<'gc>], +) -> Result, Error> { + let this: Result, Error> = this.ok_or_else(|| "No valid this parameter".into()); + let this = this?; + let name: Result<&Value<'gc>, Error> = args.get(0).ok_or_else(|| "No name specified".into()); + let name = name?.as_string()?; + + if let Some(ns) = this.resolve_any(name)? { + if !ns.is_private() { + let qname = QName::new(ns, name); + return Ok(this.property_is_enumerable(&qname).into()); + } + } + + Ok(false.into()) +} + +/// `Object.prototype.setPropertyIsEnumerable` +pub fn set_property_is_enumerable<'gc>( + _activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Option>, + args: &[Value<'gc>], +) -> Result, Error> { + let this: Result, Error> = this.ok_or_else(|| "No valid this parameter".into()); + let this = this?; + let name: Result<&Value<'gc>, Error> = args.get(0).ok_or_else(|| "No name specified".into()); + let name = name?.as_string()?; + let is_enum = args + .get(1) + .cloned() + .unwrap_or(Value::Bool(true)) + .as_bool()?; + + if let Some(ns) = this.resolve_any(name)? { + if !ns.is_private() { + let qname = QName::new(ns, name); + this.set_local_property_is_enumerable(context.gc_context, &qname, is_enum)?; + } + } + + Ok(Value::Undefined) +} + +/// 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, '_>, + mut object_proto: Object<'gc>, + fn_proto: Object<'gc>, +) { + object_proto.install_method( + gc_context, + QName::new(Namespace::public_namespace(), "toString"), + 0, + FunctionObject::from_builtin(gc_context, to_string, fn_proto), + ); + object_proto.install_method( + gc_context, + QName::new(Namespace::public_namespace(), "toLocaleString"), + 0, + FunctionObject::from_builtin(gc_context, to_locale_string, fn_proto), + ); + object_proto.install_method( + gc_context, + QName::new(Namespace::public_namespace(), "valueOf"), + 0, + FunctionObject::from_builtin(gc_context, value_of, fn_proto), + ); + object_proto.install_method( + gc_context, + QName::new(Namespace::as3_namespace(), "hasOwnProperty"), + 0, + FunctionObject::from_builtin(gc_context, has_own_property, fn_proto), + ); + object_proto.install_method( + gc_context, + QName::new(Namespace::as3_namespace(), "isPrototypeOf"), + 0, + FunctionObject::from_builtin(gc_context, is_prototype_of, fn_proto), + ); + object_proto.install_method( + gc_context, + QName::new(Namespace::as3_namespace(), "propertyIsEnumerable"), + 0, + FunctionObject::from_builtin(gc_context, property_is_enumerable, fn_proto), + ); + object_proto.install_method( + gc_context, + QName::new(Namespace::public_namespace(), "setPropertyIsEnumerable"), + 0, + FunctionObject::from_builtin(gc_context, set_property_is_enumerable, fn_proto), + ); +} diff --git a/core/src/avm2/method.rs b/core/src/avm2/method.rs new file mode 100644 index 000000000000..b0daccdb98a3 --- /dev/null +++ b/core/src/avm2/method.rs @@ -0,0 +1,178 @@ +//! AVM2 methods + +use crate::avm2::activation::Activation; +use crate::avm2::object::Object; +use crate::avm2::script::TranslationUnit; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::{Collect, CollectionContext, Gc, MutationContext}; +use std::fmt; +use std::rc::Rc; +use swf::avm2::types::{AbcFile, Index, Method as AbcMethod, MethodBody as AbcMethodBody}; + +#[derive(Clone, Debug, Collect)] +#[collect(require_static)] +pub struct CollectWrapper(T); + +/// Represents a function defined in Ruffle's code. +/// +/// Parameters are as follows: +/// +/// * The AVM2 runtime +/// * The action context +/// * The current `this` object +/// * The arguments this function was called with +/// +/// Native functions are allowed to return a value or `None`. `None` indicates +/// that the given value will not be returned on the stack and instead will +/// resolve on the AVM stack, as if you had called a non-native function. If +/// your function yields `None`, you must ensure that the top-most activation +/// in the AVM1 runtime will return with the value of this function. +pub type NativeMethod<'gc> = fn( + &mut Activation<'_, 'gc>, + &mut UpdateContext<'_, 'gc, '_>, + Option>, + &[Value<'gc>], +) -> Result, Error>; + +/// Represents a reference to an AVM2 method and body. +#[derive(Collect, Clone, Debug)] +#[collect(no_drop)] +pub struct BytecodeMethod<'gc> { + /// The translation unit this function was defined in. + pub txunit: TranslationUnit<'gc>, + + /// The underlying ABC file of the above translation unit. + pub abc: CollectWrapper>, + + /// The ABC method this function uses. + pub abc_method: u32, + + /// The ABC method body this function uses. + pub abc_method_body: Option, +} + +impl<'gc> BytecodeMethod<'gc> { + /// Construct an `BytecodeMethod` from an `AbcFile` and method index. + /// + /// The method body index will be determined by searching through the ABC + /// for a matching method. If none exists, this function returns `None`. + pub fn from_method_index( + txunit: TranslationUnit<'gc>, + abc_method: Index, + mc: MutationContext<'gc, '_>, + ) -> Option> { + let abc = txunit.abc(); + + if abc.methods.get(abc_method.0 as usize).is_some() { + for (index, method_body) in abc.method_bodies.iter().enumerate() { + if method_body.method.0 == abc_method.0 { + return Some(Gc::allocate( + mc, + Self { + txunit, + abc: CollectWrapper(txunit.abc()), + abc_method: abc_method.0, + abc_method_body: Some(index as u32), + }, + )); + } + } + } + + Some(Gc::allocate( + mc, + Self { + txunit, + abc: CollectWrapper(txunit.abc()), + abc_method: abc_method.0, + abc_method_body: None, + }, + )) + } + + /// Get the underlying ABC file. + pub fn abc(&self) -> Rc { + self.txunit.abc() + } + + /// Get the underlying translation unit this method was defined in. + pub fn translation_unit(&self) -> TranslationUnit<'gc> { + self.txunit + } + + /// Get a reference to the ABC method entry this refers to. + pub fn method(&self) -> &AbcMethod { + &self.abc.0.methods.get(self.abc_method as usize).unwrap() + } + + /// Get a reference to the ABC method body entry this refers to. + /// + /// Some methods do not have bodies; this returns `None` in that case. + pub fn body(&self) -> Option<&AbcMethodBody> { + if let Some(abc_method_body) = self.abc_method_body { + self.abc.0.method_bodies.get(abc_method_body as usize) + } else { + None + } + } +} + +/// An uninstantiated method that can either be natively implemented or sourced +/// from an ABC file. +#[derive(Clone)] +pub enum Method<'gc> { + /// A native method. + Native(NativeMethod<'gc>), + + /// An ABC-provided method entry. + Entry(Gc<'gc, BytecodeMethod<'gc>>), +} + +unsafe impl<'gc> Collect for Method<'gc> { + fn trace(&self, cc: CollectionContext) { + match self { + Method::Native(_nf) => {} + Method::Entry(entry) => entry.trace(cc), + } + } +} + +impl<'gc> fmt::Debug for Method<'gc> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Method::Native(_nf) => f + .debug_tuple("Method::Native") + .field(&"".to_string()) + .finish(), + Method::Entry(entry) => f.debug_tuple("Method::Entry").field(entry).finish(), + } + } +} + +impl<'gc> From> for Method<'gc> { + fn from(nf: NativeMethod<'gc>) -> Self { + Self::Native(nf) + } +} + +impl<'gc> From>> for Method<'gc> { + fn from(bm: Gc<'gc, BytecodeMethod<'gc>>) -> Self { + Self::Entry(bm) + } +} + +impl<'gc> Method<'gc> { + /// Access the bytecode of this method. + /// + /// This function returns `Err` if there is no bytecode for this method. + pub fn into_bytecode(self) -> Result>, Error> { + match self { + Method::Native(_) => { + Err("Attempted to unwrap a native method as a user-defined one".into()) + } + Method::Entry(bm) => Ok(bm), + } + } +} diff --git a/core/src/avm2/names.rs b/core/src/avm2/names.rs new file mode 100644 index 000000000000..6595dc66df0f --- /dev/null +++ b/core/src/avm2/names.rs @@ -0,0 +1,339 @@ +//! AVM2 names & namespacing + +use crate::avm2::script::TranslationUnit; +use crate::avm2::string::AvmString; +use crate::avm2::{Avm2, Error}; +use gc_arena::{Collect, MutationContext}; +use swf::avm2::types::{ + Index, Multiname as AbcMultiname, Namespace as AbcNamespace, NamespaceSet as AbcNamespaceSet, +}; + +/// Represents the name of a namespace. +#[derive(Clone, Collect, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[collect(no_drop)] +pub enum Namespace<'gc> { + Namespace(AvmString<'gc>), + Package(AvmString<'gc>), + PackageInternal(AvmString<'gc>), + Protected(AvmString<'gc>), + Explicit(AvmString<'gc>), + StaticProtected(AvmString<'gc>), + Private(AvmString<'gc>), + Any, +} + +impl<'gc> Namespace<'gc> { + /// Read a namespace declaration from the ABC constant pool and copy it to + /// a namespace value. + pub fn from_abc_namespace( + translation_unit: TranslationUnit<'gc>, + namespace_index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result { + if namespace_index.0 == 0 { + return Ok(Self::Any); + } + + let actual_index = namespace_index.0 as usize - 1; + let abc = translation_unit.abc(); + let abc_namespace: Result<_, Error> = abc + .constant_pool + .namespaces + .get(actual_index) + .ok_or_else(|| format!("Unknown namespace constant {}", namespace_index.0).into()); + + Ok(match abc_namespace? { + AbcNamespace::Namespace(idx) => { + Self::Namespace(translation_unit.pool_string(idx.0, mc)?) + } + AbcNamespace::Package(idx) => Self::Package(translation_unit.pool_string(idx.0, mc)?), + AbcNamespace::PackageInternal(idx) => { + Self::PackageInternal(translation_unit.pool_string(idx.0, mc)?) + } + AbcNamespace::Protected(idx) => { + Self::Protected(translation_unit.pool_string(idx.0, mc)?) + } + AbcNamespace::Explicit(idx) => Self::Explicit(translation_unit.pool_string(idx.0, mc)?), + AbcNamespace::StaticProtected(idx) => { + Self::StaticProtected(translation_unit.pool_string(idx.0, mc)?) + } + AbcNamespace::Private(idx) => Self::Private(translation_unit.pool_string(idx.0, mc)?), + }) + } + + pub fn public_namespace() -> Self { + Namespace::Package("".into()) + } + + pub fn as3_namespace() -> Self { + Namespace::Namespace("http://adobe.com/AS3/2006/builtin".into()) + } + + pub fn package(package_name: impl Into>) -> Self { + Namespace::Package(package_name.into()) + } + + pub fn is_any(&self) -> bool { + match self { + Self::Any => true, + _ => false, + } + } + + pub fn is_private(&self) -> bool { + match self { + Self::Private(_) => true, + _ => false, + } + } +} + +/// A `QName`, likely "qualified name", consists of a namespace and name string. +/// +/// This is technically interchangeable with `xml::XMLName`, as they both +/// implement `QName`; however, AVM2 and XML have separate representations. +/// +/// A property cannot be retrieved or set without first being resolved into a +/// `QName`. All other forms of names and multinames are either versions of +/// `QName` with unspecified parameters, or multiple names to be checked in +/// order. +#[derive(Clone, Collect, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[collect(no_drop)] +pub struct QName<'gc> { + ns: Namespace<'gc>, + name: AvmString<'gc>, +} + +impl<'gc> QName<'gc> { + pub fn new(ns: Namespace<'gc>, name: impl Into>) -> Self { + Self { + ns, + name: name.into(), + } + } + + pub fn dynamic_name(local_part: impl Into>) -> Self { + Self { + ns: Namespace::public_namespace(), + name: local_part.into(), + } + } + + /// Pull a `QName` from the multiname pool. + /// + /// This function returns an Err if the multiname does not exist or is not + /// a `QName`. + pub fn from_abc_multiname( + translation_unit: TranslationUnit<'gc>, + multiname_index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result { + if multiname_index.0 == 0 { + return Err("Attempted to load a trait name of index zero".into()); + } + + let actual_index = multiname_index.0 as usize - 1; + let abc = translation_unit.abc(); + let abc_multiname: Result<_, Error> = abc + .constant_pool + .multinames + .get(actual_index) + .ok_or_else(|| format!("Unknown multiname constant {}", multiname_index.0).into()); + + Ok(match abc_multiname? { + AbcMultiname::QName { namespace, name } => Self { + ns: Namespace::from_abc_namespace(translation_unit, namespace.clone(), mc)?, + name: translation_unit.pool_string(name.0, mc)?, + }, + _ => return Err("Attempted to pull QName from non-QName multiname".into()), + }) + } + + pub fn local_name(&self) -> AvmString<'gc> { + self.name + } + + pub fn namespace(&self) -> &Namespace<'gc> { + &self.ns + } +} + +/// A `Multiname` consists of a name which could be resolved in one or more +/// potential namespaces. +/// +/// All unresolved names are of the form `Multiname`, and the name resolution +/// process consists of searching each name space for a given name. +/// +/// The existence of a `name` of `None` indicates the `Any` name. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct Multiname<'gc> { + ns: Vec>, + name: Option>, +} + +impl<'gc> Multiname<'gc> { + /// Read a namespace set from the ABC constant pool, and return a list of + /// copied namespaces. + fn abc_namespace_set( + translation_unit: TranslationUnit<'gc>, + namespace_set_index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result>, Error> { + if namespace_set_index.0 == 0 { + //TODO: What is namespace set zero? + return Ok(vec![]); + } + + let actual_index = namespace_set_index.0 as usize - 1; + let abc = translation_unit.abc(); + let ns_set: Result<_, Error> = abc + .constant_pool + .namespace_sets + .get(actual_index) + .ok_or_else(|| { + format!("Unknown namespace set constant {}", namespace_set_index.0).into() + }); + let mut result = vec![]; + + for ns in ns_set? { + result.push(Namespace::from_abc_namespace( + translation_unit, + ns.clone(), + mc, + )?) + } + + Ok(result) + } + + /// Read a multiname from the ABC constant pool, copying it into the most + /// general form of multiname. + pub fn from_abc_multiname( + translation_unit: TranslationUnit<'gc>, + multiname_index: Index, + avm: &mut Avm2<'gc>, + mc: MutationContext<'gc, '_>, + ) -> Result { + let actual_index: Result = (multiname_index.0 as usize) + .checked_sub(1) + .ok_or_else(|| "Attempted to resolve a multiname at index zero. This is a bug.".into()); + let actual_index = actual_index?; + let abc = translation_unit.abc(); + let abc_multiname: Result<_, Error> = abc + .constant_pool + .multinames + .get(actual_index) + .ok_or_else(|| format!("Unknown multiname constant {}", multiname_index.0).into()); + + Ok(match abc_multiname? { + AbcMultiname::QName { namespace, name } | AbcMultiname::QNameA { namespace, name } => { + Self { + ns: vec![Namespace::from_abc_namespace( + translation_unit, + namespace.clone(), + mc, + )?], + name: translation_unit.pool_string_option(name.0, mc)?, + } + } + AbcMultiname::RTQName { name } | AbcMultiname::RTQNameA { name } => { + let ns = avm.pop().as_namespace()?.clone(); + Self { + ns: vec![ns], + name: translation_unit.pool_string_option(name.0, mc)?, + } + } + AbcMultiname::RTQNameL | AbcMultiname::RTQNameLA => { + let ns = avm.pop().as_namespace()?.clone(); + let name = avm.pop().as_string()?; + Self { + ns: vec![ns], + name: Some(name), + } + } + AbcMultiname::Multiname { + namespace_set, + name, + } + | AbcMultiname::MultinameA { + namespace_set, + name, + } => Self { + ns: Self::abc_namespace_set(translation_unit, namespace_set.clone(), mc)?, + name: translation_unit.pool_string_option(name.0, mc)?, + }, + AbcMultiname::MultinameL { namespace_set } + | AbcMultiname::MultinameLA { namespace_set } => { + let name = avm.pop().as_string()?; + Self { + ns: Self::abc_namespace_set(translation_unit, namespace_set.clone(), mc)?, + name: Some(name), + } + } + }) + } + + /// Read a static multiname from the ABC constant pool + /// + /// This function prohibits the use of runtime-qualified and late-bound + /// names. Runtime multinames will instead result in an error. + pub fn from_abc_multiname_static( + translation_unit: TranslationUnit<'gc>, + multiname_index: Index, + mc: MutationContext<'gc, '_>, + ) -> Result { + let actual_index: Result = + (multiname_index.0 as usize).checked_sub(1).ok_or_else(|| { + "Attempted to resolve a (static) multiname at index zero. This is a bug.".into() + }); + let actual_index = actual_index?; + let abc = translation_unit.abc(); + let abc_multiname: Result<_, Error> = abc + .constant_pool + .multinames + .get(actual_index) + .ok_or_else(|| format!("Unknown multiname constant {}", multiname_index.0).into()); + + Ok(match abc_multiname? { + AbcMultiname::QName { namespace, name } | AbcMultiname::QNameA { namespace, name } => { + Self { + ns: vec![Namespace::from_abc_namespace( + translation_unit, + namespace.clone(), + mc, + )?], + name: translation_unit.pool_string_option(name.0, mc)?, + } + } + AbcMultiname::Multiname { + namespace_set, + name, + } + | AbcMultiname::MultinameA { + namespace_set, + name, + } => Self { + ns: Self::abc_namespace_set(translation_unit, namespace_set.clone(), mc)?, + name: translation_unit.pool_string_option(name.0, mc)?, + }, + _ => return Err(format!("Multiname {} is not static", multiname_index.0).into()), + }) + } + + /// Indicates the any type (any name in any namespace). + pub fn any() -> Self { + Self { + ns: vec![Namespace::Any], + name: None, + } + } + + pub fn namespace_set(&self) -> impl Iterator> { + self.ns.iter() + } + + pub fn local_name(&self) -> Option> { + self.name + } +} diff --git a/core/src/avm2/object.rs b/core/src/avm2/object.rs new file mode 100644 index 000000000000..46f6a38ccde0 --- /dev/null +++ b/core/src/avm2/object.rs @@ -0,0 +1,690 @@ +//! AVM2 objects. + +use crate::avm2::activation::Activation; +use crate::avm2::class::Class; +use crate::avm2::function::{Executable, FunctionObject}; +use crate::avm2::names::{Multiname, Namespace, QName}; +use crate::avm2::r#trait::{Trait, TraitKind}; +use crate::avm2::scope::Scope; +use crate::avm2::script_object::ScriptObject; +use crate::avm2::string::AvmString; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::{Collect, GcCell, MutationContext}; +use ruffle_macros::enum_trait_object; +use std::fmt::Debug; + +/// Represents an object that can be directly interacted with by the AVM2 +/// runtime. +#[enum_trait_object( + #[derive(Clone, Collect, Debug, Copy)] + #[collect(no_drop)] + pub enum Object<'gc> { + ScriptObject(ScriptObject<'gc>), + FunctionObject(FunctionObject<'gc>) + } +)] +pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy { + /// Retrieve a property by it's QName, without taking prototype lookups + /// into account. + fn get_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error>; + + /// Retrieve a property by it's QName. + fn get_property( + &mut self, + reciever: Object<'gc>, + name: &QName<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + if !self.has_instantiated_property(name) { + for abc_trait in self.get_trait(name)? { + self.install_trait(activation, context, abc_trait, reciever)?; + } + } + + let has_no_getter = self.has_own_virtual_setter(name) && !self.has_own_virtual_getter(name); + + if self.has_own_property(name)? && !has_no_getter { + return self.get_property_local(reciever, name, activation, context); + } + + if let Some(mut proto) = self.proto() { + return proto.get_property(reciever, name, activation, context); + } + + Ok(Value::Undefined) + } + + /// Retrieve the base prototype that a particular QName trait is defined in. + /// + /// This function returns `None` for non-trait properties, such as actually + /// defined prototype methods for ES3-style classes. + fn get_base_proto(self, name: &QName<'gc>) -> Result>, Error> { + if self.provides_trait(name)? { + return Ok(Some(self.into())); + } + + if let Some(proto) = self.proto() { + return proto.get_base_proto(name); + } + + Ok(None) + } + + /// Set a property on this specific object. + fn set_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error>; + + /// Set a property by it's QName. + fn set_property( + &mut self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + if !self.has_instantiated_property(name) { + for abc_trait in self.get_trait(name)? { + self.install_trait(activation, context, abc_trait, reciever)?; + } + } + + if self.has_own_virtual_setter(name) { + return self.set_property_local(reciever, name, value, activation, context); + } + + let mut proto = self.proto(); + while let Some(mut my_proto) = proto { + //NOTE: This only works because we validate ahead-of-time that + //we're calling a virtual setter. If you call `set_property` on + //a non-virtual you will actually alter the prototype. + if my_proto.has_own_virtual_setter(name) { + return my_proto.set_property(reciever, name, value, activation, context); + } + + proto = my_proto.proto(); + } + + reciever.set_property_local(reciever, name, value, activation, context) + } + + /// Init a property on this specific object. + fn init_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error>; + + /// Init a property by it's QName. + fn init_property( + &mut self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + if !self.has_instantiated_property(name) { + for abc_trait in self.get_trait(name)? { + self.install_trait(activation, context, abc_trait, reciever)?; + } + } + + if self.has_own_virtual_setter(name) { + return self.init_property_local(reciever, name, value, activation, context); + } + + let mut proto = self.proto(); + while let Some(mut my_proto) = proto { + //NOTE: This only works because we validate ahead-of-time that + //we're calling a virtual setter. If you call `set_property` on + //a non-virtual you will actually alter the prototype. + if my_proto.has_own_virtual_setter(name) { + return my_proto.init_property(reciever, name, value, activation, context); + } + + proto = my_proto.proto(); + } + + reciever.init_property_local(reciever, name, value, activation, context) + } + + /// Retrieve a slot by it's index. + fn get_slot(self, id: u32) -> Result, Error>; + + /// Set a slot by it's index. + fn set_slot( + self, + id: u32, + value: Value<'gc>, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error>; + + /// Initialize a slot by it's index. + fn init_slot( + self, + id: u32, + value: Value<'gc>, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error>; + + /// Retrieve a method by it's index. + fn get_method(self, id: u32) -> Option>; + + /// Retrieves a trait entry by name. + /// + /// This function returns `None` if no such trait exists, or the object + /// does not have traits. It returns `Err` if *any* trait in the object is + /// malformed in some way. + fn get_trait(self, name: &QName<'gc>) -> Result>, Error>; + + /// Populate a list of traits that this object provides. + /// + /// This function yields traits for class constructors and prototypes, but + /// not instances. For resolving traits for normal `TObject` methods, use + /// `get_trait` and `has_trait` as it will tell you if the current object + /// has a given trait. + fn get_provided_trait( + &self, + name: &QName<'gc>, + known_traits: &mut Vec>, + ) -> Result<(), Error>; + + /// Retrieves the scope chain of the object at time of it's creation. + /// + /// The scope chain is used to determine the starting scope stack when an + /// object is called, as well as any class methods on the object. + /// Non-method functions and prototype functions (ES3 methods) do not use + /// this scope chain. + fn get_scope(self) -> Option>>; + + /// Resolve a multiname into a single QName, if any of the namespaces + /// match. + fn resolve_multiname(self, multiname: &Multiname<'gc>) -> Result>, Error> { + for ns in multiname.namespace_set() { + if ns.is_any() { + if let Some(name) = multiname.local_name() { + let ns = self.resolve_any(name)?; + return Ok(ns.map(|ns| QName::new(ns, name))); + } else { + return Ok(None); + } + } else if let Some(name) = multiname.local_name() { + let qname = QName::new(ns.clone(), name); + if self.has_property(&qname)? { + return Ok(Some(qname)); + } + } else { + return Ok(None); + } + } + + if let Some(proto) = self.proto() { + return Ok(proto.resolve_multiname(multiname)?); + } + + Ok(None) + } + + /// Given a local name, find the namespace it resides in, if any. + /// + /// The `Namespace` must not be `Namespace::Any`, as this function exists + /// specifically resolve names in that namespace. + /// + /// Trait names will be resolve on class constructors and object instances, + /// but not prototypes. If you want to search a prototype's provided traits + /// you must walk the prototype chain using `resolve_any_trait`. + fn resolve_any(self, local_name: AvmString<'gc>) -> Result>, Error>; + + /// Given a local name of a trait, find the namespace it resides in, if any. + /// + /// This function only works for names which are trait properties, not + /// dynamic or prototype properties. Furthermore, instance prototypes *will* + /// resolve trait names here, contrary to their behavior in `resolve_any.` + fn resolve_any_trait(self, local_name: AvmString<'gc>) + -> Result>, Error>; + + /// Indicates whether or not a property exists on an object. + fn has_property(self, name: &QName<'gc>) -> Result { + if self.has_own_property(name)? { + Ok(true) + } else if let Some(proto) = self.proto() { + Ok(proto.has_own_property(name)?) + } else { + Ok(false) + } + } + + /// Indicates whether or not a property or trait exists on an object and is + /// not part of the prototype chain. + fn has_own_property(self, name: &QName<'gc>) -> Result; + + /// Returns true if an object has one or more traits of a given name. + fn has_trait(self, name: &QName<'gc>) -> Result; + + /// Returns true if an object is part of a class that defines a trait of a + /// given name on itself (as opposed to merely inheriting a superclass + /// trait.) + fn provides_trait(self, name: &QName<'gc>) -> Result; + + /// Indicates whether or not a property or *instantiated* trait exists on + /// an object and is not part of the prototype chain. + /// + /// Unlike `has_own_property`, this will not yield `true` for traits this + /// object can have but has not yet instantiated. + fn has_instantiated_property(self, name: &QName<'gc>) -> bool; + + /// Check if a particular object contains a virtual getter by the given + /// name. + fn has_own_virtual_getter(self, name: &QName<'gc>) -> bool; + + /// Check if a particular object contains a virtual setter by the given + /// name. + fn has_own_virtual_setter(self, name: &QName<'gc>) -> bool; + + /// Indicates whether or not a property is overwritable. + fn is_property_overwritable( + self, + gc_context: MutationContext<'gc, '_>, + _name: &QName<'gc>, + ) -> bool; + + /// Delete a named property from the object. + /// + /// Returns false if the property cannot be deleted. + fn delete_property(&self, gc_context: MutationContext<'gc, '_>, name: &QName<'gc>) -> 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>; + + /// Retrieve a given enumerable name by index. + /// + /// Enumerants are listed by index, starting from zero. A value of `None` + /// indicates that no enumerant with that index, or any greater index, + /// exists. (In other words, it means stop.) + /// + /// Objects are responsible for maintaining a consistently ordered and + /// indexed list of enumerable names which can be queried by this + /// mechanism. + fn get_enumerant_name(&self, index: u32) -> Option>; + + /// Determine if a property is currently enumerable. + /// + /// Properties that do not exist are also not enumerable. + fn property_is_enumerable(&self, name: &QName<'gc>) -> bool; + + /// Mark a dynamic property on this object as enumerable. + fn set_local_property_is_enumerable( + &self, + mc: MutationContext<'gc, '_>, + name: &QName<'gc>, + is_enumerable: bool, + ) -> Result<(), Error>; + + /// Install a method (or any other non-slot value) on an object. + fn install_method( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ); + + /// Install a getter method on an object property. + fn install_getter( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error>; + + /// Install a setter method on an object property. + fn install_setter( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error>; + + /// Install a dynamic or built-in value property on an object. + fn install_dynamic_property( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + value: Value<'gc>, + ) -> Result<(), Error>; + + /// Install a slot on an object property. + fn install_slot( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + id: u32, + value: Value<'gc>, + ); + + /// Install a const on an object property. + fn install_const( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + id: u32, + value: Value<'gc>, + ); + + /// Install a trait from the current object. + /// + /// This function should only be called once, as reinstalling a trait may + /// also unset already set properties. It may either be called immediately + /// when the object is instantiated or lazily; this behavior is ostensibly + /// controlled by the `lazy_init` flag provided to `load_abc`, but in + /// practice every version of Flash and Animate uses lazy trait + /// installation. + /// + /// The `reciever` property allows specifying the object that methods are + /// bound to. It should always be `self` except when doing things with + /// `super`, which needs to create bound methods pointing to a different + /// object. + fn install_trait( + &mut self, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + trait_entry: Trait<'gc>, + reciever: Object<'gc>, + ) -> Result<(), Error> { + self.install_foreign_trait(activation, context, trait_entry, self.get_scope(), reciever) + } + + /// Install a trait from anywyere. + fn install_foreign_trait( + &mut self, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + trait_entry: Trait<'gc>, + scope: Option>>, + reciever: Object<'gc>, + ) -> Result<(), Error> { + let fn_proto = activation.avm2().prototypes().function; + let trait_name = trait_entry.name().clone(); + avm_debug!( + "Installing trait {:?} of kind {:?}", + trait_name, + trait_entry.kind() + ); + + match trait_entry.kind() { + TraitKind::Slot { + slot_id, + default_value, + .. + } => { + self.install_slot( + context.gc_context, + trait_name, + *slot_id, + default_value.clone().unwrap_or(Value::Undefined), + ); + } + TraitKind::Method { + disp_id, method, .. + } => { + let function = FunctionObject::from_method( + context.gc_context, + method.clone(), + scope, + fn_proto, + Some(reciever), + ); + self.install_method(context.gc_context, trait_name, *disp_id, function); + } + TraitKind::Getter { + disp_id, method, .. + } => { + let function = FunctionObject::from_method( + context.gc_context, + method.clone(), + scope, + fn_proto, + Some(reciever), + ); + self.install_getter(context.gc_context, trait_name, *disp_id, function)?; + } + TraitKind::Setter { + disp_id, method, .. + } => { + let function = FunctionObject::from_method( + context.gc_context, + method.clone(), + scope, + fn_proto, + Some(reciever), + ); + self.install_setter(context.gc_context, trait_name, *disp_id, function)?; + } + TraitKind::Class { slot_id, class } => { + let class_read = class.read(); + let super_class = if let Some(sc_name) = class_read.super_class_name() { + let super_name = self + .resolve_multiname(sc_name)? + .unwrap_or_else(|| QName::dynamic_name("Object")); + + let super_class: Result, Error> = self + .get_property(reciever, &super_name, activation, context)? + .as_object() + .map_err(|_e| { + format!("Could not resolve superclass {:?}", super_name.local_name()) + .into() + }); + + Some(super_class?) + } else { + None + }; + + let (class_object, _cinit) = + FunctionObject::from_class(activation, context, *class, super_class, scope)?; + self.install_const( + context.gc_context, + class_read.name().clone(), + *slot_id, + class_object.into(), + ); + } + TraitKind::Function { + slot_id, function, .. + } => { + let mut fobject = FunctionObject::from_method( + context.gc_context, + function.clone(), + scope, + fn_proto, + None, + ); + let es3_proto = + ScriptObject::object(context.gc_context, activation.avm2().prototypes().object); + + fobject.install_slot( + context.gc_context, + QName::new(Namespace::public_namespace(), "prototype"), + 0, + es3_proto.into(), + ); + self.install_const(context.gc_context, trait_name, *slot_id, fobject.into()); + } + TraitKind::Const { + slot_id, + default_value, + .. + } => { + self.install_const( + context.gc_context, + trait_name, + *slot_id, + default_value.clone().unwrap_or(Value::Undefined), + ); + } + } + + Ok(()) + } + + /// Call the object. + fn call( + self, + _reciever: Option>, + _arguments: &[Value<'gc>], + _activation: &mut Activation<'_, 'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _base_proto: Option>, + ) -> Result, Error> { + Err("Object is not callable".into()) + } + + /// Construct a host object of some kind and return it's cell. + /// + /// As the first step in object construction, the `construct` method is + /// called on the prototype to create a new object. The prototype may + /// construct any object implementation it wants, however, it's expected + /// to produce a like `TObject` implementor with itself as the new object's + /// proto. + /// + /// After construction, the constructor function is `call`ed with the new + /// object as `this` to initialize the object. + /// + /// `construct`ed objects should instantiate instance traits of the class + /// that this prototype represents. + /// + /// 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 construct( + &self, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], + ) -> Result, Error>; + + /// Construct a host object prototype of some kind and return it. + /// + /// This is called specifically to construct prototypes. The primary + /// difference is that a new class and scope closure are defined here. + /// Objects constructed from the new prototype should use that new class + /// and scope closure when instantiating non-prototype traits. + /// + /// Unlike `construct`, `derive`d objects should *not* instantiate instance + /// traits. + fn derive( + &self, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + class: GcCell<'gc, Class<'gc>>, + scope: Option>>, + ) -> Result, Error>; + + /// Implement the result of calling `Object.prototype.toString` on this + /// object class. + /// + /// `toString` is a method used to request an object be coerced to a string + /// value. The default implementation is stored here. User-specified string + /// coercions happen by defining `toString` in a downstream class or + /// prototype; this is then picked up by the VM runtime when doing + /// coercions. + fn to_string(&self, mc: MutationContext<'gc, '_>) -> Result, Error>; + + /// Implement the result of calling `Object.prototype.valueOf` on this + /// object class. + /// + /// `valueOf` is a method used to request an object be coerced to a + /// primitive value. Typically, this would be a number of some kind. + fn value_of(&self, mc: MutationContext<'gc, '_>) -> Result, Error>; + + /// Enumerate all interfaces implemented by this object. + fn interfaces(&self) -> Vec>; + + /// Set the interface list for this object. + fn set_interfaces(&self, context: MutationContext<'gc, '_>, iface_list: Vec>); + + /// Determine if this object is an instance of a given type. + /// + /// The given object should be the constructor for the given type we are + /// checking against this object. It's prototype will be searched in the + /// prototype chain of this object. If `check_interfaces` is enabled, then + /// the interfaces listed on each prototype will also be checked. + #[allow(unused_mut)] //it's not unused + fn is_instance_of( + &self, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + mut constructor: Object<'gc>, + check_interfaces: bool, + ) -> Result { + let type_proto = constructor + .get_property( + constructor, + &QName::dynamic_name("prototype"), + activation, + context, + )? + .as_object()?; + let mut my_proto = self.proto(); + + //TODO: Is it a verification error to do `obj instanceof bare_object`? + while let Some(proto) = my_proto { + if Object::ptr_eq(proto, type_proto) { + return Ok(true); + } + + if check_interfaces { + for interface in proto.interfaces() { + if Object::ptr_eq(interface, type_proto) { + return Ok(true); + } + } + } + + my_proto = proto.proto() + } + + Ok(false) + } + + /// Get a raw pointer value for this object. + fn as_ptr(&self) -> *const ObjectPtr; + + /// Get this object's `Executable`, if it has one. + fn as_executable(&self) -> Option> { + None + } +} + +pub enum ObjectPtr {} + +impl<'gc> Object<'gc> { + pub fn ptr_eq(a: Object<'gc>, b: Object<'gc>) -> bool { + a.as_ptr() == b.as_ptr() + } +} diff --git a/core/src/avm2/property.rs b/core/src/avm2/property.rs new file mode 100644 index 000000000000..c9c12e550ad3 --- /dev/null +++ b/core/src/avm2/property.rs @@ -0,0 +1,260 @@ +//! Property data structures + +use self::Attribute::*; +use crate::avm2::function::Executable; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::return_value::ReturnValue; +use crate::avm2::value::Value; +use crate::avm2::Error; +use enumset::{EnumSet, EnumSetType}; +use gc_arena::{Collect, CollectionContext}; + +/// Attributes of properties in the AVM runtime. +/// +/// TODO: Replace with AVM2 properties for traits +#[derive(EnumSetType, Debug)] +pub enum Attribute { + DontDelete, + ReadOnly, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug)] +pub enum Property<'gc> { + Virtual { + get: Option>, + set: Option>, + attributes: EnumSet, + }, + Stored { + value: Value<'gc>, + attributes: EnumSet, + }, + Slot { + slot_id: u32, + attributes: EnumSet, + }, +} + +unsafe impl<'gc> Collect for Property<'gc> { + fn trace(&self, cc: CollectionContext) { + match self { + Property::Virtual { get, set, .. } => { + get.trace(cc); + set.trace(cc); + } + Property::Stored { value, .. } => value.trace(cc), + Property::Slot { .. } => {} + } + } +} + +impl<'gc> Property<'gc> { + /// Create a new stored property. + pub fn new_stored(value: impl Into>) -> Self { + Property::Stored { + value: value.into(), + attributes: EnumSet::from(Attribute::DontDelete), + } + } + + /// Create a new stored property. + pub fn new_const(value: impl Into>) -> Self { + Property::Stored { + value: value.into(), + attributes: Attribute::ReadOnly | Attribute::DontDelete, + } + } + + /// Convert a value into a dynamic property. + pub fn new_dynamic_property(value: impl Into>) -> Self { + Property::Stored { + value: value.into(), + attributes: EnumSet::empty(), + } + } + + /// Convert a function into a method. + /// + /// This applies ReadOnly/DontDelete to the property. + pub fn new_method(fn_obj: Object<'gc>) -> Self { + Property::Stored { + value: fn_obj.into(), + attributes: Attribute::ReadOnly | Attribute::DontDelete, + } + } + + /// Create a new, unconfigured virtual property item. + pub fn new_virtual() -> Self { + Property::Virtual { + get: None, + set: None, + attributes: Attribute::ReadOnly | Attribute::DontDelete, + } + } + + /// Create a new slot property. + pub fn new_slot(slot_id: u32) -> Self { + Property::Slot { + slot_id, + attributes: EnumSet::from(Attribute::DontDelete), + } + } + + /// Install a getter into this property. + /// + /// This function errors if attempting to install executables into a + /// non-virtual property. + pub fn install_virtual_getter(&mut self, getter_impl: Executable<'gc>) -> Result<(), Error> { + match self { + Property::Virtual { get, .. } => *get = Some(getter_impl), + Property::Stored { .. } => return Err("Not a virtual property".into()), + Property::Slot { .. } => return Err("Not a virtual property".into()), + }; + + Ok(()) + } + + /// Install a setter into this property. + /// + /// This function errors if attempting to install executables into a + /// non-virtual property. + pub fn install_virtual_setter(&mut self, setter_impl: Executable<'gc>) -> Result<(), Error> { + match self { + Property::Virtual { set, .. } => *set = Some(setter_impl), + Property::Stored { .. } => return Err("Not a virtual property".into()), + Property::Slot { .. } => return Err("Not a virtual property".into()), + }; + + Ok(()) + } + + /// Get the value of a property slot. + /// + /// This function yields `ReturnValue` because some properties may be + /// user-defined. + pub fn get( + &self, + this: Object<'gc>, + base_proto: Option>, + ) -> Result, Error> { + match self { + Property::Virtual { get: Some(get), .. } => Ok(ReturnValue::defer_execution( + *get, + Some(this), + vec![], + base_proto, + )), + Property::Virtual { get: None, .. } => Ok(Value::Undefined.into()), + Property::Stored { value, .. } => Ok(value.to_owned().into()), + Property::Slot { slot_id, .. } => this.get_slot(*slot_id).map(|v| v.into()), + } + } + + /// Set a property slot. + /// + /// This function returns a `ReturnValue` which should be resolved. The + /// resulting `Value` is unimportant and should be discarded. + /// + /// This function cannot set slot properties and will panic if one + /// is encountered. + pub fn set( + &mut self, + this: Object<'gc>, + base_proto: Option>, + new_value: impl Into>, + ) -> Result, Error> { + match self { + Property::Virtual { set, .. } => { + if let Some(function) = set { + return Ok(ReturnValue::defer_execution( + *function, + Some(this), + vec![new_value.into()], + base_proto, + )); + } + + Ok(Value::Undefined.into()) + } + Property::Stored { + value, attributes, .. + } => { + if !attributes.contains(ReadOnly) { + *value = new_value.into(); + } + + Ok(Value::Undefined.into()) + } + Property::Slot { .. } => panic!("Cannot recursively set slots"), + } + } + + /// Init a property slot. + /// + /// The difference between `set` and `init` is that this function does not + /// respect `ReadOnly` and will allow initializing nominally `const` + /// properties, at least once. Virtual properties with no setter cannot be + /// initialized. + /// + /// This function returns a `ReturnValue` which should be resolved. The + /// resulting `Value` is unimportant and should be discarded. + /// + /// This function cannot initialize slot properties and will panic if one + /// is encountered. + pub fn init( + &mut self, + this: Object<'gc>, + base_proto: Option>, + new_value: impl Into>, + ) -> Result, Error> { + match self { + Property::Virtual { set, .. } => { + if let Some(function) = set { + return Ok(ReturnValue::defer_execution( + *function, + Some(this), + vec![new_value.into()], + base_proto, + )); + } + + Ok(Value::Undefined.into()) + } + Property::Stored { value, .. } => { + *value = new_value.into(); + + Ok(Value::Undefined.into()) + } + Property::Slot { .. } => panic!("Cannot recursively init slots"), + } + } + + /// Retrieve the slot ID of a property. + /// + /// This function yields `None` if this property is not a slot. + pub fn slot_id(&self) -> Option { + match self { + Property::Slot { slot_id, .. } => Some(*slot_id), + _ => None, + } + } + + pub fn can_delete(&self) -> bool { + match self { + Property::Virtual { attributes, .. } => !attributes.contains(DontDelete), + Property::Stored { attributes, .. } => !attributes.contains(DontDelete), + Property::Slot { attributes, .. } => !attributes.contains(DontDelete), + } + } + + pub fn is_overwritable(&self) -> bool { + match self { + Property::Virtual { + attributes, set, .. + } => !attributes.contains(ReadOnly) && !set.is_none(), + Property::Stored { attributes, .. } => !attributes.contains(ReadOnly), + Property::Slot { attributes, .. } => !attributes.contains(ReadOnly), + } + } +} diff --git a/core/src/avm2/property_map.rs b/core/src/avm2/property_map.rs new file mode 100644 index 000000000000..c9d762c38b03 --- /dev/null +++ b/core/src/avm2/property_map.rs @@ -0,0 +1,7 @@ +//! Property map + +use crate::avm2::names::QName; +use std::collections::HashMap; + +/// Type which represents named properties on an object. +pub type PropertyMap<'gc, V> = HashMap, V>; diff --git a/core/src/avm2/return_value.rs b/core/src/avm2/return_value.rs new file mode 100644 index 000000000000..6d0e56d651ee --- /dev/null +++ b/core/src/avm2/return_value.rs @@ -0,0 +1,113 @@ +//! Return value enum + +use crate::avm2::activation::Activation; +use crate::avm2::function::Executable; +use crate::avm2::object::Object; +use crate::avm2::{Error, Value}; +use crate::context::UpdateContext; +use std::fmt; + +/// Represents the return value of a function call that has not yet executed. +/// +/// It is a panicking logic error to attempt to run AVM2 code while any +/// reachable object is in a locked state. Ergo, it is sometimes necessary to +/// be able to return what *should* be called rather than actually running the +/// code on the current Rust stack. This type exists to force deferred +/// execution of some child AVM2 frame. +/// +/// It is also possible to stuff a regular `Value` in here - this is provided +/// for the convenience of functions that may be able to resolve a value +/// without needing a free stack. `ReturnValue` should not be used as a generic +/// wrapper for `Value`, as it can also defer actual execution, and it should +/// be resolved at the earliest safe opportunity. +/// +/// It is `must_use` - failing to resolve a return value is a compiler warning. +#[must_use = "Return values must be used"] +pub enum ReturnValue<'gc> { + /// ReturnValue has already been computed. + /// + /// This exists primarily for functions that don't necessarily need to + /// always defer code execution - say, if they already have the result and + /// do not need a free stack frame to run an activation on. + Immediate(Value<'gc>), + + /// ReturnValue has not yet been computed. + /// + /// This exists for functions that do need to reference the result of user + /// code in order to produce their result. + ResultOf { + executable: Executable<'gc>, + unbound_reciever: Option>, + arguments: Vec>, + base_proto: Option>, + }, +} + +impl fmt::Debug for ReturnValue<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Immediate(v) => f.debug_tuple("ReturnValue::Immediate").field(v).finish(), + Self::ResultOf { + executable: _executable, + unbound_reciever, + arguments, + base_proto, + } => f + .debug_struct("ReturnValue") + .field("executable", &"") + .field("unbound_reciever", unbound_reciever) + .field("arguments", arguments) + .field("base_proto", base_proto) + .finish(), + } + } +} + +impl<'gc> ReturnValue<'gc> { + /// Construct a new return value. + pub fn defer_execution( + executable: Executable<'gc>, + unbound_reciever: Option>, + arguments: Vec>, + base_proto: Option>, + ) -> Self { + Self::ResultOf { + executable, + unbound_reciever, + arguments, + base_proto, + } + } + + /// Resolve the underlying deferred execution. + /// + /// All return values must eventually resolved - it is a compile error to + /// fail to do so. + pub fn resolve( + self, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + match self { + Self::Immediate(v) => Ok(v), + Self::ResultOf { + executable, + unbound_reciever, + arguments, + base_proto, + } => executable.exec( + unbound_reciever, + &arguments, + activation, + context, + base_proto, + ), + } + } +} + +impl<'gc> From> for ReturnValue<'gc> { + fn from(v: Value<'gc>) -> Self { + Self::Immediate(v) + } +} diff --git a/core/src/avm2/scope.rs b/core/src/avm2/scope.rs new file mode 100644 index 000000000000..37d594ec2bec --- /dev/null +++ b/core/src/avm2/scope.rs @@ -0,0 +1,149 @@ +//! Represents AVM2 scope chain resolution. + +use crate::avm2::activation::Activation; +use crate::avm2::names::Multiname; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::{Collect, GcCell, MutationContext}; +use std::cell::Ref; + +/// Indicates what kind of scope a scope is. +#[derive(Copy, Clone, Debug, PartialEq, Collect)] +#[collect(no_drop)] +pub enum ScopeClass { + /// Scope represents global or closure scope. + GlobalOrClosure, + + /// Scope represents an object added to the scope chain with `with`. + /// It is not inherited when closures are defined. Furthermore, a `with` + /// scope gains the ability to be searched for dynamic properties. + With, +} + +/// Represents a scope chain for an AVM2 activation. +#[derive(Debug, Collect)] +#[collect(no_drop)] +pub struct Scope<'gc> { + parent: Option>>, + class: ScopeClass, + values: Object<'gc>, +} + +impl<'gc> Scope<'gc> { + /// Push a scope onto the stack, producing a new scope chain that's one + /// item longer. + pub fn push_scope( + scope_stack: Option>>, + object: Object<'gc>, + mc: MutationContext<'gc, '_>, + ) -> GcCell<'gc, Self> { + GcCell::allocate( + mc, + Self { + parent: scope_stack, + class: ScopeClass::GlobalOrClosure, + values: object, + }, + ) + } + + /// Construct a with scope to be used as the scope during a with block. + /// + /// A with block adds an object to the top of the scope chain, so unqualified + /// references will try to resolve on that object first. + pub fn push_with( + scope_stack: Option>>, + with_object: Object<'gc>, + mc: MutationContext<'gc, '_>, + ) -> GcCell<'gc, Self> { + GcCell::allocate( + mc, + Scope { + parent: scope_stack, + class: ScopeClass::With, + values: with_object, + }, + ) + } + + pub fn pop_scope(&self) -> Option>> { + self.parent + } + + /// Returns a reference to the current local scope object. + pub fn locals(&self) -> &Object<'gc> { + &self.values + } + + /// Returns a reference to the current local scope object for mutation. + pub fn locals_mut(&mut self) -> &mut Object<'gc> { + &mut self.values + } + + /// Returns a reference to the parent scope object. + pub fn parent(&self) -> Option>> { + match self.parent { + Some(ref p) => Some(p.read()), + None => None, + } + } + + pub fn parent_cell(&self) -> Option>> { + self.parent + } + + /// Find an object that contains a given property in the scope stack. + /// + /// This function yields `None` if no such scope exists. + pub fn find( + &self, + name: &Multiname<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result>, Error> { + if let Some(qname) = self.locals().resolve_multiname(name)? { + if self.locals().has_property(&qname)? { + return Ok(Some(*self.locals())); + } + } + + if let Some(scope) = self.parent() { + return scope.find(name, activation, context); + } + + Ok(None) + } + + /// Resolve a particular value in the scope chain. + /// + /// This function yields `None` if no such scope exists to provide the + /// property's value. + pub fn resolve( + &mut self, + name: &Multiname<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result>, Error> { + if let Some(qname) = self.locals().resolve_multiname(name)? { + if self.locals().has_property(&qname)? { + return Ok(Some(self.values.get_property( + self.values, + &qname, + activation, + context, + )?)); + } + } + + if let Some(parent) = self.parent { + return parent + .write(context.gc_context) + .resolve(name, activation, context); + } + + //TODO: Should undefined variables halt execution? + Ok(None) + } +} diff --git a/core/src/avm2/script.rs b/core/src/avm2/script.rs new file mode 100644 index 000000000000..05fc6dfd366a --- /dev/null +++ b/core/src/avm2/script.rs @@ -0,0 +1,286 @@ +//! Whole script representation + +use crate::avm2::class::Class; +use crate::avm2::method::{BytecodeMethod, Method}; +use crate::avm2::r#trait::Trait; +use crate::avm2::string::AvmString; +use crate::avm2::Error; +use fnv::FnvHashMap; +use gc_arena::{Collect, Gc, GcCell, MutationContext}; +use std::mem::drop; +use std::rc::Rc; +use swf::avm2::types::{AbcFile, Index, Script as AbcScript}; + +#[derive(Clone, Debug, Collect)] +#[collect(require_static)] +pub struct CollectWrapper(T); + +#[derive(Copy, Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct TranslationUnit<'gc>(GcCell<'gc, TranslationUnitData<'gc>>); + +/// A loaded ABC file, with any loaded ABC items alongside it. +/// +/// A `TranslationUnit` is constructed when ABC loading begins, and it stores +/// all loaded ABC items (classes, methods, and scripts) as they are loaded. +/// Unit items are loaded lazily and retained in the `TranslationUnit` for +/// later retrieval. +/// +/// Loaded versions of ABC items consist of the types `Class`, `Method`, and +/// `Script`, all of which correspond to their `swf` equivalents, but with +/// names preloaded. This roughly corresponds to the logical "loading" phase of +/// ABC execution as documented in the AVM2 Overview. "Linking" takes place by +/// constructing the appropriate runtime object for that item. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct TranslationUnitData<'gc> { + /// The ABC file that all of the following loaded data comes from. + abc: CollectWrapper>, + + /// All classes loaded from the ABC's class list. + classes: FnvHashMap>>, + + /// All methods loaded from the ABC's method list. + methods: FnvHashMap>, + + /// All scripts loaded from the ABC's scripts list. + scripts: FnvHashMap>>, + + /// All strings loaded from the ABC's strings list. + strings: FnvHashMap>, +} + +impl<'gc> TranslationUnit<'gc> { + pub fn from_abc(abc: Rc, mc: MutationContext<'gc, '_>) -> Self { + Self(GcCell::allocate( + mc, + TranslationUnitData { + abc: CollectWrapper(abc), + classes: FnvHashMap::default(), + methods: FnvHashMap::default(), + scripts: FnvHashMap::default(), + strings: FnvHashMap::default(), + }, + )) + } + + /// Retrieve the underlying `AbcFile` for this translation unit. + pub fn abc(self) -> Rc { + self.0.read().abc.0.clone() + } + + /// Load a method from the ABC file and return it's method definition. + pub fn load_method( + self, + method_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + let read = self.0.read(); + if let Some(method) = read.methods.get(&method_index) { + return Ok(method.clone()); + } + + drop(read); + + let method: Result>, Error> = + BytecodeMethod::from_method_index(self, Index::new(method_index), mc) + .ok_or_else(|| "Method index does not exist".into()); + let method: Method<'gc> = method?.into(); + + self.0 + .write(mc) + .methods + .insert(method_index, method.clone()); + + Ok(method) + } + + /// Load a class from the ABC file and return it's class definition. + pub fn load_class( + self, + class_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result>, Error> { + let read = self.0.read(); + if let Some(class) = read.classes.get(&class_index) { + return Ok(*class); + } + + drop(read); + + let class = Class::from_abc_index(self, class_index, mc)?; + self.0.write(mc).classes.insert(class_index, class); + + class.write(mc).load_traits(self, class_index, mc)?; + + Ok(class) + } + + /// Load a script from the ABC file and return it's script definition. + pub fn load_script( + self, + script_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result>, Error> { + let read = self.0.read(); + if let Some(scripts) = read.scripts.get(&script_index) { + return Ok(*scripts); + } + + drop(read); + + let script = Script::from_abc_index(self, script_index, mc)?; + self.0.write(mc).scripts.insert(script_index, script); + + script.write(mc).load_traits(self, script_index, mc)?; + + Ok(script) + } + + /// Load a string from the ABC's constant pool. + /// + /// This function yields an error if no such string index exists. + /// + /// This function yields `None` to signal string index zero, which callers + /// are free to interpret as the context demands. + pub fn pool_string_option( + self, + string_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result>, Error> { + let mut write = self.0.write(mc); + if let Some(string) = write.strings.get(&string_index) { + return Ok(Some(*string)); + } + + if string_index == 0 { + return Ok(None); + } + + let avm_string = AvmString::new( + mc, + write + .abc + .0 + .constant_pool + .strings + .get(string_index as usize - 1) + .ok_or_else(|| format!("Unknown string constant {}", string_index))?, + ); + write.strings.insert(string_index, avm_string); + + Ok(Some(avm_string)) + } + + /// Load a string from the ABC's constant pool. + /// + /// This function yields an error if no such string index exists. + /// + /// String index 0 is always `""`. If you need to instead treat 0 as + /// something else, then please use `pool_string_option`. + pub fn pool_string( + self, + string_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + Ok(self + .pool_string_option(string_index, mc)? + .unwrap_or_default()) + } +} + +/// A loaded Script from an ABC file. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct Script<'gc> { + /// The initializer method to run for the script. + init: Method<'gc>, + + /// Traits that this script uses. + traits: Vec>, + + /// Whether or not we loaded our traits. + traits_loaded: bool, +} + +impl<'gc> Script<'gc> { + /// Construct a script from a `TranslationUnit` and it's script index. + /// + /// The returned script will be allocated, but no traits will be loaded. + /// The caller is responsible for storing the class in the + /// `TranslationUnit` and calling `load_traits` to complete the + /// trait-loading process. + pub fn from_abc_index( + unit: TranslationUnit<'gc>, + script_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result, Error> { + let abc = unit.abc(); + let script: Result<&AbcScript, Error> = abc + .scripts + .get(script_index as usize) + .ok_or_else(|| "LoadError: Script index not valid".into()); + let script = script?; + + let init = unit.load_method(script.init_method.0, mc)?; + + Ok(GcCell::allocate( + mc, + Self { + init, + traits: Vec::new(), + traits_loaded: false, + }, + )) + } + + /// Finish the class-loading process by loading traits. + /// + /// This process must be done after the `Script` has been stored in the + /// `TranslationUnit`. Failing to do so runs the risk of runaway recursion + /// or double-borrows. It should be done before the script is actually + /// executed. + pub fn load_traits( + &mut self, + unit: TranslationUnit<'gc>, + script_index: u32, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + if self.traits_loaded { + return Ok(()); + } + + self.traits_loaded = true; + + let abc = unit.abc(); + let script: Result<_, Error> = abc + .scripts + .get(script_index as usize) + .ok_or_else(|| "LoadError: Script index not valid".into()); + let script = script?; + + for abc_trait in script.traits.iter() { + self.traits + .push(Trait::from_abc_trait(unit, &abc_trait, mc)?); + } + + Ok(()) + } + + /// Return the entrypoint for the script. + pub fn init(&self) -> Method<'gc> { + self.init.clone() + } + + /// Return traits for this script. + /// + /// This function will return an error if it is incorrectly called before + /// traits are loaded. + pub fn traits(&self) -> Result<&[Trait<'gc>], Error> { + if !self.traits_loaded { + return Err("LoadError: Script traits accessed before they were loaded!".into()); + } + + Ok(&self.traits) + } +} diff --git a/core/src/avm2/script_object.rs b/core/src/avm2/script_object.rs new file mode 100644 index 000000000000..c306f9836ea5 --- /dev/null +++ b/core/src/avm2/script_object.rs @@ -0,0 +1,908 @@ +//! Default AVM2 object impl + +use crate::avm2::activation::Activation; +use crate::avm2::class::Class; +use crate::avm2::function::Executable; +use crate::avm2::names::{Namespace, QName}; +use crate::avm2::object::{Object, ObjectPtr, TObject}; +use crate::avm2::property::Property; +use crate::avm2::property_map::PropertyMap; +use crate::avm2::r#trait::Trait; +use crate::avm2::return_value::ReturnValue; +use crate::avm2::scope::Scope; +use crate::avm2::slot::Slot; +use crate::avm2::string::AvmString; +use crate::avm2::value::Value; +use crate::avm2::Error; +use crate::context::UpdateContext; +use gc_arena::{Collect, GcCell, MutationContext}; +use std::collections::HashMap; +use std::fmt::Debug; + +/// Default implementation of `avm2::Object`. +#[derive(Clone, Collect, Debug, Copy)] +#[collect(no_drop)] +pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>); + +/// Information necessary for a script object to have a class attached to it. +/// +/// Classes can be attached to a `ScriptObject` such that the class's traits +/// are instantiated on-demand. Either class or instance traits can be +/// instantiated. +/// +/// Trait instantiation obeys prototyping rules: prototypes provide their +/// instances with classes to pull traits from. +#[derive(Clone, Collect, Debug)] +#[collect(no_drop)] +pub enum ScriptObjectClass<'gc> { + /// Instantiate instance traits, for prototypes. + InstancePrototype(GcCell<'gc, Class<'gc>>, Option>>), + + /// Instantiate class traits, for class constructors. + ClassConstructor(GcCell<'gc, Class<'gc>>, Option>>), + + /// Do not instantiate any class or instance traits. + NoClass, +} + +/// Base data common to all `TObject` implementations. +/// +/// Host implementations of `TObject` should embed `ScriptObjectData` and +/// forward any trait method implementations it does not overwrite to this +/// struct. +#[derive(Clone, Collect, Debug)] +#[collect(no_drop)] +pub struct ScriptObjectData<'gc> { + /// Properties stored on this object. + values: PropertyMap<'gc, Property<'gc>>, + + /// Slots stored on this object. + slots: Vec>, + + /// Methods stored on this object. + methods: Vec>>, + + /// Implicit prototype of this script object. + proto: Option>, + + /// The class that this script object represents. + class: ScriptObjectClass<'gc>, + + /// Enumeratable property names. + enumerants: Vec>, + + /// Interfaces implemented by this object. (prototypes only) + interfaces: Vec>, +} + +impl<'gc> TObject<'gc> for ScriptObject<'gc> { + fn get_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let rv = self + .0 + .read() + .get_property_local(reciever, name, activation)?; + + rv.resolve(activation, context) + } + + fn set_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + let rv = self + .0 + .write(context.gc_context) + .set_property_local(reciever, name, value, activation, context)?; + + rv.resolve(activation, context)?; + + Ok(()) + } + + fn init_property_local( + self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result<(), Error> { + let rv = self + .0 + .write(context.gc_context) + .init_property_local(reciever, name, value, activation, context)?; + + rv.resolve(activation, context)?; + + Ok(()) + } + + fn is_property_overwritable( + self, + gc_context: MutationContext<'gc, '_>, + name: &QName<'gc>, + ) -> bool { + self.0.write(gc_context).is_property_overwritable(name) + } + + fn delete_property(&self, gc_context: MutationContext<'gc, '_>, name: &QName<'gc>) -> bool { + self.0.write(gc_context).delete_property(name) + } + + fn get_slot(self, id: u32) -> Result, Error> { + self.0.read().get_slot(id) + } + + fn set_slot( + self, + id: u32, + value: Value<'gc>, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + self.0.write(mc).set_slot(id, value, mc) + } + + fn init_slot( + self, + id: u32, + value: Value<'gc>, + mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + self.0.write(mc).init_slot(id, value, mc) + } + + fn get_method(self, id: u32) -> Option> { + self.0.read().get_method(id) + } + + fn get_trait(self, name: &QName<'gc>) -> Result>, Error> { + self.0.read().get_trait(name) + } + + fn get_provided_trait( + &self, + name: &QName<'gc>, + known_traits: &mut Vec>, + ) -> Result<(), Error> { + self.0.read().get_provided_trait(name, known_traits) + } + + fn get_scope(self) -> Option>> { + self.0.read().get_scope() + } + + fn resolve_any(self, local_name: AvmString<'gc>) -> Result>, Error> { + self.0.read().resolve_any(local_name) + } + + fn resolve_any_trait( + self, + local_name: AvmString<'gc>, + ) -> Result>, Error> { + self.0.read().resolve_any_trait(local_name) + } + + fn has_own_property(self, name: &QName<'gc>) -> Result { + self.0.read().has_own_property(name) + } + + fn has_trait(self, name: &QName<'gc>) -> Result { + self.0.read().has_trait(name) + } + + fn provides_trait(self, name: &QName<'gc>) -> Result { + self.0.read().provides_trait(name) + } + + fn has_instantiated_property(self, name: &QName<'gc>) -> bool { + self.0.read().has_instantiated_property(name) + } + + fn has_own_virtual_getter(self, name: &QName<'gc>) -> bool { + self.0.read().has_own_virtual_getter(name) + } + + fn has_own_virtual_setter(self, name: &QName<'gc>) -> bool { + self.0.read().has_own_virtual_setter(name) + } + + fn proto(&self) -> Option> { + self.0.read().proto + } + + fn get_enumerant_name(&self, index: u32) -> Option> { + self.0.read().get_enumerant_name(index) + } + + fn property_is_enumerable(&self, name: &QName<'gc>) -> bool { + self.0.read().property_is_enumerable(name) + } + + fn set_local_property_is_enumerable( + &self, + mc: MutationContext<'gc, '_>, + name: &QName<'gc>, + is_enumerable: bool, + ) -> Result<(), Error> { + self.0 + .write(mc) + .set_local_property_is_enumerable(name, is_enumerable) + } + + fn as_ptr(&self) -> *const ObjectPtr { + self.0.as_ptr() as *const ObjectPtr + } + + fn construct( + &self, + _activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], + ) -> Result, Error> { + let this: Object<'gc> = Object::ScriptObject(*self); + Ok(ScriptObject::object(context.gc_context, this)) + } + + fn derive( + &self, + _activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + class: GcCell<'gc, Class<'gc>>, + scope: Option>>, + ) -> Result, Error> { + let this: Object<'gc> = Object::ScriptObject(*self); + Ok(ScriptObject::prototype( + context.gc_context, + this, + class, + scope, + )) + } + + fn to_string(&self, _mc: MutationContext<'gc, '_>) -> Result, Error> { + Ok("[object Object]".into()) + } + + fn value_of(&self, _mc: MutationContext<'gc, '_>) -> Result, Error> { + Ok(Value::Object(Object::from(*self))) + } + + fn install_method( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) { + self.0.write(mc).install_method(name, disp_id, function) + } + + fn install_getter( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error> { + self.0.write(mc).install_getter(name, disp_id, function) + } + + fn install_setter( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error> { + self.0.write(mc).install_setter(name, disp_id, function) + } + + fn install_dynamic_property( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + value: Value<'gc>, + ) -> Result<(), Error> { + self.0.write(mc).install_dynamic_property(name, value) + } + + fn install_slot( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + id: u32, + value: Value<'gc>, + ) { + self.0.write(mc).install_slot(name, id, value) + } + + fn install_const( + &mut self, + mc: MutationContext<'gc, '_>, + name: QName<'gc>, + id: u32, + value: Value<'gc>, + ) { + self.0.write(mc).install_const(name, id, value) + } + + fn interfaces(&self) -> Vec> { + self.0.read().interfaces() + } + + fn set_interfaces(&self, context: MutationContext<'gc, '_>, iface_list: Vec>) { + self.0.write(context).set_interfaces(iface_list) + } +} + +impl<'gc> ScriptObject<'gc> { + /// Construct a bare object with no base class. + /// + /// This is *not* the same thing as an object literal, which actually does + /// have a base class: `Object`. + pub fn bare_object(mc: MutationContext<'gc, '_>) -> Object<'gc> { + ScriptObject(GcCell::allocate( + mc, + ScriptObjectData::base_new(None, ScriptObjectClass::NoClass), + )) + .into() + } + + /// Construct a bare class prototype with no base class. + /// + /// This appears to be used specifically for interfaces, which have no base + /// class. + pub fn bare_prototype( + mc: MutationContext<'gc, '_>, + class: GcCell<'gc, Class<'gc>>, + scope: Option>>, + ) -> Object<'gc> { + let script_class = ScriptObjectClass::InstancePrototype(class, scope); + + ScriptObject(GcCell::allocate( + mc, + ScriptObjectData::base_new(None, script_class), + )) + .into() + } + + /// Construct an object with a prototype. + pub fn object(mc: MutationContext<'gc, '_>, proto: Object<'gc>) -> Object<'gc> { + ScriptObject(GcCell::allocate( + mc, + ScriptObjectData::base_new(Some(proto), ScriptObjectClass::NoClass), + )) + .into() + } + + /// Construct a prototype for an ES4 class. + pub fn prototype( + mc: MutationContext<'gc, '_>, + proto: Object<'gc>, + class: GcCell<'gc, Class<'gc>>, + scope: Option>>, + ) -> Object<'gc> { + let script_class = ScriptObjectClass::InstancePrototype(class, scope); + + ScriptObject(GcCell::allocate( + mc, + ScriptObjectData::base_new(Some(proto), script_class), + )) + .into() + } +} + +impl<'gc> ScriptObjectData<'gc> { + pub fn base_new(proto: Option>, trait_source: ScriptObjectClass<'gc>) -> Self { + ScriptObjectData { + values: HashMap::new(), + slots: Vec::new(), + methods: Vec::new(), + proto, + class: trait_source, + enumerants: Vec::new(), + interfaces: Vec::new(), + } + } + + pub fn get_property_local( + &self, + reciever: Object<'gc>, + name: &QName<'gc>, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error> { + let prop = self.values.get(name); + + if let Some(prop) = prop { + prop.get(reciever, activation.base_proto().or(self.proto)) + } else { + Ok(Value::Undefined.into()) + } + } + + pub fn set_property_local( + &mut self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + let slot_id = if let Some(prop) = self.values.get(name) { + if let Some(slot_id) = prop.slot_id() { + Some(slot_id) + } else { + None + } + } else { + None + }; + + if let Some(slot_id) = slot_id { + self.set_slot(slot_id, value, context.gc_context)?; + Ok(Value::Undefined.into()) + } else if self.values.contains_key(name) { + let prop = self.values.get_mut(name).unwrap(); + let proto = self.proto; + prop.set(reciever, activation.base_proto().or(proto), value) + } else { + //TODO: Not all classes are dynamic like this + self.enumerants.push(name.clone()); + self.values + .insert(name.clone(), Property::new_dynamic_property(value)); + + Ok(Value::Undefined.into()) + } + } + + pub fn init_property_local( + &mut self, + reciever: Object<'gc>, + name: &QName<'gc>, + value: Value<'gc>, + activation: &mut Activation<'_, 'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Result, Error> { + if let Some(prop) = self.values.get_mut(name) { + if let Some(slot_id) = prop.slot_id() { + self.init_slot(slot_id, value, context.gc_context)?; + Ok(Value::Undefined.into()) + } else { + let proto = self.proto; + prop.init(reciever, activation.base_proto().or(proto), value) + } + } else { + //TODO: Not all classes are dynamic like this + self.values + .insert(name.clone(), Property::new_dynamic_property(value)); + + Ok(Value::Undefined.into()) + } + } + + pub fn is_property_overwritable(&self, name: &QName<'gc>) -> bool { + self.values + .get(name) + .map(|p| p.is_overwritable()) + .unwrap_or(true) + } + + pub fn delete_property(&mut self, name: &QName<'gc>) -> bool { + let can_delete = if let Some(prop) = self.values.get(name) { + prop.can_delete() + } else { + false + }; + + if can_delete { + self.values.remove(name); + } + + can_delete + } + + pub fn get_slot(&self, id: u32) -> Result, Error> { + //TODO: slot inheritance, I think? + self.slots + .get(id as usize) + .cloned() + .ok_or_else(|| format!("Slot index {} out of bounds!", id).into()) + .map(|slot| slot.get().unwrap_or(Value::Undefined)) + } + + /// Set a slot by it's index. + pub fn set_slot( + &mut self, + id: u32, + value: Value<'gc>, + _mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + if let Some(slot) = self.slots.get_mut(id as usize) { + slot.set(value) + } else { + Err(format!("Slot index {} out of bounds!", id).into()) + } + } + + /// Set a slot by it's index. + pub fn init_slot( + &mut self, + id: u32, + value: Value<'gc>, + _mc: MutationContext<'gc, '_>, + ) -> Result<(), Error> { + if let Some(slot) = self.slots.get_mut(id as usize) { + slot.init(value) + } else { + Err(format!("Slot index {} out of bounds!", id).into()) + } + } + + /// Retrieve a method from the method table. + pub fn get_method(&self, id: u32) -> Option> { + self.methods.get(id as usize).and_then(|v| *v) + } + + pub fn get_trait(&self, name: &QName<'gc>) -> Result>, Error> { + match &self.class { + //Class constructors have local traits only. + ScriptObjectClass::ClassConstructor(..) => { + let mut known_traits = Vec::new(); + self.get_provided_trait(name, &mut known_traits)?; + + Ok(known_traits) + } + + //Prototypes do not have traits available locally, but they provide + //traits instead. + ScriptObjectClass::InstancePrototype(..) => Ok(Vec::new()), + + //Instances walk the prototype chain to build a list of known + //traits provided by the classes attached to those prototypes. + ScriptObjectClass::NoClass => { + let mut known_traits = Vec::new(); + let mut chain = Vec::new(); + let mut proto = self.proto(); + + while let Some(p) = proto { + chain.push(p); + proto = p.proto(); + } + + for proto in chain.iter().rev() { + proto.get_provided_trait(name, &mut known_traits)?; + } + + Ok(known_traits) + } + } + } + + pub fn get_provided_trait( + &self, + name: &QName<'gc>, + known_traits: &mut Vec>, + ) -> Result<(), Error> { + match &self.class { + ScriptObjectClass::ClassConstructor(class, ..) => { + class.read().lookup_class_traits(name, known_traits) + } + ScriptObjectClass::InstancePrototype(class, ..) => { + class.read().lookup_instance_traits(name, known_traits) + } + ScriptObjectClass::NoClass => Ok(()), + } + } + + pub fn has_trait(&self, name: &QName<'gc>) -> Result { + match &self.class { + //Class constructors have local traits only. + ScriptObjectClass::ClassConstructor(..) => self.provides_trait(name), + + //Prototypes do not have traits available locally, but we walk + //through them to find traits (see `provides_trait`) + ScriptObjectClass::InstancePrototype(..) => Ok(false), + + //Instances walk the prototype chain to build a list of known + //traits provided by the classes attached to those prototypes. + ScriptObjectClass::NoClass => { + let mut proto = self.proto(); + + while let Some(p) = proto { + if p.provides_trait(name)? { + return Ok(true); + } + + proto = p.proto(); + } + + Ok(false) + } + } + } + + pub fn provides_trait(&self, name: &QName<'gc>) -> Result { + match &self.class { + ScriptObjectClass::ClassConstructor(class, ..) => { + Ok(class.read().has_class_trait(name)) + } + ScriptObjectClass::InstancePrototype(class, ..) => { + Ok(class.read().has_instance_trait(name)) + } + ScriptObjectClass::NoClass => Ok(false), + } + } + + pub fn get_scope(&self) -> Option>> { + match &self.class { + ScriptObjectClass::ClassConstructor(_class, scope) => *scope, + ScriptObjectClass::InstancePrototype(_class, scope) => *scope, + ScriptObjectClass::NoClass => self.proto().and_then(|proto| proto.get_scope()), + } + } + + pub fn resolve_any(&self, local_name: AvmString<'gc>) -> Result>, Error> { + for (key, _value) in self.values.iter() { + if key.local_name() == local_name { + return Ok(Some(key.namespace().clone())); + } + } + + match self.class { + ScriptObjectClass::ClassConstructor(..) => self.resolve_any_trait(local_name), + ScriptObjectClass::NoClass => self.resolve_any_trait(local_name), + _ => Ok(None), + } + } + + pub fn resolve_any_trait( + &self, + local_name: AvmString<'gc>, + ) -> Result>, Error> { + if let Some(proto) = self.proto { + let proto_trait_name = proto.resolve_any_trait(local_name)?; + if let Some(ns) = proto_trait_name { + return Ok(Some(ns)); + } + } + + match &self.class { + ScriptObjectClass::ClassConstructor(class, ..) => { + Ok(class.read().resolve_any_class_trait(local_name)) + } + ScriptObjectClass::InstancePrototype(class, ..) => { + Ok(class.read().resolve_any_instance_trait(local_name)) + } + ScriptObjectClass::NoClass => Ok(None), + } + } + + pub fn has_own_property(&self, name: &QName<'gc>) -> Result { + Ok(self.values.get(name).is_some() || self.has_trait(name)?) + } + + pub fn has_instantiated_property(&self, name: &QName<'gc>) -> bool { + self.values.get(name).is_some() + } + + pub fn has_own_virtual_getter(&self, name: &QName<'gc>) -> bool { + match self.values.get(name) { + Some(Property::Virtual { get: Some(_), .. }) => true, + _ => false, + } + } + + pub fn has_own_virtual_setter(&self, name: &QName<'gc>) -> bool { + match self.values.get(name) { + Some(Property::Virtual { set: Some(_), .. }) => true, + _ => false, + } + } + + pub fn proto(&self) -> Option> { + self.proto + } + + pub fn get_enumerant_name(&self, index: u32) -> Option> { + // NOTE: AVM2 object enumeration is one of the weakest parts of an + // otherwise well-designed VM. Notably, because of the way they + // implemented `hasnext` and `hasnext2`, all enumerants start from ONE. + // Hence why we have to `checked_sub` here in case some miscompiled + // code doesn't check for the zero index, which is actually a failure + // sentinel. + let true_index = (index as usize).checked_sub(1)?; + + self.enumerants.get(true_index).cloned() + } + + pub fn property_is_enumerable(&self, name: &QName<'gc>) -> bool { + self.enumerants.contains(name) + } + + pub fn set_local_property_is_enumerable( + &mut self, + name: &QName<'gc>, + is_enumerable: bool, + ) -> Result<(), Error> { + if is_enumerable && self.values.contains_key(name) && !self.enumerants.contains(name) { + // Traits are never enumerable + if self.has_trait(name)? { + return Ok(()); + } + + self.enumerants.push(name.clone()); + } else if !is_enumerable && self.enumerants.contains(name) { + let mut index = None; + for (i, other_name) in self.enumerants.iter().enumerate() { + if other_name == name { + index = Some(i); + } + } + + if let Some(index) = index { + self.enumerants.remove(index); + } + } + + Ok(()) + } + + pub fn class(&self) -> &ScriptObjectClass<'gc> { + &self.class + } + + /// Install a method into the object. + pub fn install_method(&mut self, name: QName<'gc>, disp_id: u32, function: Object<'gc>) { + if disp_id > 0 { + if self.methods.len() <= disp_id as usize { + self.methods + .resize_with(disp_id as usize + 1, Default::default); + } + + *self.methods.get_mut(disp_id as usize).unwrap() = Some(function); + } + + self.values.insert(name, Property::new_method(function)); + } + + /// Install a getter into the object. + /// + /// This is a little more complicated than methods, since virtual property + /// slots can be installed in two parts. Thus, we need to support + /// installing them in either order. + pub fn install_getter( + &mut self, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error> { + let executable: Result, Error> = function + .as_executable() + .ok_or_else(|| "Attempted to install getter without a valid method".into()); + let executable = executable?; + + if disp_id > 0 { + if self.methods.len() <= disp_id as usize { + self.methods + .resize_with(disp_id as usize + 1, Default::default); + } + + *self.methods.get_mut(disp_id as usize).unwrap() = Some(function); + } + + if !self.values.contains_key(&name) { + self.values.insert(name.clone(), Property::new_virtual()); + } + + self.values + .get_mut(&name) + .unwrap() + .install_virtual_getter(executable) + } + + /// Install a setter into the object. + /// + /// This is a little more complicated than methods, since virtual property + /// slots can be installed in two parts. Thus, we need to support + /// installing them in either order. + pub fn install_setter( + &mut self, + name: QName<'gc>, + disp_id: u32, + function: Object<'gc>, + ) -> Result<(), Error> { + let executable: Result, Error> = function + .as_executable() + .ok_or_else(|| "Attempted to install setter without a valid method".into()); + let executable = executable?; + + if disp_id > 0 { + if self.methods.len() <= disp_id as usize { + self.methods + .resize_with(disp_id as usize + 1, Default::default); + } + + *self.methods.get_mut(disp_id as usize).unwrap() = Some(function); + } + + if !self.values.contains_key(&name) { + self.values.insert(name.clone(), Property::new_virtual()); + } + + self.values + .get_mut(&name) + .unwrap() + .install_virtual_setter(executable) + } + + pub fn install_dynamic_property( + &mut self, + name: QName<'gc>, + value: Value<'gc>, + ) -> Result<(), Error> { + self.values + .insert(name, Property::new_dynamic_property(value)); + + Ok(()) + } + + /// Install a slot onto the object. + /// + /// Slot number zero indicates a slot ID that is unknown and should be + /// allocated by the VM - as far as I know, there is no way to discover + /// slot IDs, so we don't allocate a slot for them at all. + pub fn install_slot(&mut self, name: QName<'gc>, id: u32, value: Value<'gc>) { + if id == 0 { + self.values.insert(name, Property::new_stored(value)); + } else { + self.values.insert(name, Property::new_slot(id)); + if self.slots.len() < id as usize + 1 { + self.slots.resize_with(id as usize + 1, Default::default); + } + + if let Some(slot) = self.slots.get_mut(id as usize) { + *slot = Slot::new(value); + } + } + } + + /// Install a const onto the object. + /// + /// Slot number zero indicates a slot ID that is unknown and should be + /// allocated by the VM - as far as I know, there is no way to discover + /// slot IDs, so we don't allocate a slot for them at all. + pub fn install_const(&mut self, name: QName<'gc>, id: u32, value: Value<'gc>) { + if id == 0 { + self.values.insert(name, Property::new_const(value)); + } else { + self.values.insert(name, Property::new_slot(id)); + if self.slots.len() < id as usize + 1 { + self.slots.resize_with(id as usize + 1, Default::default); + } + + if let Some(slot) = self.slots.get_mut(id as usize) { + *slot = Slot::new_const(value); + } + } + } + + /// Enumerate all interfaces implemented by this object. + pub fn interfaces(&self) -> Vec> { + self.interfaces.clone() + } + + /// Set the interface list for this object. + pub fn set_interfaces(&mut self, iface_list: Vec>) { + self.interfaces = iface_list; + } +} diff --git a/core/src/avm2/slot.rs b/core/src/avm2/slot.rs new file mode 100644 index 000000000000..7c2b070a790b --- /dev/null +++ b/core/src/avm2/slot.rs @@ -0,0 +1,98 @@ +//! Slot contents type + +use crate::avm2::property::Attribute; +use crate::avm2::value::Value; +use crate::avm2::Error; +use enumset::EnumSet; +use gc_arena::{Collect, CollectionContext}; + +/// Represents a single slot on an object. +#[derive(Clone, Debug)] +pub enum Slot<'gc> { + /// An unoccupied slot. + /// + /// Attempts to read an unoccupied slot proceed up the prototype chain. + /// Writing an unoccupied slot will always fail. + Unoccupied, + + /// An occupied slot. + /// + /// TODO: For some reason, rustc believes this variant is unused. + Occupied { + value: Value<'gc>, + attributes: EnumSet, + }, +} + +unsafe impl<'gc> Collect for Slot<'gc> { + fn trace(&self, cc: CollectionContext) { + match self { + Self::Unoccupied => {} + Self::Occupied { value, .. } => value.trace(cc), + } + } +} + +impl<'gc> Default for Slot<'gc> { + fn default() -> Self { + Self::Unoccupied + } +} + +impl<'gc> Slot<'gc> { + /// Create a normal slot with a given value. + pub fn new(value: impl Into>) -> Self { + Self::Occupied { + value: value.into(), + attributes: EnumSet::empty(), + } + } + + /// Create a `const` slot that cannot be overwritten. + pub fn new_const(value: impl Into>) -> Self { + Self::Occupied { + value: value.into(), + attributes: EnumSet::from(Attribute::ReadOnly), + } + } + + /// Retrieve the value of this slot. + pub fn get(&self) -> Option> { + match self { + Self::Unoccupied => None, + Self::Occupied { value, .. } => Some(value.clone()), + } + } + + /// Write the value of this slot. + pub fn set(&mut self, new_value: impl Into>) -> Result<(), Error> { + match self { + Self::Unoccupied => Err("Cannot overwrite unoccupied slot".into()), + Self::Occupied { value, attributes } => { + if attributes.contains(Attribute::ReadOnly) { + return Err("Cannot overwrite const slot".into()); + } + + //TODO: Type assert + + *value = new_value.into(); + + Ok(()) + } + } + } + + /// Initialize a slot to a particular value. + pub fn init(&mut self, new_value: impl Into>) -> Result<(), Error> { + match self { + Self::Unoccupied => Err("Cannot initialize unoccupied slot".into()), + Self::Occupied { value, .. } => { + //TODO: Type assert + + *value = new_value.into(); + + Ok(()) + } + } + } +} diff --git a/core/src/avm2/string.rs b/core/src/avm2/string.rs new file mode 100644 index 000000000000..da58c3f2fa68 --- /dev/null +++ b/core/src/avm2/string.rs @@ -0,0 +1,3 @@ +//! AVM2 String representation + +pub use crate::avm1::AvmString; diff --git a/core/src/avm2/trait.rs b/core/src/avm2/trait.rs new file mode 100644 index 000000000000..49f52209fa49 --- /dev/null +++ b/core/src/avm2/trait.rs @@ -0,0 +1,200 @@ +//! Active trait definitions + +use crate::avm2::class::Class; +use crate::avm2::method::Method; +use crate::avm2::names::{Multiname, QName}; +use crate::avm2::script::TranslationUnit; +use crate::avm2::value::{abc_default_value, Value}; +use crate::avm2::Error; +use gc_arena::{Collect, GcCell, MutationContext}; +use swf::avm2::types::{Trait as AbcTrait, TraitKind as AbcTraitKind}; + +/// Represents a trait as loaded into the VM. +/// +/// A trait is an uninstantiated AVM2 property. Traits are used by objects to +/// track how to construct their properties when first accessed. +/// +/// This type exists primarily to support classes with native methods. Adobe's +/// implementation of AVM2 handles native classes by having a special ABC file +/// load before all other code. We instead generate an initial heap in the same +/// manner as we do in AVM1, which means that we need to have a way to +/// dynamically originate traits that do not come from any particular ABC file. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct Trait<'gc> { + /// The name of this trait. + name: QName<'gc>, + + /// Whether or not traits in downstream classes are allowed to override + /// this trait. + is_final: bool, + + /// Whether or not this trait is intended to override an upstream class's + /// trait. + is_override: bool, + + /// The kind of trait in use. + kind: TraitKind<'gc>, +} + +/// The fields for a particular kind of trait. +/// +/// The kind of a trait also determines how it's instantiated on the object. +/// See each individual variant for more information. +#[derive(Clone, Debug, Collect)] +#[collect(no_drop)] +pub enum TraitKind<'gc> { + /// A data field on an object instance that can be read from and written + /// to. + Slot { + slot_id: u32, + type_name: Multiname<'gc>, + default_value: Option>, + }, + + /// A method on an object that can be called. + Method { disp_id: u32, method: Method<'gc> }, + + /// A getter property on an object that can be read. + Getter { disp_id: u32, method: Method<'gc> }, + + /// A setter property on an object that can be written. + Setter { disp_id: u32, method: Method<'gc> }, + + /// A class property on an object that can be used to construct more + /// objects. + Class { + slot_id: u32, + class: GcCell<'gc, Class<'gc>>, + }, + + /// A free function (not an instance method) that can be called. + Function { slot_id: u32, function: Method<'gc> }, + + /// A data field on an object that is always a particular value, and cannot + /// be overridden. + Const { + slot_id: u32, + type_name: Multiname<'gc>, + default_value: Option>, + }, +} + +impl<'gc> Trait<'gc> { + /// Convert an ABC trait into a loaded trait. + pub fn from_abc_trait( + unit: TranslationUnit<'gc>, + abc_trait: &AbcTrait, + mc: MutationContext<'gc, '_>, + ) -> Result { + let name = QName::from_abc_multiname(unit, abc_trait.name.clone(), mc)?; + + Ok(match &abc_trait.kind { + AbcTraitKind::Slot { + slot_id, + type_name, + value, + } => Trait { + name, + is_final: abc_trait.is_final, + is_override: abc_trait.is_override, + kind: TraitKind::Slot { + slot_id: *slot_id, + type_name: if type_name.0 == 0 { + Multiname::any() + } else { + Multiname::from_abc_multiname_static(unit, type_name.clone(), mc)? + }, + default_value: if let Some(dv) = value { + Some(abc_default_value(unit, &dv, mc)?) + } else { + None + }, + }, + }, + AbcTraitKind::Method { disp_id, method } => Trait { + name, + is_final: abc_trait.is_final, + is_override: abc_trait.is_override, + kind: TraitKind::Method { + disp_id: *disp_id, + method: unit.load_method(method.0, mc)?, + }, + }, + AbcTraitKind::Getter { disp_id, method } => Trait { + name, + is_final: abc_trait.is_final, + is_override: abc_trait.is_override, + kind: TraitKind::Getter { + disp_id: *disp_id, + method: unit.load_method(method.0, mc)?, + }, + }, + AbcTraitKind::Setter { disp_id, method } => Trait { + name, + is_final: abc_trait.is_final, + is_override: abc_trait.is_override, + kind: TraitKind::Setter { + disp_id: *disp_id, + method: unit.load_method(method.0, mc)?, + }, + }, + AbcTraitKind::Class { slot_id, class } => Trait { + name, + is_final: abc_trait.is_final, + is_override: abc_trait.is_override, + kind: TraitKind::Class { + slot_id: *slot_id, + class: unit.load_class(class.0, mc)?, + }, + }, + AbcTraitKind::Function { slot_id, function } => Trait { + name, + is_final: abc_trait.is_final, + is_override: abc_trait.is_override, + kind: TraitKind::Function { + slot_id: *slot_id, + function: unit.load_method(function.0, mc)?, + }, + }, + AbcTraitKind::Const { + slot_id, + type_name, + value, + } => Trait { + name, + is_final: abc_trait.is_final, + is_override: abc_trait.is_override, + kind: TraitKind::Const { + slot_id: *slot_id, + type_name: if type_name.0 == 0 { + Multiname::any() + } else { + Multiname::from_abc_multiname_static(unit, type_name.clone(), mc)? + }, + default_value: if let Some(dv) = value { + Some(abc_default_value(unit, &dv, mc)?) + } else { + None + }, + }, + }, + }) + } + + pub fn name(&self) -> &QName<'gc> { + &self.name + } + + pub fn kind(&self) -> &TraitKind<'gc> { + &self.kind + } + + pub fn is_final(&self) -> bool { + self.is_final + } + + pub fn is_override(&self) -> bool { + self.is_override + } +} diff --git a/core/src/avm2/value.rs b/core/src/avm2/value.rs new file mode 100644 index 000000000000..58498f9c6187 --- /dev/null +++ b/core/src/avm2/value.rs @@ -0,0 +1,262 @@ +//! AVM2 values + +use crate::avm2::names::Namespace; +use crate::avm2::object::Object; +use crate::avm2::script::TranslationUnit; +use crate::avm2::string::AvmString; +use crate::avm2::Error; +use gc_arena::{Collect, MutationContext}; +use std::f64::NAN; +use swf::avm2::types::{DefaultValue as AbcDefaultValue, Index}; + +/// An AVM2 value. +/// +/// TODO: AVM2 also needs Scope, Namespace, and XML values. +#[derive(Clone, Collect, Debug)] +#[collect(no_drop)] +pub enum Value<'gc> { + Undefined, + Null, + Bool(bool), + Number(f64), + String(AvmString<'gc>), + Namespace(Namespace<'gc>), + Object(Object<'gc>), +} + +impl<'gc> From> for Value<'gc> { + fn from(string: AvmString<'gc>) -> Self { + Value::String(string) + } +} + +impl<'gc> From<&'static str> for Value<'gc> { + fn from(string: &'static str) -> Self { + Value::String(string.into()) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: bool) -> Self { + Value::Bool(value) + } +} + +impl<'gc, T> From for Value<'gc> +where + Object<'gc>: From, +{ + fn from(value: T) -> Self { + Value::Object(Object::from(value)) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: f64) -> Self { + Value::Number(value) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: f32) -> Self { + Value::Number(f64::from(value)) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: u8) -> Self { + Value::Number(f64::from(value)) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: i16) -> Self { + Value::Number(f64::from(value)) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: u16) -> Self { + Value::Number(f64::from(value)) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: i32) -> Self { + Value::Number(f64::from(value)) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: u32) -> Self { + Value::Number(f64::from(value)) + } +} + +impl<'gc> From for Value<'gc> { + fn from(value: usize) -> Self { + Value::Number(value as f64) + } +} + +impl<'gc> From> for Value<'gc> { + fn from(value: Namespace<'gc>) -> Self { + Value::Namespace(value) + } +} + +impl PartialEq for Value<'_> { + fn eq(&self, other: &Self) -> bool { + match self { + Value::Undefined => match other { + Value::Undefined => true, + _ => false, + }, + Value::Null => match other { + Value::Null => true, + _ => false, + }, + Value::Bool(value) => match other { + Value::Bool(other_value) => value == other_value, + _ => false, + }, + Value::Number(value) => match other { + Value::Number(other_value) => value == other_value, + _ => false, + }, + Value::String(value) => match other { + Value::String(other_value) => value == other_value, + _ => false, + }, + Value::Object(value) => match other { + Value::Object(other_value) => Object::ptr_eq(*value, *other_value), + _ => false, + }, + Value::Namespace(ns) => match other { + Value::Namespace(other_ns) => ns == other_ns, + _ => false, + }, + } + } +} + +pub fn abc_int(translation_unit: TranslationUnit<'_>, index: Index) -> Result { + if index.0 == 0 { + return Ok(0); + } + + translation_unit + .abc() + .constant_pool + .ints + .get(index.0 as usize - 1) + .cloned() + .ok_or_else(|| format!("Unknown int constant {}", index.0).into()) +} + +pub fn abc_uint(translation_unit: TranslationUnit<'_>, index: Index) -> Result { + if index.0 == 0 { + return Ok(0); + } + + translation_unit + .abc() + .constant_pool + .uints + .get(index.0 as usize - 1) + .cloned() + .ok_or_else(|| format!("Unknown uint constant {}", index.0).into()) +} + +pub fn abc_double(translation_unit: TranslationUnit<'_>, index: Index) -> Result { + if index.0 == 0 { + return Ok(NAN); + } + + translation_unit + .abc() + .constant_pool + .doubles + .get(index.0 as usize - 1) + .cloned() + .ok_or_else(|| format!("Unknown double constant {}", index.0).into()) +} + +/// Retrieve a default value as an AVM2 `Value`. +pub fn abc_default_value<'gc>( + translation_unit: TranslationUnit<'gc>, + default: &AbcDefaultValue, + mc: MutationContext<'gc, '_>, +) -> Result, Error> { + match default { + AbcDefaultValue::Int(i) => abc_int(translation_unit, *i).map(|v| v.into()), + AbcDefaultValue::Uint(u) => abc_uint(translation_unit, *u).map(|v| v.into()), + AbcDefaultValue::Double(d) => abc_double(translation_unit, *d).map(|v| v.into()), + AbcDefaultValue::String(s) => translation_unit.pool_string(s.0, mc).map(|v| v.into()), + AbcDefaultValue::True => Ok(true.into()), + AbcDefaultValue::False => Ok(false.into()), + AbcDefaultValue::Null => Ok(Value::Null), + AbcDefaultValue::Undefined => Ok(Value::Undefined), + AbcDefaultValue::Namespace(ns) + | AbcDefaultValue::Package(ns) + | AbcDefaultValue::PackageInternal(ns) + | AbcDefaultValue::Protected(ns) + | AbcDefaultValue::Explicit(ns) + | AbcDefaultValue::StaticProtected(ns) + | AbcDefaultValue::Private(ns) => { + Namespace::from_abc_namespace(translation_unit, ns.clone(), mc).map(|v| v.into()) + } + } +} + +impl<'gc> Value<'gc> { + pub fn as_object(&self) -> Result, Error> { + if let Value::Object(object) = self { + Ok(*object) + } else { + Err(format!("Expected Object, found {:?}", self).into()) + } + } + + /// Demand a string value, erroring out if one is not found. + /// + /// TODO: This should be replaced with `coerce_string` where possible. + pub fn as_string(&self) -> Result, Error> { + match self { + Value::String(s) => Ok(*s), + _ => Err(format!("Expected String, found {:?}", self).into()), + } + } + + /// Coerce a value into a string. + pub fn coerce_string(self) -> AvmString<'gc> { + match self { + Value::String(s) => s, + Value::Bool(true) => "true".into(), + Value::Bool(false) => "false".into(), + _ => "".into(), + } + } + + pub fn as_number(&self) -> Result { + match self { + Value::Number(f) => Ok(*f), + _ => Err(format!("Expected Number, found {:?}", self).into()), + } + } + + pub fn as_bool(&self) -> Result { + if let Value::Bool(b) = self { + Ok(*b) + } else { + Err(format!("Expected Boolean, found {:?}", self).into()) + } + } + + pub fn as_namespace(&self) -> Result<&Namespace<'gc>, Error> { + match self { + Value::Namespace(ns) => Ok(ns), + _ => Err(format!("Expected Namespace, found {:?}", self).into()), + } + } +} diff --git a/core/src/context.rs b/core/src/context.rs index 6619d534cb42..f06f41cdf581 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -246,6 +246,13 @@ pub enum ActionType<'gc> { method: &'static str, args: Vec>, }, + + /// AVM2 ABC files. + DoABC { + name: String, + is_lazy_initialize: bool, + abc: SwfSlice, + }, } impl fmt::Debug for ActionType<'_> { @@ -279,6 +286,16 @@ impl fmt::Debug for ActionType<'_> { .field("method", method) .field("args", args) .finish(), + ActionType::DoABC { + name, + is_lazy_initialize, + abc, + } => f + .debug_struct("ActionType::DoABC") + .field("name", name) + .field("is_lazy_initialize", is_lazy_initialize) + .field("bytecode", abc) + .finish(), } } } diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index b7b50482746b..7dfa462de012 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -134,7 +134,7 @@ impl<'gc> MovieClip<'gc> { pub fn preload( self, - avm: &mut Avm1<'gc>, + avm1: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, morph_shapes: &mut fnv::FnvHashMap, ) { @@ -250,7 +250,7 @@ impl<'gc> MovieClip<'gc> { .write(context.gc_context) .define_sound(context, reader), TagCode::DefineSprite => self.0.write(context.gc_context).define_sprite( - avm, + avm1, context, reader, tag_len, @@ -264,7 +264,8 @@ impl<'gc> MovieClip<'gc> { .0 .write(context.gc_context) .define_text(context, reader, 2), - TagCode::DoInitAction => self.do_init_action(avm, context, reader, tag_len), + TagCode::DoInitAction => self.do_init_action(avm1, context, reader, tag_len), + TagCode::DoAbc => self.do_abc(context, reader, tag_len), TagCode::ExportAssets => self .0 .write(context.gc_context) @@ -326,7 +327,7 @@ impl<'gc> MovieClip<'gc> { &mut cur_frame, ), TagCode::ScriptLimits => { - self.0.write(context.gc_context).script_limits(reader, avm) + self.0.write(context.gc_context).script_limits(reader, avm1) } TagCode::SoundStreamHead => self .0 @@ -396,6 +397,46 @@ impl<'gc> MovieClip<'gc> { Ok(()) } + #[inline] + fn do_abc( + self, + context: &mut UpdateContext<'_, 'gc, '_>, + reader: &mut SwfStream<&[u8]>, + tag_len: usize, + ) -> DecodeResult { + // Queue the actions. + // TODO: The tag reader parses the entire ABC file, instead of just + // giving us a `SwfSlice` for later parsing, so we have to replcate the + // *entire* parsing code here. This sucks. + let flags = reader.read_u32()?; + let name = reader.read_c_string()?; + let is_lazy_initialize = flags & 1 != 0; + + // The rest of the tag is an ABC file so we can take our SwfSlice now. + let slice = self + .0 + .read() + .static_data + .swf + .resize_to_reader(reader, tag_len) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Invalid source or tag length when running init action", + ) + })?; + context.action_queue.queue_actions( + self.into(), + ActionType::DoABC { + name, + is_lazy_initialize, + abc: slice, + }, + false, + ); + Ok(()) + } + #[allow(dead_code)] pub fn playing(self) -> bool { self.0.read().playing() diff --git a/core/src/lib.rs b/core/src/lib.rs index 96f7cabdd390..dec8e212c48f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,6 +10,7 @@ extern crate smallvec; extern crate downcast_rs; mod avm1; +mod avm2; mod bounding_box; mod character; pub mod color_transform; diff --git a/core/src/loader.rs b/core/src/loader.rs index 4fde1490f457..d0aed7607f60 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -298,7 +298,7 @@ impl<'gc> Loader<'gc> { Box::pin(async move { player.lock().expect("Could not lock player!!").update( - |avm, uc| -> Result<(), Error> { + |avm1, _avm2, uc| -> Result<(), Error> { let (clip, broadcaster) = match uc.load_manager.get_loader(handle) { Some(Loader::Movie { target_clip, @@ -316,7 +316,7 @@ impl<'gc> Loader<'gc> { .replace_with_movie(uc.gc_context, None); if let Some(broadcaster) = broadcaster { - avm.run_stack_frame_for_method( + avm1.run_stack_frame_for_method( clip, broadcaster, NEWEST_PLAYER_VERSION, @@ -337,7 +337,7 @@ impl<'gc> Loader<'gc> { player .lock() .expect("Could not lock player!!") - .update(|avm, uc| { + .update(|avm1, _avm2, uc| { let (clip, broadcaster) = match uc.load_manager.get_loader(handle) { Some(Loader::Movie { target_clip, @@ -349,7 +349,7 @@ impl<'gc> Loader<'gc> { }; if let Some(broadcaster) = broadcaster { - avm.run_stack_frame_for_method( + avm1.run_stack_frame_for_method( clip, broadcaster, NEWEST_PLAYER_VERSION, @@ -369,10 +369,10 @@ impl<'gc> Loader<'gc> { .expect("Attempted to load movie into not movie clip"); mc.replace_with_movie(uc.gc_context, Some(movie.clone())); - mc.post_instantiation(avm, uc, clip, None, false); + mc.post_instantiation(avm1, uc, clip, None, false); let mut morph_shapes = fnv::FnvHashMap::default(); - mc.preload(avm, uc, &mut morph_shapes); + mc.preload(avm1, uc, &mut morph_shapes); // Finalize morph shapes. for (id, static_data) in morph_shapes { @@ -386,7 +386,7 @@ impl<'gc> Loader<'gc> { } if let Some(broadcaster) = broadcaster { - avm.run_stack_frame_for_method( + avm1.run_stack_frame_for_method( clip, broadcaster, NEWEST_PLAYER_VERSION, @@ -411,7 +411,7 @@ impl<'gc> Loader<'gc> { //This also can get errors from decoding an invalid SWF file, //too. We should distinguish those to player code. player.lock().expect("Could not lock player!!").update( - |avm, uc| -> Result<(), Error> { + |avm1, _avm2, uc| -> Result<(), Error> { let (clip, broadcaster) = match uc.load_manager.get_loader(handle) { Some(Loader::Movie { target_clip, @@ -423,7 +423,7 @@ impl<'gc> Loader<'gc> { }; if let Some(broadcaster) = broadcaster { - avm.run_stack_frame_for_method( + avm1.run_stack_frame_for_method( clip, broadcaster, NEWEST_PLAYER_VERSION, @@ -467,7 +467,7 @@ impl<'gc> Loader<'gc> { Box::pin(async move { let data = fetch.await?; - player.lock().unwrap().update(|avm, uc| { + player.lock().unwrap().update(|avm1, _avm2, uc| { let loader = uc.load_manager.get_loader(handle); let that = match loader { Some(Loader::Form { target_object, .. }) => *target_object, @@ -476,13 +476,14 @@ impl<'gc> Loader<'gc> { }; let mut activation = Activation::from_nothing( - avm, + avm1, ActivationIdentifier::root("[Form Loader]"), uc.swf.version(), - avm.global_object_cell(), + avm1.global_object_cell(), uc.gc_context, *uc.levels.get(&0).unwrap(), ); + for (k, v) in form_urlencoded::parse(&data) { that.set( &k, @@ -561,7 +562,7 @@ impl<'gc> Loader<'gc> { let xmlstring = String::from_utf8(data)?; player.lock().expect("Could not lock player!!").update( - |avm, uc| -> Result<(), Error> { + |avm1, _avm2, uc| -> Result<(), Error> { let (mut node, active_clip) = match uc.load_manager.get_loader(handle) { Some(Loader::XML { target_node, @@ -573,8 +574,8 @@ impl<'gc> Loader<'gc> { }; let object = - node.script_object(uc.gc_context, Some(avm.prototypes().xml_node)); - avm.run_stack_frame_for_method( + node.script_object(uc.gc_context, Some(avm1.prototypes().xml_node)); + avm1.run_stack_frame_for_method( active_clip, object, NEWEST_PLAYER_VERSION, @@ -583,7 +584,7 @@ impl<'gc> Loader<'gc> { &[200.into()], ); - avm.run_stack_frame_for_method( + avm1.run_stack_frame_for_method( active_clip, object, NEWEST_PLAYER_VERSION, @@ -597,7 +598,7 @@ impl<'gc> Loader<'gc> { )?; } else { player.lock().expect("Could not lock player!!").update( - |avm, uc| -> Result<(), Error> { + |avm1, _avm2, uc| -> Result<(), Error> { let (mut node, active_clip) = match uc.load_manager.get_loader(handle) { Some(Loader::XML { target_node, @@ -609,8 +610,9 @@ impl<'gc> Loader<'gc> { }; let object = - node.script_object(uc.gc_context, Some(avm.prototypes().xml_node)); - avm.run_stack_frame_for_method( + node.script_object(uc.gc_context, Some(avm1.prototypes().xml_node)); + + avm1.run_stack_frame_for_method( active_clip, object, NEWEST_PLAYER_VERSION, @@ -619,7 +621,7 @@ impl<'gc> Loader<'gc> { &[404.into()], ); - avm.run_stack_frame_for_method( + avm1.run_stack_frame_for_method( active_clip, object, NEWEST_PLAYER_VERSION, diff --git a/core/src/player.rs b/core/src/player.rs index b0aaa5901f53..5ced943baf64 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -4,6 +4,7 @@ use crate::avm1::globals::system::SystemProperties; use crate::avm1::listeners::SystemListener; use crate::avm1::object::Object; use crate::avm1::{Avm1, AvmString, TObject, Timers, Value}; +use crate::avm2::Avm2; use crate::backend::input::{InputBackend, MouseCursor}; use crate::backend::storage::StorageBackend; use crate::backend::{ @@ -53,7 +54,12 @@ struct GcRootData<'gc> { /// The object being dragged via a `startDrag` action. drag_object: Option>, - avm: Avm1<'gc>, + /// Interpreter state for AVM1 code. + avm1: Avm1<'gc>, + + /// Interpreter state for AVM2 code. + avm2: Avm2<'gc>, + action_queue: ActionQueue<'gc>, /// Object which manages asynchronous processes that need to interact with @@ -80,6 +86,7 @@ impl<'gc> GcRootData<'gc> { &mut Library<'gc>, &mut ActionQueue<'gc>, &mut Avm1<'gc>, + &mut Avm2<'gc>, &mut Option>, &mut LoadManager<'gc>, &mut HashMap>, @@ -90,7 +97,8 @@ impl<'gc> GcRootData<'gc> { &mut self.levels, &mut self.library, &mut self.action_queue, - &mut self.avm, + &mut self.avm1, + &mut self.avm2, &mut self.drag_object, &mut self.load_manager, &mut self.shared_objects, @@ -238,7 +246,8 @@ impl Player { levels: BTreeMap::new(), mouse_hovered_object: None, drag_object: None, - avm: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), + avm1: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), + avm2: Avm2::new(gc_context), action_queue: ActionQueue::new(), load_manager: LoadManager::new(), shared_objects: HashMap::new(), @@ -272,19 +281,19 @@ impl Player { storage, }; - player.mutate_with_update_context(|avm, context| { + player.mutate_with_update_context(|avm1, _avm2, context| { let mut root: DisplayObject = MovieClip::from_movie(context.gc_context, movie.clone()).into(); root.set_depth(context.gc_context, 0); - root.post_instantiation(avm, context, root, None, false); + root.post_instantiation(avm1, context, root, None, false); root.set_name(context.gc_context, ""); context.levels.insert(0, root); let mut activation = Activation::from_nothing( - avm, + avm1, ActivationIdentifier::root("[Version Setter]"), context.swf.version(), - avm.global_object_cell(), + avm1.global_object_cell(), context.gc_context, *context.levels.get(&0).unwrap(), ); @@ -405,14 +414,14 @@ impl Player { } = event { if self.input.is_key_down(KeyCode::Control) && self.input.is_key_down(KeyCode::Alt) { - self.mutate_with_update_context(|avm, context| { + self.mutate_with_update_context(|avm1, _avm2, context| { let mut dumper = VariableDumper::new(" "); let mut activation = Activation::from_nothing( - avm, + avm1, ActivationIdentifier::root("[Variable Dumper]"), context.swf.version(), - avm.global_object_cell(), + avm1.global_object_cell(), context.gc_context, *context.levels.get(&0).unwrap(), ); @@ -477,11 +486,11 @@ impl Player { }; if button_event.is_some() { - self.mutate_with_update_context(|avm, context| { + self.mutate_with_update_context(|avm1, _avm2, context| { let levels: Vec> = context.levels.values().copied().collect(); for level in levels { if let Some(button_event) = button_event { - let state = level.handle_clip_event(avm, context, button_event); + let state = level.handle_clip_event(avm1, context, button_event); if state == ClipEventResult::Handled { return; } @@ -501,12 +510,12 @@ impl Player { }; if clip_event.is_some() || mouse_event_name.is_some() { - self.mutate_with_update_context(|avm, context| { + self.mutate_with_update_context(|avm1, _avm2, context| { let levels: Vec> = context.levels.values().copied().collect(); for level in levels { if let Some(clip_event) = clip_event { - level.handle_clip_event(avm, context, clip_event); + level.handle_clip_event(avm1, context, clip_event); } } @@ -525,7 +534,7 @@ impl Player { } let mut is_mouse_down = self.is_mouse_down; - self.mutate_with_update_context(|avm, context| { + self.mutate_with_update_context(|avm1, avm2, context| { if let Some(node) = context.mouse_hovered_object { if node.removed() { context.mouse_hovered_object = None; @@ -537,7 +546,7 @@ impl Player { is_mouse_down = true; needs_render = true; if let Some(node) = context.mouse_hovered_object { - node.handle_clip_event(avm, context, ClipEvent::Press); + node.handle_clip_event(avm1, context, ClipEvent::Press); } } @@ -545,14 +554,14 @@ impl Player { is_mouse_down = false; needs_render = true; if let Some(node) = context.mouse_hovered_object { - node.handle_clip_event(avm, context, ClipEvent::Release); + node.handle_clip_event(avm1, context, ClipEvent::Release); } } _ => (), } - Self::run_actions(avm, context); + Self::run_actions(avm1, avm2, context); }); self.is_mouse_down = is_mouse_down; if needs_render { @@ -563,7 +572,7 @@ impl Player { /// Update dragged object, if any. fn update_drag(&mut self) { let mouse_pos = self.mouse_pos; - self.mutate_with_update_context(|_activation, context| { + self.mutate_with_update_context(|_avm1, _avm2, context| { if let Some(drag_object) = &mut context.drag_object { if drag_object.display_object.removed() { // Be sure to clear the drag if the object was removed. @@ -598,13 +607,13 @@ impl Player { let mouse_pos = self.mouse_pos; let mut new_cursor = self.mouse_cursor; - let hover_changed = self.mutate_with_update_context(|avm, context| { + let hover_changed = self.mutate_with_update_context(|avm1, avm2, context| { // Check hovered object. let mut new_hovered = None; for (_depth, level) in context.levels.clone().iter().rev() { if new_hovered.is_none() { new_hovered = - level.mouse_pick(avm, context, *level, (mouse_pos.0, mouse_pos.1)); + level.mouse_pick(avm1, context, *level, (mouse_pos.0, mouse_pos.1)); } else { break; } @@ -616,20 +625,20 @@ impl Player { // RollOut of previous node. if let Some(node) = cur_hovered { if !node.removed() { - node.handle_clip_event(avm, context, ClipEvent::RollOut); + node.handle_clip_event(avm1, context, ClipEvent::RollOut); } } - // RollOver on new node. + // RollOver on new node.I stil new_cursor = MouseCursor::Arrow; if let Some(node) = new_hovered { new_cursor = MouseCursor::Hand; - node.handle_clip_event(avm, context, ClipEvent::RollOver); + node.handle_clip_event(avm1, context, ClipEvent::RollOver); } context.mouse_hovered_object = new_hovered; - Self::run_actions(avm, context); + Self::run_actions(avm1, avm2, context); true } else { false @@ -650,12 +659,12 @@ impl Player { /// This should only be called once. Further movie loads should preload the /// specific `MovieClip` referenced. fn preload(&mut self) { - self.mutate_with_update_context(|activation, context| { + self.mutate_with_update_context(|avm1, _avm2, context| { let mut morph_shapes = fnv::FnvHashMap::default(); let root = *context.levels.get(&0).expect("root level"); root.as_movie_clip() .unwrap() - .preload(activation, context, &mut morph_shapes); + .preload(avm1, context, &mut morph_shapes); // Finalize morph shapes. for (id, static_data) in morph_shapes { @@ -669,7 +678,7 @@ impl Player { } pub fn run_frame(&mut self) { - self.update(|avm, update_context| { + self.update(|avm1, _avm2, update_context| { // TODO: In what order are levels run? // NOTE: We have to copy all the layer pointers into a separate list // because level updates can create more levels, which we don't @@ -677,7 +686,7 @@ impl Player { let levels: Vec<_> = update_context.levels.values().copied().collect(); for mut level in levels { - level.run_frame(avm, update_context); + level.run_frame(avm1, update_context); } }); self.needs_render = true; @@ -750,7 +759,11 @@ impl Player { self.input.deref_mut() } - fn run_actions<'gc>(avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>) { + fn run_actions<'gc>( + avm1: &mut Avm1<'gc>, + avm2: &mut Avm2<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) { // Note that actions can queue further actions, so a while loop is necessary here. while let Some(actions) = context.action_queue.pop_action() { // We don't run frame actions if the clip was removed after it queued the action. @@ -761,7 +774,7 @@ impl Player { match actions.action_type { // DoAction/clip event code ActionType::Normal { bytecode } => { - avm.run_stack_frame_for_action( + avm1.run_stack_frame_for_action( actions.clip, "[Frame]", context.swf.header().version, @@ -775,10 +788,10 @@ impl Player { events, } => { let mut activation = Activation::from_nothing( - avm, + avm1, ActivationIdentifier::root("[Construct]"), context.swf.version(), - avm.global_object_cell(), + avm1.global_object_cell(), context.gc_context, *context.levels.get(&0).unwrap(), ); @@ -815,7 +828,7 @@ impl Player { events, } => { for event in events { - avm.run_stack_frame_for_action( + avm1.run_stack_frame_for_action( actions.clip, "[Construct]", context.swf.header().version, @@ -826,7 +839,7 @@ impl Player { } // Event handler method call (e.g. onEnterFrame) ActionType::Method { object, name, args } => { - avm.run_stack_frame_for_method( + avm1.run_stack_frame_for_method( actions.clip, object, context.swf.header().version, @@ -844,7 +857,7 @@ impl Player { } => { // A native function ends up resolving immediately, // so this doesn't require any further execution. - avm.notify_system_listeners( + avm1.notify_system_listeners( actions.clip, context.swf.version(), context, @@ -853,6 +866,17 @@ impl Player { &args, ); } + + // DoABC code + ActionType::DoABC { + name, + is_lazy_initialize, + abc, + } => { + if let Err(e) = avm2.load_abc(abc, &name, is_lazy_initialize, context) { + log::warn!("Error loading ABC file: {}", e); + } + } } } } @@ -898,7 +922,11 @@ impl Player { /// This takes cares of populating the `UpdateContext` struct, avoiding borrow issues. fn mutate_with_update_context(&mut self, f: F) -> R where - F: for<'a, 'gc> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>) -> R, + F: for<'a, 'gc> FnOnce( + &mut Avm1<'gc>, + &mut Avm2<'gc>, + &mut UpdateContext<'a, 'gc, '_>, + ) -> R, { // We have to do this piecewise borrowing of fields before the closure to avoid // completely borrowing `self`. @@ -945,7 +973,8 @@ impl Player { levels, library, action_queue, - avm, + avm1, + avm2, drag_object, load_manager, shared_objects, @@ -970,7 +999,7 @@ impl Player { mouse_position, drag_object, stage_size: (stage_width, stage_height), - system_prototypes: avm.prototypes().clone(), + system_prototypes: avm1.prototypes().clone(), player, load_manager, system: system_properties, @@ -982,7 +1011,7 @@ impl Player { needs_render, }; - let ret = f(avm, &mut update_context); + let ret = f(avm1, avm2, &mut update_context); // Hovered object may have been updated; copy it back to the GC root. root_data.mouse_hovered_object = update_context.mouse_hovered_object; @@ -1017,12 +1046,16 @@ impl Player { /// hover state up to date, and running garbage collection. pub fn update(&mut self, func: F) -> R where - F: for<'a, 'gc> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>) -> R, + F: for<'a, 'gc> FnOnce( + &mut Avm1<'gc>, + &mut Avm2<'gc>, + &mut UpdateContext<'a, 'gc, '_>, + ) -> R, { - let rval = self.mutate_with_update_context(|avm, context| { - let rval = func(avm, context); + let rval = self.mutate_with_update_context(|avm1, avm2, context| { + let rval = func(avm1, avm2, context); - Self::run_actions(avm, context); + Self::run_actions(avm1, avm2, context); rval }); @@ -1038,12 +1071,12 @@ impl Player { } pub fn flush_shared_objects(&mut self) { - self.update(|avm, context| { + self.update(|avm1, _avm2, context| { let mut activation = Activation::from_nothing( - avm, + avm1, ActivationIdentifier::root("[Flush]"), context.swf.version(), - avm.global_object_cell(), + avm1.global_object_cell(), context.gc_context, *context.levels.get(&0).unwrap(), ); @@ -1058,8 +1091,9 @@ impl Player { /// Update all AVM-based timers (such as created via setInterval). /// Returns the approximate amount of time until the next timer tick. pub fn update_timers(&mut self, dt: f64) { - self.time_til_next_timer = - self.mutate_with_update_context(|avm, context| Timers::update_timers(avm, context, dt)); + self.time_til_next_timer = self.mutate_with_update_context(|avm1, _avm2, context| { + Timers::update_timers(avm1, context, dt) + }); } } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index bb809260779b..d64b30258b36 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -227,6 +227,40 @@ swf_tests! { (set_interval, "avm1/set_interval", 20), (context_menu, "avm1/context_menu", 1), (context_menu_item, "avm1/context_menu_item", 1), + (as3_hello_world, "avm2/hello_world", 1), + (as3_function_call, "avm2/function_call", 1), + (as3_function_call_via_call, "avm2/function_call_via_call", 1), + (as3_constructor_call, "avm2/constructor_call", 1), + (as3_class_methods, "avm2/class_methods", 1), + (as3_es3_inheritance, "avm2/es3_inheritance", 1), + (as3_es4_inheritance, "avm2/es4_inheritance", 1), + (as3_stored_properties, "avm2/stored_properties", 1), + (as3_virtual_properties, "avm2/virtual_properties", 1), + (as3_es4_oop_prototypes, "avm2/es4_oop_prototypes", 1), + (as3_es4_method_binding, "avm2/es4_method_binding", 1), + (as3_control_flow_bool, "avm2/control_flow_bool", 1), + (as3_control_flow_stricteq, "avm2/control_flow_stricteq", 1), + (as3_object_enumeration, "avm2/object_enumeration", 1), + (as3_class_enumeration, "avm2/class_enumeration", 1), + (as3_is_prototype_of, "avm2/is_prototype_of", 1), + (as3_has_own_property, "avm2/has_own_property", 1), + (as3_property_is_enumerable, "avm2/property_is_enumerable", 1), + (as3_set_property_is_enumerable, "avm2/set_property_is_enumerable", 1), + (as3_object_to_string, "avm2/object_to_string", 1), + (as3_function_to_string, "avm2/function_to_string", 1), + (as3_class_to_string, "avm2/class_to_string", 1), + (as3_object_to_locale_string, "avm2/object_to_locale_string", 1), + (as3_function_to_locale_string, "avm2/function_to_locale_string", 1), + (as3_class_to_locale_string, "avm2/class_to_locale_string", 1), + (as3_object_value_of, "avm2/object_value_of", 1), + (as3_function_value_of, "avm2/function_value_of", 1), + (as3_class_value_of, "avm2/class_value_of", 1), + (as3_if_stricteq, "avm2/if_stricteq", 1), + (as3_if_strictne, "avm2/if_strictne", 1), + (as3_strict_equality, "avm2/strict_equality", 1), + (as3_es4_interfaces, "avm2/es4_interfaces", 1), + (as3_istype, "avm2/istype", 1), + (as3_instanceof, "avm2/instanceof", 1), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm2/class_enumeration/Test.as b/core/tests/swfs/avm2/class_enumeration/Test.as new file mode 100644 index 000000000000..34b506010f9d --- /dev/null +++ b/core/tests/swfs/avm2/class_enumeration/Test.as @@ -0,0 +1,17 @@ +package { + public class Test {} +} + +dynamic class ES4Class { + public var variable = "TEST FAIL: Trait properties are NOT enumerable! (var)"; + public const constant = "TEST FAIL: Trait properties are NOT enumerable! (const)"; +} + +var x = new ES4Class(); +x.dynamic_variable = "variable value"; +ES4Class.prototype.dynamic_prototype_variable = "prototype value"; + +for (var name in x) { + trace(name); + trace(x[name]); +} \ No newline at end of file diff --git a/core/tests/swfs/avm2/class_enumeration/output.txt b/core/tests/swfs/avm2/class_enumeration/output.txt new file mode 100644 index 000000000000..ccd06b32894d --- /dev/null +++ b/core/tests/swfs/avm2/class_enumeration/output.txt @@ -0,0 +1,4 @@ +dynamic_variable +variable value +dynamic_prototype_variable +prototype value diff --git a/core/tests/swfs/avm2/class_enumeration/test.fla b/core/tests/swfs/avm2/class_enumeration/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/class_enumeration/test.fla differ diff --git a/core/tests/swfs/avm2/class_enumeration/test.swf b/core/tests/swfs/avm2/class_enumeration/test.swf new file mode 100644 index 000000000000..984bd1d56343 Binary files /dev/null and b/core/tests/swfs/avm2/class_enumeration/test.swf differ diff --git a/core/tests/swfs/avm2/class_methods/Test.as b/core/tests/swfs/avm2/class_methods/Test.as new file mode 100644 index 000000000000..34e857907cc8 --- /dev/null +++ b/core/tests/swfs/avm2/class_methods/Test.as @@ -0,0 +1,27 @@ +package { + public class Test { + } +} + +class Test2 { + { + trace("Class constructor"); + } + + function Test2() { + trace("Instance constructor"); + } + + static function classMethod() { + trace("Class method"); + } + + function method() { + trace("Instance method"); + } +} + +trace("Script initializer"); +Test2.classMethod(); +var x = new Test2(); +x.method(); \ No newline at end of file diff --git a/core/tests/swfs/avm2/class_methods/output.txt b/core/tests/swfs/avm2/class_methods/output.txt new file mode 100644 index 000000000000..188d37d197e8 --- /dev/null +++ b/core/tests/swfs/avm2/class_methods/output.txt @@ -0,0 +1,5 @@ +Class constructor +Script initializer +Class method +Instance constructor +Instance method diff --git a/core/tests/swfs/avm2/class_methods/test.fla b/core/tests/swfs/avm2/class_methods/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/class_methods/test.fla differ diff --git a/core/tests/swfs/avm2/class_methods/test.swf b/core/tests/swfs/avm2/class_methods/test.swf new file mode 100644 index 000000000000..9fce5d135154 Binary files /dev/null and b/core/tests/swfs/avm2/class_methods/test.swf differ diff --git a/core/tests/swfs/avm2/class_to_locale_string/Test.as b/core/tests/swfs/avm2/class_to_locale_string/Test.as new file mode 100644 index 000000000000..6598f397e1f6 --- /dev/null +++ b/core/tests/swfs/avm2/class_to_locale_string/Test.as @@ -0,0 +1,10 @@ +package { + public class Test {} +} + +class ES4Class { + +} + +trace("//ES4Class.toLocaleString()"); +trace(ES4Class.toLocaleString()); \ No newline at end of file diff --git a/core/tests/swfs/avm2/class_to_locale_string/output.txt b/core/tests/swfs/avm2/class_to_locale_string/output.txt new file mode 100644 index 000000000000..8a41ded47582 --- /dev/null +++ b/core/tests/swfs/avm2/class_to_locale_string/output.txt @@ -0,0 +1,2 @@ +//ES4Class.toLocaleString() +[class ES4Class] diff --git a/core/tests/swfs/avm2/class_to_locale_string/test.fla b/core/tests/swfs/avm2/class_to_locale_string/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/class_to_locale_string/test.fla differ diff --git a/core/tests/swfs/avm2/class_to_locale_string/test.swf b/core/tests/swfs/avm2/class_to_locale_string/test.swf new file mode 100644 index 000000000000..59f5e0b5bf02 Binary files /dev/null and b/core/tests/swfs/avm2/class_to_locale_string/test.swf differ diff --git a/core/tests/swfs/avm2/class_to_string/Test.as b/core/tests/swfs/avm2/class_to_string/Test.as new file mode 100644 index 000000000000..d9eccd9a9ac5 --- /dev/null +++ b/core/tests/swfs/avm2/class_to_string/Test.as @@ -0,0 +1,10 @@ +package { + public class Test {} +} + +class ES4Class { + +} + +trace("//ES4Class.toString()"); +trace(ES4Class.toString()); \ No newline at end of file diff --git a/core/tests/swfs/avm2/class_to_string/output.txt b/core/tests/swfs/avm2/class_to_string/output.txt new file mode 100644 index 000000000000..9c7c7c8649ae --- /dev/null +++ b/core/tests/swfs/avm2/class_to_string/output.txt @@ -0,0 +1,2 @@ +//ES4Class.toString() +[class ES4Class] diff --git a/core/tests/swfs/avm2/class_to_string/test.fla b/core/tests/swfs/avm2/class_to_string/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/class_to_string/test.fla differ diff --git a/core/tests/swfs/avm2/class_to_string/test.swf b/core/tests/swfs/avm2/class_to_string/test.swf new file mode 100644 index 000000000000..11448fc01f8c Binary files /dev/null and b/core/tests/swfs/avm2/class_to_string/test.swf differ diff --git a/core/tests/swfs/avm2/class_value_of/Test.as b/core/tests/swfs/avm2/class_value_of/Test.as new file mode 100644 index 000000000000..7f8b320edcd1 --- /dev/null +++ b/core/tests/swfs/avm2/class_value_of/Test.as @@ -0,0 +1,10 @@ +package { + public class Test {} +} + +class ES4Class { + +} + +trace("//ES4Class.valueOf() === ES4Class"); +trace(ES4Class.valueOf() === ES4Class); \ No newline at end of file diff --git a/core/tests/swfs/avm2/class_value_of/output.txt b/core/tests/swfs/avm2/class_value_of/output.txt new file mode 100644 index 000000000000..199f9dd8a297 --- /dev/null +++ b/core/tests/swfs/avm2/class_value_of/output.txt @@ -0,0 +1,2 @@ +//ES4Class.valueOf() === ES4Class +true diff --git a/core/tests/swfs/avm2/class_value_of/test.fla b/core/tests/swfs/avm2/class_value_of/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/class_value_of/test.fla differ diff --git a/core/tests/swfs/avm2/class_value_of/test.swf b/core/tests/swfs/avm2/class_value_of/test.swf new file mode 100644 index 000000000000..ba4f6e441aca Binary files /dev/null and b/core/tests/swfs/avm2/class_value_of/test.swf differ diff --git a/core/tests/swfs/avm2/constructor_call/Test.as b/core/tests/swfs/avm2/constructor_call/Test.as new file mode 100644 index 000000000000..ab4401bb16e5 --- /dev/null +++ b/core/tests/swfs/avm2/constructor_call/Test.as @@ -0,0 +1,14 @@ +package { + public class Test { + } +} + +class Test2 { + function Test2(v1, v2, v3) { + trace(v1); + trace(v2); + trace(v3); + } +} + +new Test2("arg1", "arg2", "arg3"); \ No newline at end of file diff --git a/core/tests/swfs/avm2/constructor_call/output.txt b/core/tests/swfs/avm2/constructor_call/output.txt new file mode 100644 index 000000000000..6a8e7e73b1ea --- /dev/null +++ b/core/tests/swfs/avm2/constructor_call/output.txt @@ -0,0 +1,3 @@ +arg1 +arg2 +arg3 diff --git a/core/tests/swfs/avm2/constructor_call/test.fla b/core/tests/swfs/avm2/constructor_call/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/constructor_call/test.fla differ diff --git a/core/tests/swfs/avm2/constructor_call/test.swf b/core/tests/swfs/avm2/constructor_call/test.swf new file mode 100644 index 000000000000..6104d91193c5 Binary files /dev/null and b/core/tests/swfs/avm2/constructor_call/test.swf differ diff --git a/core/tests/swfs/avm2/control_flow_bool/Test.as b/core/tests/swfs/avm2/control_flow_bool/Test.as new file mode 100644 index 000000000000..aeeb49045de3 --- /dev/null +++ b/core/tests/swfs/avm2/control_flow_bool/Test.as @@ -0,0 +1,30 @@ +package { + public class Test {} +} + +var t = true; +var f = false; + +if (t) { + trace("if (true)"); +} else { + trace("TEST FAIL: if (true)"); +} + +if (!f) { + trace("if (!false)"); +} else { + trace("TEST FAIL: if (!false)"); +} + +if (!t) { + trace("TEST FAIL: if (!true)"); +} else { + trace("if (!true)"); +} + +if (f) { + trace("TEST FAIL: if (false)"); +} else { + trace("if (false)"); +} \ No newline at end of file diff --git a/core/tests/swfs/avm2/control_flow_bool/output.txt b/core/tests/swfs/avm2/control_flow_bool/output.txt new file mode 100644 index 000000000000..c1e3ebcab604 --- /dev/null +++ b/core/tests/swfs/avm2/control_flow_bool/output.txt @@ -0,0 +1,4 @@ +if (true) +if (!false) +if (!true) +if (false) diff --git a/core/tests/swfs/avm2/control_flow_bool/test.fla b/core/tests/swfs/avm2/control_flow_bool/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/control_flow_bool/test.fla differ diff --git a/core/tests/swfs/avm2/control_flow_bool/test.swf b/core/tests/swfs/avm2/control_flow_bool/test.swf new file mode 100644 index 000000000000..e691fb87b525 Binary files /dev/null and b/core/tests/swfs/avm2/control_flow_bool/test.swf differ diff --git a/core/tests/swfs/avm2/control_flow_stricteq/Test.as b/core/tests/swfs/avm2/control_flow_stricteq/Test.as new file mode 100644 index 000000000000..bbb32a797415 --- /dev/null +++ b/core/tests/swfs/avm2/control_flow_stricteq/Test.as @@ -0,0 +1,54 @@ +package { + public class Test {} +} + +var t = true; +var f = false; + +if (t === t) { + trace("if (true === true)"); +} else { + trace("TEST FAIL: if (true === true)"); +} + +if (t === f) { + trace("TEST FAIL: if (true === false)"); +} else { + trace("if (true === false)"); +} + +if (f === t) { + trace("TEST FAIL: if (false === true)"); +} else { + trace("if (false === true)"); +} + +if (f === f) { + trace("if (false === false)"); +} else { + trace("TEST FAIL: if (false === false)"); +} + +if (t !== t) { + trace("TEST FAIL: if (true !== true)"); +} else { + trace("if (true !== true)"); +} + +if (t !== f) { + trace("if (true !== false)"); +} else { + trace("TEST FAIL: if (true !== false)"); +} + +if (f !== t) { + trace("if (false !== true)"); +} else { + trace("TEST FAIL: if (false !== true)"); +} + +if (f !== f) { + trace("TEST FAIL: if (false !== false)"); +} else { + trace("if (false !== false)"); +} \ No newline at end of file diff --git a/core/tests/swfs/avm2/control_flow_stricteq/output.txt b/core/tests/swfs/avm2/control_flow_stricteq/output.txt new file mode 100644 index 000000000000..d92f63df2667 --- /dev/null +++ b/core/tests/swfs/avm2/control_flow_stricteq/output.txt @@ -0,0 +1,8 @@ +if (true === true) +if (true === false) +if (false === true) +if (false === false) +if (true !== true) +if (true !== false) +if (false !== true) +if (false !== false) diff --git a/core/tests/swfs/avm2/control_flow_stricteq/test.fla b/core/tests/swfs/avm2/control_flow_stricteq/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/control_flow_stricteq/test.fla differ diff --git a/core/tests/swfs/avm2/control_flow_stricteq/test.swf b/core/tests/swfs/avm2/control_flow_stricteq/test.swf new file mode 100644 index 000000000000..421426e41a0e Binary files /dev/null and b/core/tests/swfs/avm2/control_flow_stricteq/test.swf differ diff --git a/core/tests/swfs/avm2/es3_inheritance/Test.as b/core/tests/swfs/avm2/es3_inheritance/Test.as new file mode 100644 index 000000000000..9d4d0536bc2a --- /dev/null +++ b/core/tests/swfs/avm2/es3_inheritance/Test.as @@ -0,0 +1,67 @@ +package { + public class Test { + } +} + +function Test2() { + trace("Instance constructor"); +} + +Test2.prototype.method = function() { + trace("Instance method"); +} + +Test2.prototype.method2 = function() { + trace("Instance method 2"); +} + +function Test3() { + trace("Child instance constructor pre-super"); + Test2.call(this); + trace("Child instance constructor post-super"); +} + +Test3.prototype = new Test2(); + +Test3.prototype.method = function() { + trace("Child instance method pre-super"); + Test2.prototype.method.call(this); + trace("Child instance method post-super"); +} + +Test3.prototype.method3 = function() { + trace("Child instance method3 pre-super"); + Test2.prototype.method.call(this); + trace("Child instance method3 post-super"); +} + +function Test4() { + trace("Grandchild instance constructor pre-super"); + Test3.call(this); + trace("Grandchild instance constructor post-super"); +} + +Test4.prototype = new Test3(); + +Test4.prototype.method2 = function () { + trace("Grandchild instance method2 pre-super"); + Test3.prototype.method2.call(this); + trace("Grandchild instance method2 post-super"); +} + +Test4.prototype.method3 = function () { + trace("Grandchild instance method3 pre-super"); + Test3.prototype.method3.call(this); + trace("Grandchild instance method3 post-super"); +} + +trace("Script initializer"); +var x = new Test3(); +x.method(); +x.method2(); +x.method3(); + +var y = new Test4(); +y.method(); +y.method2(); +y.method3(); \ No newline at end of file diff --git a/core/tests/swfs/avm2/es3_inheritance/output.txt b/core/tests/swfs/avm2/es3_inheritance/output.txt new file mode 100644 index 000000000000..3d03db4d47ee --- /dev/null +++ b/core/tests/swfs/avm2/es3_inheritance/output.txt @@ -0,0 +1,31 @@ +Instance constructor +Child instance constructor pre-super +Instance constructor +Child instance constructor post-super +Script initializer +Child instance constructor pre-super +Instance constructor +Child instance constructor post-super +Child instance method pre-super +Instance method +Child instance method post-super +Instance method 2 +Child instance method3 pre-super +Instance method +Child instance method3 post-super +Grandchild instance constructor pre-super +Child instance constructor pre-super +Instance constructor +Child instance constructor post-super +Grandchild instance constructor post-super +Child instance method pre-super +Instance method +Child instance method post-super +Grandchild instance method2 pre-super +Instance method 2 +Grandchild instance method2 post-super +Grandchild instance method3 pre-super +Child instance method3 pre-super +Instance method +Child instance method3 post-super +Grandchild instance method3 post-super diff --git a/core/tests/swfs/avm2/es3_inheritance/test.fla b/core/tests/swfs/avm2/es3_inheritance/test.fla new file mode 100644 index 000000000000..6c88e118b224 Binary files /dev/null and b/core/tests/swfs/avm2/es3_inheritance/test.fla differ diff --git a/core/tests/swfs/avm2/es3_inheritance/test.swf b/core/tests/swfs/avm2/es3_inheritance/test.swf new file mode 100644 index 000000000000..a9d6767c51db Binary files /dev/null and b/core/tests/swfs/avm2/es3_inheritance/test.swf differ diff --git a/core/tests/swfs/avm2/es4_inheritance/Test.as b/core/tests/swfs/avm2/es4_inheritance/Test.as new file mode 100644 index 000000000000..ab3c3d3d6cea --- /dev/null +++ b/core/tests/swfs/avm2/es4_inheritance/Test.as @@ -0,0 +1,81 @@ +package { + public class Test { + } +} + +class Test2 { + { + trace("Class constructor"); + } + + function Test2() { + trace("Instance constructor"); + } + + function method() { + trace("Instance method"); + } + + function method2() { + trace("Instance method 2"); + } +} + +class Test3 extends Test2 { + { + trace("Child class constructor"); + } + + function Test3() { + trace("Child instance constructor pre-super"); + super(); + trace("Child instance constructor post-super"); + } + + override function method() { + trace("Child instance method pre-super"); + super.method(); + trace("Child instance method post-super"); + } + + function method3() { + trace("Child instance method3 pre-super"); + super.method(); + trace("Child instance method3 post-super"); + } +} + +class Test4 extends Test3 { + { + trace("Grandchild class constructor"); + } + + function Test4() { + trace("Grandchild instance constructor pre-super"); + super(); + trace("Grandchild instance constructor post-super"); + } + + override function method2() { + trace("Grandchild instance method2 pre-super"); + super.method2(); + trace("Grandchild instance method2 post-super"); + } + + override function method3() { + trace("Grandchild instance method3 pre-super"); + super.method3(); + trace("Grandchild instance method3 post-super"); + } +} + +trace("Script initializer"); +var x = new Test3(); +x.method(); +x.method2(); +x.method3(); + +var y = new Test4(); +y.method(); +y.method2(); +y.method3(); \ No newline at end of file diff --git a/core/tests/swfs/avm2/es4_inheritance/output.txt b/core/tests/swfs/avm2/es4_inheritance/output.txt new file mode 100644 index 000000000000..3b93ab9bbf0d --- /dev/null +++ b/core/tests/swfs/avm2/es4_inheritance/output.txt @@ -0,0 +1,30 @@ +Class constructor +Child class constructor +Grandchild class constructor +Script initializer +Child instance constructor pre-super +Instance constructor +Child instance constructor post-super +Child instance method pre-super +Instance method +Child instance method post-super +Instance method 2 +Child instance method3 pre-super +Instance method +Child instance method3 post-super +Grandchild instance constructor pre-super +Child instance constructor pre-super +Instance constructor +Child instance constructor post-super +Grandchild instance constructor post-super +Child instance method pre-super +Instance method +Child instance method post-super +Grandchild instance method2 pre-super +Instance method 2 +Grandchild instance method2 post-super +Grandchild instance method3 pre-super +Child instance method3 pre-super +Instance method +Child instance method3 post-super +Grandchild instance method3 post-super diff --git a/core/tests/swfs/avm2/es4_inheritance/test.fla b/core/tests/swfs/avm2/es4_inheritance/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/es4_inheritance/test.fla differ diff --git a/core/tests/swfs/avm2/es4_inheritance/test.swf b/core/tests/swfs/avm2/es4_inheritance/test.swf new file mode 100644 index 000000000000..5234147940b5 Binary files /dev/null and b/core/tests/swfs/avm2/es4_inheritance/test.swf differ diff --git a/core/tests/swfs/avm2/es4_interfaces/Test.as b/core/tests/swfs/avm2/es4_interfaces/Test.as new file mode 100644 index 000000000000..37616d3465a5 --- /dev/null +++ b/core/tests/swfs/avm2/es4_interfaces/Test.as @@ -0,0 +1,98 @@ +package { + public class Test { + } +} + +interface ITest2 { + { + trace("TEST FAIL: ITest2 class constructor should not run"); + } + + function method(); + function method2(); +} + +class Test2 implements ITest2 { + { + trace("Class constructor"); + } + + function Test2() { + trace("Instance constructor"); + } + + public function method() { + trace("Instance method"); + } + + public function method2() { + trace("Instance method 2"); + } +} + +interface ITest3 extends ITest2 { + { + trace("TEST FAIL: ITest3 class constructor should not run"); + } + + function method3() +} + +class Test3 extends Test2 implements ITest3 { + { + trace("Child class constructor"); + } + + function Test3() { + trace("Child instance constructor pre-super"); + super(); + trace("Child instance constructor post-super"); + } + + public override function method() { + trace("Child instance method pre-super"); + super.method(); + trace("Child instance method post-super"); + } + + public function method3() { + trace("Child instance method3 pre-super"); + super.method(); + trace("Child instance method3 post-super"); + } +} + +class Test4 extends Test3 { + { + trace("Grandchild class constructor"); + } + + function Test4() { + trace("Grandchild instance constructor pre-super"); + super(); + trace("Grandchild instance constructor post-super"); + } + + public override function method2() { + trace("Grandchild instance method2 pre-super"); + super.method2(); + trace("Grandchild instance method2 post-super"); + } + + public override function method3() { + trace("Grandchild instance method3 pre-super"); + super.method3(); + trace("Grandchild instance method3 post-super"); + } +} + +trace("Script initializer"); +var x = new Test3(); +x.method(); +x.method2(); +x.method3(); + +var y = new Test4(); +y.method(); +y.method2(); +y.method3(); \ No newline at end of file diff --git a/core/tests/swfs/avm2/es4_interfaces/output.txt b/core/tests/swfs/avm2/es4_interfaces/output.txt new file mode 100644 index 000000000000..3b93ab9bbf0d --- /dev/null +++ b/core/tests/swfs/avm2/es4_interfaces/output.txt @@ -0,0 +1,30 @@ +Class constructor +Child class constructor +Grandchild class constructor +Script initializer +Child instance constructor pre-super +Instance constructor +Child instance constructor post-super +Child instance method pre-super +Instance method +Child instance method post-super +Instance method 2 +Child instance method3 pre-super +Instance method +Child instance method3 post-super +Grandchild instance constructor pre-super +Child instance constructor pre-super +Instance constructor +Child instance constructor post-super +Grandchild instance constructor post-super +Child instance method pre-super +Instance method +Child instance method post-super +Grandchild instance method2 pre-super +Instance method 2 +Grandchild instance method2 post-super +Grandchild instance method3 pre-super +Child instance method3 pre-super +Instance method +Child instance method3 post-super +Grandchild instance method3 post-super diff --git a/core/tests/swfs/avm2/es4_interfaces/test.fla b/core/tests/swfs/avm2/es4_interfaces/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/es4_interfaces/test.fla differ diff --git a/core/tests/swfs/avm2/es4_interfaces/test.swf b/core/tests/swfs/avm2/es4_interfaces/test.swf new file mode 100644 index 000000000000..7ac3d13d97ff Binary files /dev/null and b/core/tests/swfs/avm2/es4_interfaces/test.swf differ diff --git a/core/tests/swfs/avm2/es4_method_binding/Test.as b/core/tests/swfs/avm2/es4_method_binding/Test.as new file mode 100644 index 000000000000..62a11ac0458a --- /dev/null +++ b/core/tests/swfs/avm2/es4_method_binding/Test.as @@ -0,0 +1,38 @@ +package { + public class Test {} +} + +class ES4Class extends Object { + public var value: String; + + public function ES4Class(value: String) { + this.value = value; + } + + public function test_method() { + trace(this.value); + } +} + +var x = new ES4Class("var x: ES4Class"); +var y = new ES4Class("var y: ES4Class"); + +trace("Using ES4 Class method..."); +x.test_method.call(y); +y.test_method.call(x); + +trace("Using prototype method..."); + +ES4Class.prototype.test_proto_method = function () { + trace(this.value); +} + +x.test_proto_method.call(y); +y.test_proto_method.call(x); + +(function () { + trace("Hi"); + trace(this); +}).call.call(); + +trace(Function.prototype.call.call()); \ No newline at end of file diff --git a/core/tests/swfs/avm2/es4_method_binding/output.txt b/core/tests/swfs/avm2/es4_method_binding/output.txt new file mode 100644 index 000000000000..a5463e96da97 --- /dev/null +++ b/core/tests/swfs/avm2/es4_method_binding/output.txt @@ -0,0 +1,6 @@ +Using ES4 Class method... +var x: ES4Class +var y: ES4Class +Using prototype method... +var y: ES4Class +var x: ES4Class diff --git a/core/tests/swfs/avm2/es4_method_binding/test.fla b/core/tests/swfs/avm2/es4_method_binding/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/es4_method_binding/test.fla differ diff --git a/core/tests/swfs/avm2/es4_method_binding/test.swf b/core/tests/swfs/avm2/es4_method_binding/test.swf new file mode 100644 index 000000000000..70ba78b3e767 Binary files /dev/null and b/core/tests/swfs/avm2/es4_method_binding/test.swf differ diff --git a/core/tests/swfs/avm2/es4_oop_prototypes/Test.as b/core/tests/swfs/avm2/es4_oop_prototypes/Test.as new file mode 100644 index 000000000000..c90c7052e40a --- /dev/null +++ b/core/tests/swfs/avm2/es4_oop_prototypes/Test.as @@ -0,0 +1,53 @@ +package { + public class Test {} +} + +class ES4Class extends Object { + public function ES4Class() { + trace("ES4Class constructor"); + } + + public function test_method() { + trace("ES4Class test_method"); + } + + static public function test_static() { + trace("ES4Class test_static"); + } + + public var test_var = "ES4Class test_var"; + + public const test_const = "ES4Class test_const"; + + public function get test_get() { + return "ES4Class test_get"; + } +} + +ES4Class.prototype.test_proto_var = "ES4Class test_proto_var"; +ES4Class.prototype.test_var = "TEST FAIL: Class variables override prototype variables!"; +ES4Class.prototype.test_const = "TEST FAIL: Class constants override prototype variables!"; + +ES4Class.prototype.test_proto_method = function () { + trace("ES4Class test_proto_method"); +}; + +ES4Class.prototype.test_method = function () { + trace("TEST FAIL: Class methods override prototype functions!"); +}; + +ES4Class.prototype.test_static = function () { + trace("TEST FAIL: Class static methods override prototype functions!"); +}; + +ES4Class.prototype.test_get = "TEST FAIL: Class getters override prototype properties!"; + +var x = new ES4Class(); + +trace(x.test_var); +trace(x.test_const); +trace(x.test_proto_var); +x.test_method(); +ES4Class.test_static(); +x.test_proto_method(); +trace(x.test_get); \ No newline at end of file diff --git a/core/tests/swfs/avm2/es4_oop_prototypes/output.txt b/core/tests/swfs/avm2/es4_oop_prototypes/output.txt new file mode 100644 index 000000000000..2e4c7dbd7dd5 --- /dev/null +++ b/core/tests/swfs/avm2/es4_oop_prototypes/output.txt @@ -0,0 +1,8 @@ +ES4Class constructor +ES4Class test_var +ES4Class test_const +ES4Class test_proto_var +ES4Class test_method +ES4Class test_static +ES4Class test_proto_method +ES4Class test_get diff --git a/core/tests/swfs/avm2/es4_oop_prototypes/test.fla b/core/tests/swfs/avm2/es4_oop_prototypes/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/es4_oop_prototypes/test.fla differ diff --git a/core/tests/swfs/avm2/es4_oop_prototypes/test.swf b/core/tests/swfs/avm2/es4_oop_prototypes/test.swf new file mode 100644 index 000000000000..1c4962132eeb Binary files /dev/null and b/core/tests/swfs/avm2/es4_oop_prototypes/test.swf differ diff --git a/core/tests/swfs/avm2/function_call/Test.as b/core/tests/swfs/avm2/function_call/Test.as new file mode 100644 index 000000000000..612b75f96f15 --- /dev/null +++ b/core/tests/swfs/avm2/function_call/Test.as @@ -0,0 +1,11 @@ +package { + public class Test {} +} + +function testfunc(v1, v2, v3) { + trace(v1); + trace(v2); + trace(v3); +} + +testfunc("arg1", "arg2", "arg3"); \ No newline at end of file diff --git a/core/tests/swfs/avm2/function_call/output.txt b/core/tests/swfs/avm2/function_call/output.txt new file mode 100644 index 000000000000..6a8e7e73b1ea --- /dev/null +++ b/core/tests/swfs/avm2/function_call/output.txt @@ -0,0 +1,3 @@ +arg1 +arg2 +arg3 diff --git a/core/tests/swfs/avm2/function_call/test.fla b/core/tests/swfs/avm2/function_call/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/function_call/test.fla differ diff --git a/core/tests/swfs/avm2/function_call/test.swf b/core/tests/swfs/avm2/function_call/test.swf new file mode 100644 index 000000000000..96dae9e090dc Binary files /dev/null and b/core/tests/swfs/avm2/function_call/test.swf differ diff --git a/core/tests/swfs/avm2/function_call_via_call/Test.as b/core/tests/swfs/avm2/function_call_via_call/Test.as new file mode 100644 index 000000000000..81c745852dd4 --- /dev/null +++ b/core/tests/swfs/avm2/function_call_via_call/Test.as @@ -0,0 +1,11 @@ +package { + public class Test {} +} + +function testfunc(v1, v2, v3) { + trace(v1); + trace(v2); + trace(v3); +} + +testfunc.call(null, "arg1", "arg2", "arg3"); \ No newline at end of file diff --git a/core/tests/swfs/avm2/function_call_via_call/output.txt b/core/tests/swfs/avm2/function_call_via_call/output.txt new file mode 100644 index 000000000000..6a8e7e73b1ea --- /dev/null +++ b/core/tests/swfs/avm2/function_call_via_call/output.txt @@ -0,0 +1,3 @@ +arg1 +arg2 +arg3 diff --git a/core/tests/swfs/avm2/function_call_via_call/test.fla b/core/tests/swfs/avm2/function_call_via_call/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/function_call_via_call/test.fla differ diff --git a/core/tests/swfs/avm2/function_call_via_call/test.swf b/core/tests/swfs/avm2/function_call_via_call/test.swf new file mode 100644 index 000000000000..2878e446fb16 Binary files /dev/null and b/core/tests/swfs/avm2/function_call_via_call/test.swf differ diff --git a/core/tests/swfs/avm2/function_to_locale_string/Test.as b/core/tests/swfs/avm2/function_to_locale_string/Test.as new file mode 100644 index 000000000000..5aff81c3d006 --- /dev/null +++ b/core/tests/swfs/avm2/function_to_locale_string/Test.as @@ -0,0 +1,16 @@ +package { + public class Test {} +} + +var freeFunction = function() { +}; + +function namedFunction() { + +} + +trace("//freeFunction.toLocaleString()"); +trace(freeFunction.toLocaleString()); + +trace("//namedFunction.toLocaleString()"); +trace(namedFunction.toLocaleString()); \ No newline at end of file diff --git a/core/tests/swfs/avm2/function_to_locale_string/output.txt b/core/tests/swfs/avm2/function_to_locale_string/output.txt new file mode 100644 index 000000000000..bd1c22247789 --- /dev/null +++ b/core/tests/swfs/avm2/function_to_locale_string/output.txt @@ -0,0 +1,4 @@ +//freeFunction.toLocaleString() +function Function() {} +//namedFunction.toLocaleString() +function Function() {} diff --git a/core/tests/swfs/avm2/function_to_locale_string/test.fla b/core/tests/swfs/avm2/function_to_locale_string/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/function_to_locale_string/test.fla differ diff --git a/core/tests/swfs/avm2/function_to_locale_string/test.swf b/core/tests/swfs/avm2/function_to_locale_string/test.swf new file mode 100644 index 000000000000..0c448e7ffc2c Binary files /dev/null and b/core/tests/swfs/avm2/function_to_locale_string/test.swf differ diff --git a/core/tests/swfs/avm2/function_to_string/Test.as b/core/tests/swfs/avm2/function_to_string/Test.as new file mode 100644 index 000000000000..0765fb2b10c4 --- /dev/null +++ b/core/tests/swfs/avm2/function_to_string/Test.as @@ -0,0 +1,16 @@ +package { + public class Test {} +} + +var freeFunction = function() { +}; + +function namedFunction() { + +} + +trace("//freeFunction.toString()"); +trace(freeFunction.toString()); + +trace("//namedFunction.toString()"); +trace(namedFunction.toString()); \ No newline at end of file diff --git a/core/tests/swfs/avm2/function_to_string/output.txt b/core/tests/swfs/avm2/function_to_string/output.txt new file mode 100644 index 000000000000..823210c4aad6 --- /dev/null +++ b/core/tests/swfs/avm2/function_to_string/output.txt @@ -0,0 +1,4 @@ +//freeFunction.toString() +function Function() {} +//namedFunction.toString() +function Function() {} diff --git a/core/tests/swfs/avm2/function_to_string/test.fla b/core/tests/swfs/avm2/function_to_string/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/function_to_string/test.fla differ diff --git a/core/tests/swfs/avm2/function_to_string/test.swf b/core/tests/swfs/avm2/function_to_string/test.swf new file mode 100644 index 000000000000..ae4c69a7a247 Binary files /dev/null and b/core/tests/swfs/avm2/function_to_string/test.swf differ diff --git a/core/tests/swfs/avm2/function_value_of/Test.as b/core/tests/swfs/avm2/function_value_of/Test.as new file mode 100644 index 000000000000..038d75e6c378 --- /dev/null +++ b/core/tests/swfs/avm2/function_value_of/Test.as @@ -0,0 +1,16 @@ +package { + public class Test {} +} + +var freeFunction = function() { +}; + +function namedFunction() { + +} + +trace("//freeFunction.valueOf() === freeFunction"); +trace(freeFunction.valueOf() === freeFunction); + +trace("//namedFunction.valueOf() === namedFunction"); +trace(namedFunction.valueOf() === namedFunction); \ No newline at end of file diff --git a/core/tests/swfs/avm2/function_value_of/output.txt b/core/tests/swfs/avm2/function_value_of/output.txt new file mode 100644 index 000000000000..5be28b9dbc08 --- /dev/null +++ b/core/tests/swfs/avm2/function_value_of/output.txt @@ -0,0 +1,4 @@ +//freeFunction.valueOf() === freeFunction +true +//namedFunction.valueOf() === namedFunction +true diff --git a/core/tests/swfs/avm2/function_value_of/test.fla b/core/tests/swfs/avm2/function_value_of/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/function_value_of/test.fla differ diff --git a/core/tests/swfs/avm2/function_value_of/test.swf b/core/tests/swfs/avm2/function_value_of/test.swf new file mode 100644 index 000000000000..eee5e2b1de13 Binary files /dev/null and b/core/tests/swfs/avm2/function_value_of/test.swf differ diff --git a/core/tests/swfs/avm2/has_own_property/Test.as b/core/tests/swfs/avm2/has_own_property/Test.as new file mode 100644 index 000000000000..c7878fe5a415 --- /dev/null +++ b/core/tests/swfs/avm2/has_own_property/Test.as @@ -0,0 +1,177 @@ +package { + public class Test {} +} + +class ES4Class extends Object { + public var test_var = "var"; + public const test_const = "const"; + + public function test_function() { + trace("test_function"); + } + + public function get test_virt() { + return "test_virt"; + } + + public function set test_virt(val) { + trace("test_virt"); + } + + public static var test_static_var = "var"; + public static const test_static_const = "const"; + + public static function test_static_function() { + trace("test_static_function"); + } + + public static function get test_static_virt() { + return "test_static_virt"; + } + + public static function set test_static_virt(val) { + trace("test_static_virt"); + } + + private var test_private_var = "private_var"; + private const test_private_const = "private_const"; + + private function test_private_function() { + trace("test_private_function"); + } + + private function get test_private_virt() { + return "test_private_virt"; + } + + private function set test_private_virt(val) { + trace("test_private_virt"); + } +} + +function ES3Class() { + this.test_var = "var"; +} + +ES3Class.test_static_var = "var"; + +ES3Class.test_static_function = function () { + trace("test_static_function"); +} + +ES3Class.prototype.test_function = function() { + trace("test_function"); +} + +ES3Class.prototype.test_proto = "proto_var"; + +var es4inst = new ES4Class(); +var es3inst = new ES3Class(); + +trace("//es4inst.hasOwnProperty('test_var')"); +trace(es4inst.hasOwnProperty('test_var')); +trace("//es4inst.hasOwnProperty('test_const')"); +trace(es4inst.hasOwnProperty('test_const')); +trace("//es4inst.hasOwnProperty('test_function')"); +trace(es4inst.hasOwnProperty('test_function')); +trace("//es4inst.hasOwnProperty('test_virt')"); +trace(es4inst.hasOwnProperty('test_virt')); +trace("//es4inst.hasOwnProperty('test_static_var')"); +trace(es4inst.hasOwnProperty('test_static_var')); +trace("//es4inst.hasOwnProperty('test_static_const')"); +trace(es4inst.hasOwnProperty('test_static_const')); +trace("//es4inst.hasOwnProperty('test_static_function')"); +trace(es4inst.hasOwnProperty('test_static_function')); +trace("//es4inst.hasOwnProperty('test_static_virt')"); +trace(es4inst.hasOwnProperty('test_static_virt')); +trace("//es4inst.hasOwnProperty('test_private_var')"); +trace(es4inst.hasOwnProperty('test_private_var')); +trace("//es4inst.hasOwnProperty('test_private_const')"); +trace(es4inst.hasOwnProperty('test_private_const')); +trace("//es4inst.hasOwnProperty('test_private_function')"); +trace(es4inst.hasOwnProperty('test_private_function')); +trace("//es4inst.hasOwnProperty('test_private_virt')"); +trace(es4inst.hasOwnProperty('test_private_virt')); + +trace("//ES4Class.hasOwnProperty('test_var')"); +trace(ES4Class.hasOwnProperty('test_var')); +trace("//ES4Class.hasOwnProperty('test_const')"); +trace(ES4Class.hasOwnProperty('test_const')); +trace("//ES4Class.hasOwnProperty('test_function')"); +trace(ES4Class.hasOwnProperty('test_function')); +trace("//ES4Class.hasOwnProperty('test_virt')"); +trace(ES4Class.hasOwnProperty('test_virt')); +trace("//ES4Class.hasOwnProperty('test_static_var')"); +trace(ES4Class.hasOwnProperty('test_static_var')); +trace("//ES4Class.hasOwnProperty('test_static_const')"); +trace(ES4Class.hasOwnProperty('test_static_const')); +trace("//ES4Class.hasOwnProperty('test_static_function')"); +trace(ES4Class.hasOwnProperty('test_static_function')); +trace("//ES4Class.hasOwnProperty('test_static_virt')"); +trace(ES4Class.hasOwnProperty('test_static_virt')); +trace("//ES4Class.hasOwnProperty('test_private_var')"); +trace(ES4Class.hasOwnProperty('test_private_var')); +trace("//ES4Class.hasOwnProperty('test_private_const')"); +trace(ES4Class.hasOwnProperty('test_private_const')); +trace("//ES4Class.hasOwnProperty('test_private_function')"); +trace(ES4Class.hasOwnProperty('test_private_function')); +trace("//ES4Class.hasOwnProperty('test_private_virt')"); +trace(ES4Class.hasOwnProperty('test_private_virt')); + +trace("//ES4Class.prototype.hasOwnProperty('test_var')"); +trace(ES4Class.prototype.hasOwnProperty('test_var')); +trace("//ES4Class.prototype.hasOwnProperty('test_const')"); +trace(ES4Class.prototype.hasOwnProperty('test_const')); +trace("//ES4Class.prototype.hasOwnProperty('test_function')"); +trace(ES4Class.prototype.hasOwnProperty('test_function')); +trace("//ES4Class.prototype.hasOwnProperty('test_virt')"); +trace(ES4Class.prototype.hasOwnProperty('test_virt')); +trace("//ES4Class.prototype.hasOwnProperty('test_static_var')"); +trace(ES4Class.prototype.hasOwnProperty('test_static_var')); +trace("//ES4Class.prototype.hasOwnProperty('test_static_const')"); +trace(ES4Class.prototype.hasOwnProperty('test_static_const')); +trace("//ES4Class.prototype.hasOwnProperty('test_static_function')"); +trace(ES4Class.prototype.hasOwnProperty('test_static_function')); +trace("//ES4Class.prototype.hasOwnProperty('test_static_virt')"); +trace(ES4Class.prototype.hasOwnProperty('test_static_virt')); +trace("//ES4Class.prototype.hasOwnProperty('test_private_var')"); +trace(ES4Class.prototype.hasOwnProperty('test_private_var')); +trace("//ES4Class.prototype.hasOwnProperty('test_private_const')"); +trace(ES4Class.prototype.hasOwnProperty('test_private_const')); +trace("//ES4Class.prototype.hasOwnProperty('test_private_function')"); +trace(ES4Class.prototype.hasOwnProperty('test_private_function')); +trace("//ES4Class.prototype.hasOwnProperty('test_private_virt')"); +trace(ES4Class.prototype.hasOwnProperty('test_private_virt')); + +trace("//es3inst.hasOwnProperty('test_var')"); +trace(es3inst.hasOwnProperty('test_var')); +trace("//es3inst.hasOwnProperty('test_function')"); +trace(es3inst.hasOwnProperty('test_function')); +trace("//es3inst.hasOwnProperty('test_proto')"); +trace(es3inst.hasOwnProperty('test_proto')); +trace("//es3inst.hasOwnProperty('test_static_var')"); +trace(es3inst.hasOwnProperty('test_static_var')); +trace("//es3inst.hasOwnProperty('test_static_function')"); +trace(es3inst.hasOwnProperty('test_static_function')); + +trace("//ES3Class.hasOwnProperty('test_var')"); +trace(ES3Class.hasOwnProperty('test_var')); +trace("//ES3Class.hasOwnProperty('test_function')"); +trace(ES3Class.hasOwnProperty('test_function')); +trace("//ES3Class.hasOwnProperty('test_proto')"); +trace(ES3Class.hasOwnProperty('test_proto')); +trace("//ES3Class.hasOwnProperty('test_static_var')"); +trace(ES3Class.hasOwnProperty('test_static_var')); +trace("//ES3Class.hasOwnProperty('test_static_function')"); +trace(ES3Class.hasOwnProperty('test_static_function')); + +trace("//ES3Class.prototype.hasOwnProperty('test_var')"); +trace(ES3Class.prototype.hasOwnProperty('test_var')); +trace("//ES3Class.prototype.hasOwnProperty('test_function')"); +trace(ES3Class.prototype.hasOwnProperty('test_function')); +trace("//ES3Class.prototype.hasOwnProperty('test_proto')"); +trace(ES3Class.prototype.hasOwnProperty('test_proto')); +trace("//ES3Class.prototype.hasOwnProperty('test_static_var')"); +trace(ES3Class.prototype.hasOwnProperty('test_static_var')); +trace("//ES3Class.prototype.hasOwnProperty('test_static_function')"); +trace(ES3Class.prototype.hasOwnProperty('test_static_function')); \ No newline at end of file diff --git a/core/tests/swfs/avm2/has_own_property/output.txt b/core/tests/swfs/avm2/has_own_property/output.txt new file mode 100644 index 000000000000..5123fe445f3f --- /dev/null +++ b/core/tests/swfs/avm2/has_own_property/output.txt @@ -0,0 +1,102 @@ +//es4inst.hasOwnProperty('test_var') +true +//es4inst.hasOwnProperty('test_const') +true +//es4inst.hasOwnProperty('test_function') +true +//es4inst.hasOwnProperty('test_virt') +true +//es4inst.hasOwnProperty('test_static_var') +false +//es4inst.hasOwnProperty('test_static_const') +false +//es4inst.hasOwnProperty('test_static_function') +false +//es4inst.hasOwnProperty('test_static_virt') +false +//es4inst.hasOwnProperty('test_private_var') +false +//es4inst.hasOwnProperty('test_private_const') +false +//es4inst.hasOwnProperty('test_private_function') +false +//es4inst.hasOwnProperty('test_private_virt') +false +//ES4Class.hasOwnProperty('test_var') +false +//ES4Class.hasOwnProperty('test_const') +false +//ES4Class.hasOwnProperty('test_function') +false +//ES4Class.hasOwnProperty('test_virt') +false +//ES4Class.hasOwnProperty('test_static_var') +true +//ES4Class.hasOwnProperty('test_static_const') +true +//ES4Class.hasOwnProperty('test_static_function') +true +//ES4Class.hasOwnProperty('test_static_virt') +true +//ES4Class.hasOwnProperty('test_private_var') +false +//ES4Class.hasOwnProperty('test_private_const') +false +//ES4Class.hasOwnProperty('test_private_function') +false +//ES4Class.hasOwnProperty('test_private_virt') +false +//ES4Class.prototype.hasOwnProperty('test_var') +false +//ES4Class.prototype.hasOwnProperty('test_const') +false +//ES4Class.prototype.hasOwnProperty('test_function') +false +//ES4Class.prototype.hasOwnProperty('test_virt') +false +//ES4Class.prototype.hasOwnProperty('test_static_var') +false +//ES4Class.prototype.hasOwnProperty('test_static_const') +false +//ES4Class.prototype.hasOwnProperty('test_static_function') +false +//ES4Class.prototype.hasOwnProperty('test_static_virt') +false +//ES4Class.prototype.hasOwnProperty('test_private_var') +false +//ES4Class.prototype.hasOwnProperty('test_private_const') +false +//ES4Class.prototype.hasOwnProperty('test_private_function') +false +//ES4Class.prototype.hasOwnProperty('test_private_virt') +false +//es3inst.hasOwnProperty('test_var') +true +//es3inst.hasOwnProperty('test_function') +false +//es3inst.hasOwnProperty('test_proto') +false +//es3inst.hasOwnProperty('test_static_var') +false +//es3inst.hasOwnProperty('test_static_function') +false +//ES3Class.hasOwnProperty('test_var') +false +//ES3Class.hasOwnProperty('test_function') +false +//ES3Class.hasOwnProperty('test_proto') +false +//ES3Class.hasOwnProperty('test_static_var') +true +//ES3Class.hasOwnProperty('test_static_function') +true +//ES3Class.prototype.hasOwnProperty('test_var') +false +//ES3Class.prototype.hasOwnProperty('test_function') +true +//ES3Class.prototype.hasOwnProperty('test_proto') +true +//ES3Class.prototype.hasOwnProperty('test_static_var') +false +//ES3Class.prototype.hasOwnProperty('test_static_function') +false diff --git a/core/tests/swfs/avm2/has_own_property/test.fla b/core/tests/swfs/avm2/has_own_property/test.fla new file mode 100644 index 000000000000..9117e9f0beb5 Binary files /dev/null and b/core/tests/swfs/avm2/has_own_property/test.fla differ diff --git a/core/tests/swfs/avm2/has_own_property/test.swf b/core/tests/swfs/avm2/has_own_property/test.swf new file mode 100644 index 000000000000..bc27d08317d0 Binary files /dev/null and b/core/tests/swfs/avm2/has_own_property/test.swf differ diff --git a/core/tests/swfs/avm2/hello_world/Test.as b/core/tests/swfs/avm2/hello_world/Test.as new file mode 100644 index 000000000000..6ee9ef5d251c --- /dev/null +++ b/core/tests/swfs/avm2/hello_world/Test.as @@ -0,0 +1,5 @@ +package { + public class Test {} +} + +trace("Hello world!"); \ No newline at end of file diff --git a/core/tests/swfs/avm2/hello_world/output.txt b/core/tests/swfs/avm2/hello_world/output.txt new file mode 100644 index 000000000000..cd0875583aab --- /dev/null +++ b/core/tests/swfs/avm2/hello_world/output.txt @@ -0,0 +1 @@ +Hello world! diff --git a/core/tests/swfs/avm2/hello_world/test.fla b/core/tests/swfs/avm2/hello_world/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/hello_world/test.fla differ diff --git a/core/tests/swfs/avm2/hello_world/test.swf b/core/tests/swfs/avm2/hello_world/test.swf new file mode 100644 index 000000000000..777bb8c2dff1 Binary files /dev/null and b/core/tests/swfs/avm2/hello_world/test.swf differ diff --git a/core/tests/swfs/avm2/if_stricteq/Test.as b/core/tests/swfs/avm2/if_stricteq/Test.as new file mode 100644 index 000000000000..fdeb8740fa28 --- /dev/null +++ b/core/tests/swfs/avm2/if_stricteq/Test.as @@ -0,0 +1,72 @@ +package { + public class Test {} +} + +if(2 === "2") +{ + trace("ERROR: 2 === \"2\""); +} +if(2 === 2) +{ + trace("2 === 2"); +} +if(2 === 5) +{ + trace("ERROR: 2 === 5"); +} +if(true === true) +{ + trace("true === true"); +} +if(false === false) +{ + trace("false === false"); +} +if(true === false) +{ + trace("ERROR: true === false"); +} +if(1 === true) +{ + trace("ERROR: 1 === true"); +} +if(0 === false) +{ + trace("ERROR: 0 === false"); +} +if("abc" === "abc") +{ + trace("\"abc\" === \"abc\""); +} +if(0 === undefined) +{ + trace("ERROR: 0 === undefined"); +} +if(undefined === undefined) +{ + trace("undefined === undefined"); +} +if(NaN === NaN) +{ + trace("NaN === NaN"); +} +if(undefined === NaN) +{ + trace("ERROR: undefined === NaN"); +} +if(0 === null) +{ + trace("ERROR: 0 === null"); +} +if(null === null) +{ + trace("null === null"); +} +if(undefined === null) +{ + trace("ERROR: undefined === null"); +} +if(NaN === null) +{ + trace("ERROR: NaN === null"); +} diff --git a/core/tests/swfs/avm2/if_stricteq/output.txt b/core/tests/swfs/avm2/if_stricteq/output.txt new file mode 100644 index 000000000000..63634f96e651 --- /dev/null +++ b/core/tests/swfs/avm2/if_stricteq/output.txt @@ -0,0 +1,6 @@ +2 === 2 +true === true +false === false +"abc" === "abc" +undefined === undefined +null === null diff --git a/core/tests/swfs/avm2/if_stricteq/test.fla b/core/tests/swfs/avm2/if_stricteq/test.fla new file mode 100644 index 000000000000..bbbf05379d9a Binary files /dev/null and b/core/tests/swfs/avm2/if_stricteq/test.fla differ diff --git a/core/tests/swfs/avm2/if_stricteq/test.swf b/core/tests/swfs/avm2/if_stricteq/test.swf new file mode 100644 index 000000000000..c1b15ed67e85 Binary files /dev/null and b/core/tests/swfs/avm2/if_stricteq/test.swf differ diff --git a/core/tests/swfs/avm2/if_strictne/Test.as b/core/tests/swfs/avm2/if_strictne/Test.as new file mode 100644 index 000000000000..27e3f306e233 --- /dev/null +++ b/core/tests/swfs/avm2/if_strictne/Test.as @@ -0,0 +1,72 @@ +package { + public class Test {} +} + +if(2 !== "2") +{ + trace("2 !== \"2\""); +} +if(2 !== 2) +{ + trace("ERROR: 2 !== 2"); +} +if(2 !== 5) +{ + trace("2 !== 5"); +} +if(true !== true) +{ + trace("ERROR: true !== true"); +} +if(false !== false) +{ + trace("ERROR: false !== false"); +} +if(true !== false) +{ + trace("true !== false"); +} +if(1 !== true) +{ + trace("1 !== true"); +} +if(0 !== false) +{ + trace("0 !== false"); +} +if("abc" !== "abc") +{ + trace("ERROR: \"abc\" !== \"abc\""); +} +if(0 !== undefined) +{ + trace("0 !== undefined"); +} +if(undefined !== undefined) +{ + trace("ERROR: undefined !== undefined"); +} +if(NaN !== NaN) +{ + trace("NaN !== NaN"); +} +if(undefined !== NaN) +{ + trace("undefined !== NaN"); +} +if(0 !== null) +{ + trace("0 !== null"); +} +if(null !== null) +{ + trace("ERROR: null !== null"); +} +if(undefined !== null) +{ + trace("undefined !== null"); +} +if(NaN !== null) +{ + trace("NaN !== null"); +} diff --git a/core/tests/swfs/avm2/if_strictne/output.txt b/core/tests/swfs/avm2/if_strictne/output.txt new file mode 100644 index 000000000000..1cb050425656 --- /dev/null +++ b/core/tests/swfs/avm2/if_strictne/output.txt @@ -0,0 +1,11 @@ +2 !== "2" +2 !== 5 +true !== false +1 !== true +0 !== false +0 !== undefined +NaN !== NaN +undefined !== NaN +0 !== null +undefined !== null +NaN !== null diff --git a/core/tests/swfs/avm2/if_strictne/test.fla b/core/tests/swfs/avm2/if_strictne/test.fla new file mode 100644 index 000000000000..bbbf05379d9a Binary files /dev/null and b/core/tests/swfs/avm2/if_strictne/test.fla differ diff --git a/core/tests/swfs/avm2/if_strictne/test.swf b/core/tests/swfs/avm2/if_strictne/test.swf new file mode 100644 index 000000000000..69ee612d5ebe Binary files /dev/null and b/core/tests/swfs/avm2/if_strictne/test.swf differ diff --git a/core/tests/swfs/avm2/instanceof/Test.as b/core/tests/swfs/avm2/instanceof/Test.as new file mode 100644 index 000000000000..cc0881d7a7f0 --- /dev/null +++ b/core/tests/swfs/avm2/instanceof/Test.as @@ -0,0 +1,100 @@ +package { + public class Test { + } +} + +interface ITest2 { + function method(); + function method2(); +} + +class Test2 implements ITest2 { + function Test2() { + } + + public function method() { + trace("Instance method"); + } + + public function method2() { + trace("Instance method 2"); + } +} + +interface ITest3 extends ITest2 { + function method3() +} + +class Test3 extends Test2 implements ITest3 { + function Test3() { + } + + public override function method() { + trace("Child instance method pre-super"); + super.method(); + trace("Child instance method post-super"); + } + + public function method3() { + trace("Child instance method3 pre-super"); + super.method(); + trace("Child instance method3 post-super"); + } +} + +class Test4 extends Test3 { + function Test4() { + } + + public override function method2() { + trace("Grandchild instance method2 pre-super"); + super.method2(); + trace("Grandchild instance method2 post-super"); + } + + public override function method3() { + trace("Grandchild instance method3 pre-super"); + super.method3(); + trace("Grandchild instance method3 post-super"); + } +} + +var x = new Test3(); + +trace("//x instanceof Object"); +trace(x instanceof Object); + +trace("//x instanceof Test2"); +trace(x instanceof Test2); + +trace("//x instanceof ITest2"); +trace(x instanceof ITest2); + +trace("//x instanceof Test3"); +trace(x instanceof Test3); + +trace("//x instanceof ITest3"); +trace(x instanceof ITest3); + +trace("//x instanceof Test4"); +trace(x instanceof Test4); + +var y = new Test4(); + +trace("//y instanceof Object"); +trace(y instanceof Object); + +trace("//y instanceof Test2"); +trace(y instanceof Test2); + +trace("//y instanceof ITest2"); +trace(y instanceof ITest2); + +trace("//y instanceof Test3"); +trace(y instanceof Test3); + +trace("//y instanceof ITest3"); +trace(y instanceof ITest3); + +trace("//y instanceof Test4"); +trace(y instanceof Test4); \ No newline at end of file diff --git a/core/tests/swfs/avm2/instanceof/output.txt b/core/tests/swfs/avm2/instanceof/output.txt new file mode 100644 index 000000000000..ce3c28823a2a --- /dev/null +++ b/core/tests/swfs/avm2/instanceof/output.txt @@ -0,0 +1,24 @@ +//x instanceof Object +true +//x instanceof Test2 +true +//x instanceof ITest2 +false +//x instanceof Test3 +true +//x instanceof ITest3 +false +//x instanceof Test4 +false +//y instanceof Object +true +//y instanceof Test2 +true +//y instanceof ITest2 +false +//y instanceof Test3 +true +//y instanceof ITest3 +false +//y instanceof Test4 +true diff --git a/core/tests/swfs/avm2/instanceof/test.fla b/core/tests/swfs/avm2/instanceof/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/instanceof/test.fla differ diff --git a/core/tests/swfs/avm2/instanceof/test.swf b/core/tests/swfs/avm2/instanceof/test.swf new file mode 100644 index 000000000000..c3074ac4ced9 Binary files /dev/null and b/core/tests/swfs/avm2/instanceof/test.swf differ diff --git a/core/tests/swfs/avm2/is_prototype_of/Test.as b/core/tests/swfs/avm2/is_prototype_of/Test.as new file mode 100644 index 000000000000..c23680bb7ce9 --- /dev/null +++ b/core/tests/swfs/avm2/is_prototype_of/Test.as @@ -0,0 +1,28 @@ +package { + public class Test {} +} + +class ES4Class extends Object { + +} + +function ES3Class() { + +} + +var es4inst = new ES4Class(); +var es3inst = new ES3Class(); + +trace("//ES4Class.prototype.isPrototypeOf(es4inst);"); +trace(ES4Class.prototype.isPrototypeOf(es4inst)); +trace("//Object.prototype.isPrototypeOf(es4inst);"); +trace(Object.prototype.isPrototypeOf(es4inst)); +trace("//ES3Class.prototype.isPrototypeOf(es4inst);"); +trace(ES3Class.prototype.isPrototypeOf(es4inst)); + +trace("//ES4Class.prototype.isPrototypeOf(es3inst);"); +trace(ES4Class.prototype.isPrototypeOf(es3inst)); +trace("//Object.prototype.isPrototypeOf(es3inst);"); +trace(Object.prototype.isPrototypeOf(es3inst)); +trace("//ES3Class.prototype.isPrototypeOf(es3inst);"); +trace(ES3Class.prototype.isPrototypeOf(es3inst)); \ No newline at end of file diff --git a/core/tests/swfs/avm2/is_prototype_of/output.txt b/core/tests/swfs/avm2/is_prototype_of/output.txt new file mode 100644 index 000000000000..6c82d1994b56 --- /dev/null +++ b/core/tests/swfs/avm2/is_prototype_of/output.txt @@ -0,0 +1,12 @@ +//ES4Class.prototype.isPrototypeOf(es4inst); +true +//Object.prototype.isPrototypeOf(es4inst); +true +//ES3Class.prototype.isPrototypeOf(es4inst); +false +//ES4Class.prototype.isPrototypeOf(es3inst); +false +//Object.prototype.isPrototypeOf(es3inst); +true +//ES3Class.prototype.isPrototypeOf(es3inst); +true diff --git a/core/tests/swfs/avm2/is_prototype_of/test.fla b/core/tests/swfs/avm2/is_prototype_of/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/is_prototype_of/test.fla differ diff --git a/core/tests/swfs/avm2/is_prototype_of/test.swf b/core/tests/swfs/avm2/is_prototype_of/test.swf new file mode 100644 index 000000000000..ddbacb6f4813 Binary files /dev/null and b/core/tests/swfs/avm2/is_prototype_of/test.swf differ diff --git a/core/tests/swfs/avm2/istype/Test.as b/core/tests/swfs/avm2/istype/Test.as new file mode 100644 index 000000000000..57c8a5f45d7a --- /dev/null +++ b/core/tests/swfs/avm2/istype/Test.as @@ -0,0 +1,100 @@ +package { + public class Test { + } +} + +interface ITest2 { + function method(); + function method2(); +} + +class Test2 implements ITest2 { + function Test2() { + } + + public function method() { + trace("Instance method"); + } + + public function method2() { + trace("Instance method 2"); + } +} + +interface ITest3 extends ITest2 { + function method3() +} + +class Test3 extends Test2 implements ITest3 { + function Test3() { + } + + public override function method() { + trace("Child instance method pre-super"); + super.method(); + trace("Child instance method post-super"); + } + + public function method3() { + trace("Child instance method3 pre-super"); + super.method(); + trace("Child instance method3 post-super"); + } +} + +class Test4 extends Test3 { + function Test4() { + } + + public override function method2() { + trace("Grandchild instance method2 pre-super"); + super.method2(); + trace("Grandchild instance method2 post-super"); + } + + public override function method3() { + trace("Grandchild instance method3 pre-super"); + super.method3(); + trace("Grandchild instance method3 post-super"); + } +} + +var x = new Test3(); + +trace("//x is Object"); +trace(x is Object); + +trace("//x is Test2"); +trace(x is Test2); + +trace("//x is ITest2"); +trace(x is ITest2); + +trace("//x is Test3"); +trace(x is Test3); + +trace("//x is ITest3"); +trace(x is ITest3); + +trace("//x is Test4"); +trace(x is Test4); + +var y = new Test4(); + +trace("//y is Object"); +trace(y is Object); + +trace("//y is Test2"); +trace(y is Test2); + +trace("//y is ITest2"); +trace(y is ITest2); + +trace("//y is Test3"); +trace(y is Test3); + +trace("//y is ITest3"); +trace(y is ITest3); + +trace("//y is Test4"); +trace(y is Test4); \ No newline at end of file diff --git a/core/tests/swfs/avm2/istype/output.txt b/core/tests/swfs/avm2/istype/output.txt new file mode 100644 index 000000000000..dc5e09a53397 --- /dev/null +++ b/core/tests/swfs/avm2/istype/output.txt @@ -0,0 +1,24 @@ +//x is Object +true +//x is Test2 +true +//x is ITest2 +true +//x is Test3 +true +//x is ITest3 +true +//x is Test4 +false +//y is Object +true +//y is Test2 +true +//y is ITest2 +true +//y is Test3 +true +//y is ITest3 +true +//y is Test4 +true diff --git a/core/tests/swfs/avm2/istype/test.fla b/core/tests/swfs/avm2/istype/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/istype/test.fla differ diff --git a/core/tests/swfs/avm2/istype/test.swf b/core/tests/swfs/avm2/istype/test.swf new file mode 100644 index 000000000000..2e62a627a2c9 Binary files /dev/null and b/core/tests/swfs/avm2/istype/test.swf differ diff --git a/core/tests/swfs/avm2/object_enumeration/Test.as b/core/tests/swfs/avm2/object_enumeration/Test.as new file mode 100644 index 000000000000..a2625b14bbc6 --- /dev/null +++ b/core/tests/swfs/avm2/object_enumeration/Test.as @@ -0,0 +1,10 @@ +package { + public class Test {} +} + +var x = {"key": "value"}; + +for (var name in x) { + trace(name); + trace(x[name]); +} \ No newline at end of file diff --git a/core/tests/swfs/avm2/object_enumeration/output.txt b/core/tests/swfs/avm2/object_enumeration/output.txt new file mode 100644 index 000000000000..a4d7a7fc708f --- /dev/null +++ b/core/tests/swfs/avm2/object_enumeration/output.txt @@ -0,0 +1,2 @@ +key +value diff --git a/core/tests/swfs/avm2/object_enumeration/test.fla b/core/tests/swfs/avm2/object_enumeration/test.fla new file mode 100644 index 000000000000..e84226824997 Binary files /dev/null and b/core/tests/swfs/avm2/object_enumeration/test.fla differ diff --git a/core/tests/swfs/avm2/object_enumeration/test.swf b/core/tests/swfs/avm2/object_enumeration/test.swf new file mode 100644 index 000000000000..76e4f7a31dbe Binary files /dev/null and b/core/tests/swfs/avm2/object_enumeration/test.swf differ diff --git a/core/tests/swfs/avm2/object_to_locale_string/Test.as b/core/tests/swfs/avm2/object_to_locale_string/Test.as new file mode 100644 index 000000000000..c81e08ae3041 --- /dev/null +++ b/core/tests/swfs/avm2/object_to_locale_string/Test.as @@ -0,0 +1,6 @@ +package { + public class Test {} +} + +trace("//(new Object()).toLocaleString()"); +trace((new Object()).toLocaleString()); \ No newline at end of file diff --git a/core/tests/swfs/avm2/object_to_locale_string/output.txt b/core/tests/swfs/avm2/object_to_locale_string/output.txt new file mode 100644 index 000000000000..79bbdc68a771 --- /dev/null +++ b/core/tests/swfs/avm2/object_to_locale_string/output.txt @@ -0,0 +1,2 @@ +//(new Object()).toLocaleString() +[object Object] diff --git a/core/tests/swfs/avm2/object_to_locale_string/test.fla b/core/tests/swfs/avm2/object_to_locale_string/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/object_to_locale_string/test.fla differ diff --git a/core/tests/swfs/avm2/object_to_locale_string/test.swf b/core/tests/swfs/avm2/object_to_locale_string/test.swf new file mode 100644 index 000000000000..dd325b6107cd Binary files /dev/null and b/core/tests/swfs/avm2/object_to_locale_string/test.swf differ diff --git a/core/tests/swfs/avm2/object_to_string/Test.as b/core/tests/swfs/avm2/object_to_string/Test.as new file mode 100644 index 000000000000..2a3e0b02fe76 --- /dev/null +++ b/core/tests/swfs/avm2/object_to_string/Test.as @@ -0,0 +1,6 @@ +package { + public class Test {} +} + +trace("//(new Object()).toString()"); +trace((new Object()).toString()); \ No newline at end of file diff --git a/core/tests/swfs/avm2/object_to_string/output.txt b/core/tests/swfs/avm2/object_to_string/output.txt new file mode 100644 index 000000000000..df1f145f12f5 --- /dev/null +++ b/core/tests/swfs/avm2/object_to_string/output.txt @@ -0,0 +1,2 @@ +//(new Object()).toString() +[object Object] diff --git a/core/tests/swfs/avm2/object_to_string/test.fla b/core/tests/swfs/avm2/object_to_string/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/object_to_string/test.fla differ diff --git a/core/tests/swfs/avm2/object_to_string/test.swf b/core/tests/swfs/avm2/object_to_string/test.swf new file mode 100644 index 000000000000..8832a9e12324 Binary files /dev/null and b/core/tests/swfs/avm2/object_to_string/test.swf differ diff --git a/core/tests/swfs/avm2/object_value_of/Test.as b/core/tests/swfs/avm2/object_value_of/Test.as new file mode 100644 index 000000000000..38c66a8af8c7 --- /dev/null +++ b/core/tests/swfs/avm2/object_value_of/Test.as @@ -0,0 +1,8 @@ +package { + public class Test {} +} + +var obj = new Object(); + +trace("//obj.valueOf() === obj"); +trace(obj.valueOf() === obj); \ No newline at end of file diff --git a/core/tests/swfs/avm2/object_value_of/output.txt b/core/tests/swfs/avm2/object_value_of/output.txt new file mode 100644 index 000000000000..03efbeb6ffea --- /dev/null +++ b/core/tests/swfs/avm2/object_value_of/output.txt @@ -0,0 +1,2 @@ +//obj.valueOf() === obj +true diff --git a/core/tests/swfs/avm2/object_value_of/test.fla b/core/tests/swfs/avm2/object_value_of/test.fla new file mode 100644 index 000000000000..d382fbe3e8ab Binary files /dev/null and b/core/tests/swfs/avm2/object_value_of/test.fla differ diff --git a/core/tests/swfs/avm2/object_value_of/test.swf b/core/tests/swfs/avm2/object_value_of/test.swf new file mode 100644 index 000000000000..49c490e50a38 Binary files /dev/null and b/core/tests/swfs/avm2/object_value_of/test.swf differ diff --git a/core/tests/swfs/avm2/property_is_enumerable/Test.as b/core/tests/swfs/avm2/property_is_enumerable/Test.as new file mode 100644 index 000000000000..505dc6be8709 --- /dev/null +++ b/core/tests/swfs/avm2/property_is_enumerable/Test.as @@ -0,0 +1,177 @@ +package { + public class Test {} +} + +class ES4Class extends Object { + public var test_var = "var"; + public const test_const = "const"; + + public function test_function() { + trace("test_function"); + } + + public function get test_virt() { + return "test_virt"; + } + + public function set test_virt(val) { + trace("test_virt"); + } + + public static var test_static_var = "var"; + public static const test_static_const = "const"; + + public static function test_static_function() { + trace("test_static_function"); + } + + public static function get test_static_virt() { + return "test_static_virt"; + } + + public static function set test_static_virt(val) { + trace("test_static_virt"); + } + + private var test_private_var = "private_var"; + private const test_private_const = "private_const"; + + private function test_private_function() { + trace("test_private_function"); + } + + private function get test_private_virt() { + return "test_private_virt"; + } + + private function set test_private_virt(val) { + trace("test_private_virt"); + } +} + +function ES3Class() { + this.test_var = "var"; +} + +ES3Class.test_static_var = "var"; + +ES3Class.test_static_function = function () { + trace("test_static_function"); +} + +ES3Class.prototype.test_function = function() { + trace("test_function"); +} + +ES3Class.prototype.test_proto = "proto_var"; + +var es4inst = new ES4Class(); +var es3inst = new ES3Class(); + +trace("//es4inst.propertyIsEnumerable('test_var')"); +trace(es4inst.propertyIsEnumerable('test_var')); +trace("//es4inst.propertyIsEnumerable('test_const')"); +trace(es4inst.propertyIsEnumerable('test_const')); +trace("//es4inst.propertyIsEnumerable('test_function')"); +trace(es4inst.propertyIsEnumerable('test_function')); +trace("//es4inst.propertyIsEnumerable('test_virt')"); +trace(es4inst.propertyIsEnumerable('test_virt')); +trace("//es4inst.propertyIsEnumerable('test_static_var')"); +trace(es4inst.propertyIsEnumerable('test_static_var')); +trace("//es4inst.propertyIsEnumerable('test_static_const')"); +trace(es4inst.propertyIsEnumerable('test_static_const')); +trace("//es4inst.propertyIsEnumerable('test_static_function')"); +trace(es4inst.propertyIsEnumerable('test_static_function')); +trace("//es4inst.propertyIsEnumerable('test_static_virt')"); +trace(es4inst.propertyIsEnumerable('test_static_virt')); +trace("//es4inst.propertyIsEnumerable('test_private_var')"); +trace(es4inst.propertyIsEnumerable('test_private_var')); +trace("//es4inst.propertyIsEnumerable('test_private_const')"); +trace(es4inst.propertyIsEnumerable('test_private_const')); +trace("//es4inst.propertyIsEnumerable('test_private_function')"); +trace(es4inst.propertyIsEnumerable('test_private_function')); +trace("//es4inst.propertyIsEnumerable('test_private_virt')"); +trace(es4inst.propertyIsEnumerable('test_private_virt')); + +trace("//ES4Class.propertyIsEnumerable('test_var')"); +trace(ES4Class.propertyIsEnumerable('test_var')); +trace("//ES4Class.propertyIsEnumerable('test_const')"); +trace(ES4Class.propertyIsEnumerable('test_const')); +trace("//ES4Class.propertyIsEnumerable('test_function')"); +trace(ES4Class.propertyIsEnumerable('test_function')); +trace("//ES4Class.propertyIsEnumerable('test_virt')"); +trace(ES4Class.propertyIsEnumerable('test_virt')); +trace("//ES4Class.propertyIsEnumerable('test_static_var')"); +trace(ES4Class.propertyIsEnumerable('test_static_var')); +trace("//ES4Class.propertyIsEnumerable('test_static_const')"); +trace(ES4Class.propertyIsEnumerable('test_static_const')); +trace("//ES4Class.propertyIsEnumerable('test_static_function')"); +trace(ES4Class.propertyIsEnumerable('test_static_function')); +trace("//ES4Class.propertyIsEnumerable('test_static_virt')"); +trace(ES4Class.propertyIsEnumerable('test_static_virt')); +trace("//ES4Class.propertyIsEnumerable('test_private_var')"); +trace(ES4Class.propertyIsEnumerable('test_private_var')); +trace("//ES4Class.propertyIsEnumerable('test_private_const')"); +trace(ES4Class.propertyIsEnumerable('test_private_const')); +trace("//ES4Class.propertyIsEnumerable('test_private_function')"); +trace(ES4Class.propertyIsEnumerable('test_private_function')); +trace("//ES4Class.propertyIsEnumerable('test_private_virt')"); +trace(ES4Class.propertyIsEnumerable('test_private_virt')); + +trace("//ES4Class.prototype.propertyIsEnumerable('test_var')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_var')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_const')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_const')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_function')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_function')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_virt')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_virt')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_static_var')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_static_var')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_static_const')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_static_const')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_static_function')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_static_function')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_static_virt')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_static_virt')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_private_var')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_private_var')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_private_const')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_private_const')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_private_function')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_private_function')); +trace("//ES4Class.prototype.propertyIsEnumerable('test_private_virt')"); +trace(ES4Class.prototype.propertyIsEnumerable('test_private_virt')); + +trace("//es3inst.propertyIsEnumerable('test_var')"); +trace(es3inst.propertyIsEnumerable('test_var')); +trace("//es3inst.propertyIsEnumerable('test_function')"); +trace(es3inst.propertyIsEnumerable('test_function')); +trace("//es3inst.propertyIsEnumerable('test_proto')"); +trace(es3inst.propertyIsEnumerable('test_proto')); +trace("//es3inst.propertyIsEnumerable('test_static_var')"); +trace(es3inst.propertyIsEnumerable('test_static_var')); +trace("//es3inst.propertyIsEnumerable('test_static_function')"); +trace(es3inst.propertyIsEnumerable('test_static_function')); + +trace("//ES3Class.propertyIsEnumerable('test_var')"); +trace(ES3Class.propertyIsEnumerable('test_var')); +trace("//ES3Class.propertyIsEnumerable('test_function')"); +trace(ES3Class.propertyIsEnumerable('test_function')); +trace("//ES3Class.propertyIsEnumerable('test_proto')"); +trace(ES3Class.propertyIsEnumerable('test_proto')); +trace("//ES3Class.propertyIsEnumerable('test_static_var')"); +trace(ES3Class.propertyIsEnumerable('test_static_var')); +trace("//ES3Class.propertyIsEnumerable('test_static_function')"); +trace(ES3Class.propertyIsEnumerable('test_static_function')); + +trace("//ES3Class.prototype.propertyIsEnumerable('test_var')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_var')); +trace("//ES3Class.prototype.propertyIsEnumerable('test_function')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_function')); +trace("//ES3Class.prototype.propertyIsEnumerable('test_proto')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_proto')); +trace("//ES3Class.prototype.propertyIsEnumerable('test_static_var')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_static_var')); +trace("//ES3Class.prototype.propertyIsEnumerable('test_static_function')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_static_function')); \ No newline at end of file diff --git a/core/tests/swfs/avm2/property_is_enumerable/output.txt b/core/tests/swfs/avm2/property_is_enumerable/output.txt new file mode 100644 index 000000000000..67ee3c59684d --- /dev/null +++ b/core/tests/swfs/avm2/property_is_enumerable/output.txt @@ -0,0 +1,102 @@ +//es4inst.propertyIsEnumerable('test_var') +false +//es4inst.propertyIsEnumerable('test_const') +false +//es4inst.propertyIsEnumerable('test_function') +false +//es4inst.propertyIsEnumerable('test_virt') +false +//es4inst.propertyIsEnumerable('test_static_var') +false +//es4inst.propertyIsEnumerable('test_static_const') +false +//es4inst.propertyIsEnumerable('test_static_function') +false +//es4inst.propertyIsEnumerable('test_static_virt') +false +//es4inst.propertyIsEnumerable('test_private_var') +false +//es4inst.propertyIsEnumerable('test_private_const') +false +//es4inst.propertyIsEnumerable('test_private_function') +false +//es4inst.propertyIsEnumerable('test_private_virt') +false +//ES4Class.propertyIsEnumerable('test_var') +false +//ES4Class.propertyIsEnumerable('test_const') +false +//ES4Class.propertyIsEnumerable('test_function') +false +//ES4Class.propertyIsEnumerable('test_virt') +false +//ES4Class.propertyIsEnumerable('test_static_var') +false +//ES4Class.propertyIsEnumerable('test_static_const') +false +//ES4Class.propertyIsEnumerable('test_static_function') +false +//ES4Class.propertyIsEnumerable('test_static_virt') +false +//ES4Class.propertyIsEnumerable('test_private_var') +false +//ES4Class.propertyIsEnumerable('test_private_const') +false +//ES4Class.propertyIsEnumerable('test_private_function') +false +//ES4Class.propertyIsEnumerable('test_private_virt') +false +//ES4Class.prototype.propertyIsEnumerable('test_var') +false +//ES4Class.prototype.propertyIsEnumerable('test_const') +false +//ES4Class.prototype.propertyIsEnumerable('test_function') +false +//ES4Class.prototype.propertyIsEnumerable('test_virt') +false +//ES4Class.prototype.propertyIsEnumerable('test_static_var') +false +//ES4Class.prototype.propertyIsEnumerable('test_static_const') +false +//ES4Class.prototype.propertyIsEnumerable('test_static_function') +false +//ES4Class.prototype.propertyIsEnumerable('test_static_virt') +false +//ES4Class.prototype.propertyIsEnumerable('test_private_var') +false +//ES4Class.prototype.propertyIsEnumerable('test_private_const') +false +//ES4Class.prototype.propertyIsEnumerable('test_private_function') +false +//ES4Class.prototype.propertyIsEnumerable('test_private_virt') +false +//es3inst.propertyIsEnumerable('test_var') +true +//es3inst.propertyIsEnumerable('test_function') +false +//es3inst.propertyIsEnumerable('test_proto') +false +//es3inst.propertyIsEnumerable('test_static_var') +false +//es3inst.propertyIsEnumerable('test_static_function') +false +//ES3Class.propertyIsEnumerable('test_var') +false +//ES3Class.propertyIsEnumerable('test_function') +false +//ES3Class.propertyIsEnumerable('test_proto') +false +//ES3Class.propertyIsEnumerable('test_static_var') +true +//ES3Class.propertyIsEnumerable('test_static_function') +true +//ES3Class.prototype.propertyIsEnumerable('test_var') +false +//ES3Class.prototype.propertyIsEnumerable('test_function') +true +//ES3Class.prototype.propertyIsEnumerable('test_proto') +true +//ES3Class.prototype.propertyIsEnumerable('test_static_var') +false +//ES3Class.prototype.propertyIsEnumerable('test_static_function') +false diff --git a/core/tests/swfs/avm2/property_is_enumerable/test.fla b/core/tests/swfs/avm2/property_is_enumerable/test.fla new file mode 100644 index 000000000000..9117e9f0beb5 Binary files /dev/null and b/core/tests/swfs/avm2/property_is_enumerable/test.fla differ diff --git a/core/tests/swfs/avm2/property_is_enumerable/test.swf b/core/tests/swfs/avm2/property_is_enumerable/test.swf new file mode 100644 index 000000000000..60a6b4460e6d Binary files /dev/null and b/core/tests/swfs/avm2/property_is_enumerable/test.swf differ diff --git a/core/tests/swfs/avm2/set_property_is_enumerable/Test.as b/core/tests/swfs/avm2/set_property_is_enumerable/Test.as new file mode 100644 index 000000000000..2e6c31cfbe4f --- /dev/null +++ b/core/tests/swfs/avm2/set_property_is_enumerable/Test.as @@ -0,0 +1,188 @@ +package { + public class Test {} +} + +dynamic class ES4Class extends Object { + public var test_var = "var"; + public const test_const = "const"; + + public function test_function() { + trace("test_function"); + } + + public function get test_virt() { + return "test_virt"; + } + + public function set test_virt(val) { + trace("test_virt"); + } + + public static var test_static_var = "var"; + public static const test_static_const = "const"; + + public static function test_static_function() { + trace("test_static_function"); + } + + public static function get test_static_virt() { + return "test_static_virt"; + } + + public static function set test_static_virt(val) { + trace("test_static_virt"); + } + + private var test_private_var = "private_var"; + private const test_private_const = "private_const"; + + private function test_private_function() { + trace("test_private_function"); + } + + private function get test_private_virt() { + return "test_private_virt"; + } + + private function set test_private_virt(val) { + trace("test_private_virt"); + } +} + +function ES3Class() { + this.test_var = "var"; +} + +ES3Class.test_static_var = "var"; + +ES3Class.test_static_function = function () { + trace("test_static_function"); +} + +ES3Class.prototype.test_function = function() { + trace("test_function"); +} + +ES3Class.prototype.test_proto = "proto_var"; + +var es4inst = new ES4Class(); +var es3inst = new ES3Class(); + +trace("//es4inst.propertyIsEnumerable('test_var')"); +trace(es4inst.propertyIsEnumerable('test_var')); +trace("//es4inst.setPropertyIsEnumerable('test_var', true)"); +es4inst.setPropertyIsEnumerable('test_var', true); +trace("//es4inst.propertyIsEnumerable('test_var')"); +trace(es4inst.propertyIsEnumerable('test_var')); + +trace("//es4inst.propertyIsEnumerable('test_const')"); +trace(es4inst.propertyIsEnumerable('test_const')); +trace("//es4inst.setPropertyIsEnumerable('test_const', true)"); +es4inst.setPropertyIsEnumerable('test_const', true); +trace("//es4inst.propertyIsEnumerable('test_const')"); +trace(es4inst.propertyIsEnumerable('test_const')); + +trace("//es4inst.propertyIsEnumerable('test_function')"); +trace(es4inst.propertyIsEnumerable('test_function')); +trace("//es4inst.setPropertyIsEnumerable('test_function', true)"); +es4inst.setPropertyIsEnumerable('test_function', true); +trace("//es4inst.propertyIsEnumerable('test_function')"); +trace(es4inst.propertyIsEnumerable('test_function')); + +trace("//es4inst.propertyIsEnumerable('test_virt')"); +trace(es4inst.propertyIsEnumerable('test_virt')); +trace("//es4inst.setPropertyIsEnumerable('test_virt', true)"); +es4inst.setPropertyIsEnumerable('test_virt', true); +trace("//es4inst.propertyIsEnumerable('test_virt')"); +trace(es4inst.propertyIsEnumerable('test_virt')); + +trace("//es4inst.propertyIsEnumerable('test_private_var')"); +trace(es4inst.propertyIsEnumerable('test_private_var')); +trace("//es4inst.setPropertyIsEnumerable('test_private_var', true)"); +es4inst.setPropertyIsEnumerable('test_private_var', true); +trace("//es4inst.propertyIsEnumerable('test_private_var')"); +trace(es4inst.propertyIsEnumerable('test_private_var')); + +trace("//es4inst.propertyIsEnumerable('test_private_const')"); +trace(es4inst.propertyIsEnumerable('test_private_const')); +trace("//es4inst.setPropertyIsEnumerable('test_private_const', true)"); +es4inst.setPropertyIsEnumerable('test_private_const', true); +trace("//es4inst.propertyIsEnumerable('test_private_const')"); +trace(es4inst.propertyIsEnumerable('test_private_const')); + +trace("//es4inst.propertyIsEnumerable('test_private_function')"); +trace(es4inst.propertyIsEnumerable('test_private_function')); +trace("//es4inst.setPropertyIsEnumerable('test_private_function', true)"); +es4inst.setPropertyIsEnumerable('test_private_function', true); +trace("//es4inst.propertyIsEnumerable('test_private_function')"); +trace(es4inst.propertyIsEnumerable('test_private_function')); + +trace("//es4inst.propertyIsEnumerable('test_private_virt')"); +trace(es4inst.propertyIsEnumerable('test_private_virt')); +trace("//es4inst.setPropertyIsEnumerable('test_private_virt', true)"); +es4inst.setPropertyIsEnumerable('test_private_virt', true); +trace("//es4inst.propertyIsEnumerable('test_private_virt')"); +trace(es4inst.propertyIsEnumerable('test_private_virt')); + +trace("//ES4Class.propertyIsEnumerable('test_static_var')"); +trace(ES4Class.propertyIsEnumerable('test_static_var')); +trace("//ES4Class.setPropertyIsEnumerable('test_static_var', true)"); +ES4Class.setPropertyIsEnumerable('test_static_var', true); +trace("//ES4Class.propertyIsEnumerable('test_static_var')"); +trace(ES4Class.propertyIsEnumerable('test_static_var')); + +trace("//ES4Class.propertyIsEnumerable('test_static_const')"); +trace(ES4Class.propertyIsEnumerable('test_static_const')); +trace("//ES4Class.setPropertyIsEnumerable('test_static_const', true)"); +ES4Class.setPropertyIsEnumerable('test_static_const', true); +trace("//ES4Class.propertyIsEnumerable('test_static_const')"); +trace(ES4Class.propertyIsEnumerable('test_static_const')); + +trace("//ES4Class.propertyIsEnumerable('test_static_function')"); +trace(ES4Class.propertyIsEnumerable('test_static_function')); +trace("//ES4Class.setPropertyIsEnumerable('test_static_function', true)"); +ES4Class.setPropertyIsEnumerable('test_static_function', true); +trace("//ES4Class.propertyIsEnumerable('test_static_function')"); +trace(ES4Class.propertyIsEnumerable('test_static_function')); + +trace("//ES4Class.propertyIsEnumerable('test_static_virt')"); +trace(ES4Class.propertyIsEnumerable('test_static_virt')); +trace("//ES4Class.setPropertyIsEnumerable('test_static_virt', true)"); +ES4Class.setPropertyIsEnumerable('test_static_virt', true); +trace("//ES4Class.propertyIsEnumerable('test_static_virt')"); +trace(ES4Class.propertyIsEnumerable('test_static_virt')); + +trace("//es3inst.propertyIsEnumerable('test_var')"); +trace(es3inst.propertyIsEnumerable('test_var')); +trace("//es3inst.setPropertyIsEnumerable('test_var', false)"); +es3inst.setPropertyIsEnumerable('test_var', false); +trace("//es3inst.propertyIsEnumerable('test_var')"); +trace(es3inst.propertyIsEnumerable('test_var')); + +trace("//ES3Class.propertyIsEnumerable('test_static_var')"); +trace(ES3Class.propertyIsEnumerable('test_static_var')); +trace("//ES3Class.setPropertyIsEnumerable('test_static_var', false)"); +ES3Class.setPropertyIsEnumerable('test_static_var', false); +trace("//ES3Class.propertyIsEnumerable('test_static_var')"); +trace(ES3Class.propertyIsEnumerable('test_static_var')); + +trace("//ES3Class.propertyIsEnumerable('test_static_function')"); +trace(ES3Class.propertyIsEnumerable('test_static_function')); +trace("//ES3Class.setPropertyIsEnumerable('test_static_function', false)"); +ES3Class.setPropertyIsEnumerable('test_static_function', false); +trace("//ES3Class.propertyIsEnumerable('test_static_function')"); +trace(ES3Class.propertyIsEnumerable('test_static_function')); + +trace("//ES3Class.prototype.propertyIsEnumerable('test_function')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_function')); +trace("//ES3Class.prototype.setPropertyIsEnumerable('test_function', false)"); +ES3Class.prototype.setPropertyIsEnumerable('test_function', false); +trace("//ES3Class.prototype.propertyIsEnumerable('test_function')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_function')); + +trace("//ES3Class.prototype.propertyIsEnumerable('test_proto')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_proto')); +trace("//ES3Class.prototype.setPropertyIsEnumerable('test_proto', false)"); +ES3Class.prototype.setPropertyIsEnumerable('test_proto', false); +trace("//ES3Class.prototype.propertyIsEnumerable('test_proto')"); +trace(ES3Class.prototype.propertyIsEnumerable('test_proto')); \ No newline at end of file diff --git a/core/tests/swfs/avm2/set_property_is_enumerable/output.txt b/core/tests/swfs/avm2/set_property_is_enumerable/output.txt new file mode 100644 index 000000000000..81fbf1c4f104 --- /dev/null +++ b/core/tests/swfs/avm2/set_property_is_enumerable/output.txt @@ -0,0 +1,85 @@ +//es4inst.propertyIsEnumerable('test_var') +false +//es4inst.setPropertyIsEnumerable('test_var', true) +//es4inst.propertyIsEnumerable('test_var') +false +//es4inst.propertyIsEnumerable('test_const') +false +//es4inst.setPropertyIsEnumerable('test_const', true) +//es4inst.propertyIsEnumerable('test_const') +false +//es4inst.propertyIsEnumerable('test_function') +false +//es4inst.setPropertyIsEnumerable('test_function', true) +//es4inst.propertyIsEnumerable('test_function') +false +//es4inst.propertyIsEnumerable('test_virt') +false +//es4inst.setPropertyIsEnumerable('test_virt', true) +//es4inst.propertyIsEnumerable('test_virt') +false +//es4inst.propertyIsEnumerable('test_private_var') +false +//es4inst.setPropertyIsEnumerable('test_private_var', true) +//es4inst.propertyIsEnumerable('test_private_var') +false +//es4inst.propertyIsEnumerable('test_private_const') +false +//es4inst.setPropertyIsEnumerable('test_private_const', true) +//es4inst.propertyIsEnumerable('test_private_const') +false +//es4inst.propertyIsEnumerable('test_private_function') +false +//es4inst.setPropertyIsEnumerable('test_private_function', true) +//es4inst.propertyIsEnumerable('test_private_function') +false +//es4inst.propertyIsEnumerable('test_private_virt') +false +//es4inst.setPropertyIsEnumerable('test_private_virt', true) +//es4inst.propertyIsEnumerable('test_private_virt') +false +//ES4Class.propertyIsEnumerable('test_static_var') +false +//ES4Class.setPropertyIsEnumerable('test_static_var', true) +//ES4Class.propertyIsEnumerable('test_static_var') +false +//ES4Class.propertyIsEnumerable('test_static_const') +false +//ES4Class.setPropertyIsEnumerable('test_static_const', true) +//ES4Class.propertyIsEnumerable('test_static_const') +false +//ES4Class.propertyIsEnumerable('test_static_function') +false +//ES4Class.setPropertyIsEnumerable('test_static_function', true) +//ES4Class.propertyIsEnumerable('test_static_function') +false +//ES4Class.propertyIsEnumerable('test_static_virt') +false +//ES4Class.setPropertyIsEnumerable('test_static_virt', true) +//ES4Class.propertyIsEnumerable('test_static_virt') +false +//es3inst.propertyIsEnumerable('test_var') +true +//es3inst.setPropertyIsEnumerable('test_var', false) +//es3inst.propertyIsEnumerable('test_var') +false +//ES3Class.propertyIsEnumerable('test_static_var') +true +//ES3Class.setPropertyIsEnumerable('test_static_var', false) +//ES3Class.propertyIsEnumerable('test_static_var') +false +//ES3Class.propertyIsEnumerable('test_static_function') +true +//ES3Class.setPropertyIsEnumerable('test_static_function', false) +//ES3Class.propertyIsEnumerable('test_static_function') +false +//ES3Class.prototype.propertyIsEnumerable('test_function') +true +//ES3Class.prototype.setPropertyIsEnumerable('test_function', false) +//ES3Class.prototype.propertyIsEnumerable('test_function') +false +//ES3Class.prototype.propertyIsEnumerable('test_proto') +true +//ES3Class.prototype.setPropertyIsEnumerable('test_proto', false) +//ES3Class.prototype.propertyIsEnumerable('test_proto') +false diff --git a/core/tests/swfs/avm2/set_property_is_enumerable/test.fla b/core/tests/swfs/avm2/set_property_is_enumerable/test.fla new file mode 100644 index 000000000000..9117e9f0beb5 Binary files /dev/null and b/core/tests/swfs/avm2/set_property_is_enumerable/test.fla differ diff --git a/core/tests/swfs/avm2/set_property_is_enumerable/test.swf b/core/tests/swfs/avm2/set_property_is_enumerable/test.swf new file mode 100644 index 000000000000..fd2b085b743b Binary files /dev/null and b/core/tests/swfs/avm2/set_property_is_enumerable/test.swf differ diff --git a/core/tests/swfs/avm2/stored_properties/Test.as b/core/tests/swfs/avm2/stored_properties/Test.as new file mode 100644 index 000000000000..39fb61afdab3 --- /dev/null +++ b/core/tests/swfs/avm2/stored_properties/Test.as @@ -0,0 +1,47 @@ +package { + public class Test { + } +} + +class TestWithVars { + public var prop; + public var propDefault = "y.propDefault resolved!"; + public const propConst = "y.propConst resolved!"; +} + +class ExtendedTest extends TestWithVars { + public var prop2; + public var prop2Default = "z.prop2Default resolved!"; + public const prop2Const = "z.prop2Const resolved!"; +} + +var x = {}; + +x.prop = "x.prop resolved!"; +trace(x.prop); + +var y = new TestWithVars(); + +y.prop = "y.prop resolved!"; +trace(y.prop); + +trace(y.propDefault); +y.propDefault = "y.propDefault overwritten!"; +trace(y.propDefault); + +trace(y.propConst); + +var z = new ExtendedTest(); + +z.prop = "z.prop resolved!"; +trace(z.prop); + +z.prop2 = "z.prop2 resolved!"; +trace(z.prop2); + +trace(z.propDefault); +trace(z.prop2Default); +z.propDefault = "TEST FAIL: Default overrides should not affect other instances!"; +trace(y.propDefault); + +trace(z.propConst); \ No newline at end of file diff --git a/core/tests/swfs/avm2/stored_properties/output.txt b/core/tests/swfs/avm2/stored_properties/output.txt new file mode 100644 index 000000000000..d37d1a0a1e72 --- /dev/null +++ b/core/tests/swfs/avm2/stored_properties/output.txt @@ -0,0 +1,11 @@ +x.prop resolved! +y.prop resolved! +y.propDefault resolved! +y.propDefault overwritten! +y.propConst resolved! +z.prop resolved! +z.prop2 resolved! +y.propDefault resolved! +z.prop2Default! +y.propDefault overwritten! +y.propConst resolved! diff --git a/core/tests/swfs/avm2/stored_properties/test.fla b/core/tests/swfs/avm2/stored_properties/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/stored_properties/test.fla differ diff --git a/core/tests/swfs/avm2/stored_properties/test.swf b/core/tests/swfs/avm2/stored_properties/test.swf new file mode 100644 index 000000000000..c745134008d2 Binary files /dev/null and b/core/tests/swfs/avm2/stored_properties/test.swf differ diff --git a/core/tests/swfs/avm2/strict_equality/Test.as b/core/tests/swfs/avm2/strict_equality/Test.as new file mode 100644 index 000000000000..1bae6a5dbaf5 --- /dev/null +++ b/core/tests/swfs/avm2/strict_equality/Test.as @@ -0,0 +1,54 @@ +package { + public class Test {} +} + +trace("//2 === \"2\""); +trace(2 === "2"); + +trace("//2 === 2"); +trace(2 === 2); + +trace("//2 === 5"); +trace(2 === 5); + +trace("//true === true"); +trace(true === true); + +trace("//false === false"); +trace(false === false); + +trace("//true === false"); +trace(true === false); + +trace("//1 === true"); +trace(1 === true); + +trace("//0 === false"); +trace(0 === false); + +trace("//\"abc\" === \"abc\""); +trace("abc" === "abc"); + +trace("//0 === undefined"); +trace(0 === undefined); + +trace("//undefined === undefined"); +trace(undefined === undefined); + +trace("//NaN === NaN"); +trace(NaN === NaN); + +trace("//undefined === NaN"); +trace(undefined === NaN); + +trace("//0 === null"); +trace(0 === null); + +trace("//null === null"); +trace(null === null); + +trace("//undefined === null"); +trace(undefined === null); + +trace("//NaN === null"); +trace(NaN === null); diff --git a/core/tests/swfs/avm2/strict_equality/output.txt b/core/tests/swfs/avm2/strict_equality/output.txt new file mode 100644 index 000000000000..8b1c6a7aa223 --- /dev/null +++ b/core/tests/swfs/avm2/strict_equality/output.txt @@ -0,0 +1,34 @@ +//2 === "2" +false +//2 === 2 +true +//2 === 5 +false +//true === true +true +//false === false +true +//true === false +false +//1 === true +false +//0 === false +false +//"abc" === "abc" +true +//0 === undefined +false +//undefined === undefined +true +//NaN === NaN +false +//undefined === NaN +false +//0 === null +false +//null === null +true +//undefined === null +false +//NaN === null +false diff --git a/core/tests/swfs/avm2/strict_equality/test.fla b/core/tests/swfs/avm2/strict_equality/test.fla new file mode 100644 index 000000000000..bbbf05379d9a Binary files /dev/null and b/core/tests/swfs/avm2/strict_equality/test.fla differ diff --git a/core/tests/swfs/avm2/strict_equality/test.swf b/core/tests/swfs/avm2/strict_equality/test.swf new file mode 100644 index 000000000000..ebb82eda0b9e Binary files /dev/null and b/core/tests/swfs/avm2/strict_equality/test.swf differ diff --git a/core/tests/swfs/avm2/virtual_properties/Test.as b/core/tests/swfs/avm2/virtual_properties/Test.as new file mode 100644 index 000000000000..c26496458ed9 --- /dev/null +++ b/core/tests/swfs/avm2/virtual_properties/Test.as @@ -0,0 +1,69 @@ +package { + public class Test { + } +} + +class Test2 { + function get prop() { + return "Test2 Prop"; + } + + function set prop(val:String) { + trace(val); + } + + function set prop2(val:String) { + trace("Test2 Set Prop2"); + } + + function get prop3() { + return "Test2 Prop3"; + } +} + +class Test3 extends Test2 { + function get prop2() { + return "Test3 Prop2"; + } + + function set prop3(val:String) { + trace(val); + } +} + +class Test4 extends Test3 { + override function get prop() { + trace("Child Prop2 getter"); + return super.prop; + } + + override function set prop(val:String) { + trace("Child Prop2 Setter"); + super.prop = val; + } +} + +var w = new Test2(); +trace(w.prop); +w.prop = "Setting Test2 Prop"; + +w.prop2 = "TEST FAIL - Test2::prop2 SETTER DOES NOT TRACE THIS VALUE"; + +trace(w.prop3); + +var x = new Test3(); +trace(x.prop); +x.prop = "Setting Test3 Prop"; + +trace(x.prop2); +x.prop3 = "Setting Test3 Prop3"; + +var y = new Test4(); +trace(y.prop); +y.prop = "Setting Test4 Prop"; + +trace(y.prop2); +y.prop2 = "TEST FAIL - Test4::prop2 SETTER DOES NOT TRACE THIS VALUE"; + +trace(y.prop3); +y.prop3 = "Setting Test4 Prop3"; \ No newline at end of file diff --git a/core/tests/swfs/avm2/virtual_properties/output.txt b/core/tests/swfs/avm2/virtual_properties/output.txt new file mode 100644 index 000000000000..5cc5898e5aec --- /dev/null +++ b/core/tests/swfs/avm2/virtual_properties/output.txt @@ -0,0 +1,16 @@ +Test2 Prop +Setting Test2 Prop +Test2 Set Prop2 +Test2 Prop3 +Test2 Prop +Setting Test3 Prop +Test3 Prop2 +Setting Test3 Prop3 +Child Prop2 getter +Test2 Prop +Child Prop2 Setter +Setting Test4 Prop +Test3 Prop2 +Test2 Set Prop2 +Test2 Prop3 +Setting Test4 Prop3 diff --git a/core/tests/swfs/avm2/virtual_properties/test.fla b/core/tests/swfs/avm2/virtual_properties/test.fla new file mode 100644 index 000000000000..84376c02ef3a Binary files /dev/null and b/core/tests/swfs/avm2/virtual_properties/test.fla differ diff --git a/core/tests/swfs/avm2/virtual_properties/test.swf b/core/tests/swfs/avm2/virtual_properties/test.swf new file mode 100644 index 000000000000..121fc7c63c9c Binary files /dev/null and b/core/tests/swfs/avm2/virtual_properties/test.swf differ diff --git a/swf/src/avm2/opcode.rs b/swf/src/avm2/opcode.rs index 77c110aaf5fd..8a255cfa3f45 100644 --- a/swf/src/avm2/opcode.rs +++ b/swf/src/avm2/opcode.rs @@ -1,6 +1,5 @@ #![allow(clippy::useless_attribute)] -#[allow(dead_code)] #[derive(Debug, PartialEq, Clone, Copy, FromPrimitive)] pub enum OpCode { Add = 0xA0, diff --git a/swf/src/avm2/read.rs b/swf/src/avm2/read.rs index b6ee9bc3f557..f38fa75a452c 100644 --- a/swf/src/avm2/read.rs +++ b/swf/src/avm2/read.rs @@ -1,7 +1,7 @@ use crate::avm2::types::*; use crate::error::{Error, Result}; use crate::read::SwfRead; -use std::io::Read; +use std::io::{Read, Seek, SeekFrom}; pub struct Reader { inner: R, @@ -13,6 +13,16 @@ impl SwfRead for Reader { } } +impl Reader +where + R: Read + Seek, +{ + #[inline] + pub fn seek(&mut self, relative_offset: i64) -> std::io::Result { + self.inner.seek(SeekFrom::Current(relative_offset as i64)) + } +} + impl Reader { pub fn new(inner: R) -> Reader { Reader { inner } @@ -90,11 +100,10 @@ impl Reader { self.read_u30() } - #[allow(dead_code)] fn read_i24(&mut self) -> Result { - Ok(i32::from(self.read_u8()?) - | (i32::from(self.read_u8()?) << 8) - | (i32::from(self.read_u8()?) << 16)) + Ok(i32::from(self.read_u8()? as i8) + | (i32::from(self.read_u8()? as i8) << 8) + | (i32::from(self.read_u8()? as i8) << 16)) } fn read_i32(&mut self) -> Result { let mut n: i32 = 0; @@ -517,8 +526,7 @@ impl Reader { }) } - #[allow(dead_code)] - fn read_op(&mut self) -> Result> { + pub fn read_op(&mut self) -> Result> { use crate::avm2::opcode::OpCode; use num_traits::FromPrimitive;