diff --git a/core/src/avm2/activation.rs b/core/src/avm2/activation.rs index 033fa7249..f3431fb72 100644 --- a/core/src/avm2/activation.rs +++ b/core/src/avm2/activation.rs @@ -22,7 +22,7 @@ use std::borrow::Cow; use std::cmp::{min, Ordering}; use swf::avm2::read::Reader; use swf::avm2::types::{ - Class as AbcClass, Index, Method as AbcMethod, MethodFlags as AbcMethodFlags, + Class as AbcClass, Exception, Index, Method as AbcMethod, MethodFlags as AbcMethodFlags, Multiname as AbcMultiname, Namespace as AbcNamespace, Op, }; @@ -861,6 +861,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Op::SetSuper { index } => self.op_set_super(method, index), Op::In => self.op_in(), Op::PushScope => self.op_push_scope(), + Op::NewCatch { index } => self.op_newcatch(method, index), Op::PushWith => self.op_push_with(), Op::PopScope => self.op_pop_scope(), Op::GetOuterScope { index } => self.op_get_outer_scope(index), @@ -999,9 +1000,27 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { _ => self.unknown_op(op), }; - if let Err(e) = result { - log::error!("AVM2 error: {}", e); - return Err(e); + if let Err(error) = result { + if let Some(body) = method.body() { + for e in body.exceptions.iter() { + if instruction_start >= e.from_offset as usize + && instruction_start < e.to_offset as usize + && e.type_name.0 == 0 + // Currently we support only typeless catch clauses + { + // Emulate pushing the exception object + let ws = WString::from_utf8_owned(error.to_string()); + let exception = AvmString::new(self.context.gc_context, ws); + self.context.avm2.push(exception); + + reader.seek_absolute(full_data, e.target_offset as usize); + return Ok(FrameControl::Continue); + } + } + } + + log::error!("AVM2 error: {}", error); + return Err(error); } result } else if let Err(e) = op { @@ -1550,6 +1569,26 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { Ok(FrameControl::Continue) } + fn op_newcatch( + &mut self, + method: Gc<'gc, BytecodeMethod<'gc>>, + index: Index, + ) -> Result, Error> { + if let Some(body) = method.body() { + let ex = &body.exceptions[index.0 as usize]; + let vname = ex.variable_name; + let qname = QName::from_abc_multiname( + method.translation_unit(), + vname, + self.context.gc_context, + )?; + let so = ScriptObject::catch_scope(self.context.gc_context, &qname); + self.context.avm2.push(so); + } + + Ok(FrameControl::Continue) + } + fn op_push_scope(&mut self) -> Result, Error> { let object = self.context.avm2.pop().coerce_to_object(self)?; self.scope_stack.push(Scope::new(object)); diff --git a/core/src/avm2/object/script_object.rs b/core/src/avm2/object/script_object.rs index 9c9ac3878..ac604e425 100644 --- a/core/src/avm2/object/script_object.rs +++ b/core/src/avm2/object/script_object.rs @@ -5,7 +5,7 @@ use crate::avm2::names::Multiname; use crate::avm2::object::{ClassObject, FunctionObject, Object, ObjectPtr, TObject}; use crate::avm2::value::Value; use crate::avm2::vtable::VTable; -use crate::avm2::Error; +use crate::avm2::{Error, QName}; use crate::string::AvmString; use fnv::FnvHashMap; use gc_arena::{Collect, GcCell, MutationContext}; @@ -102,6 +102,17 @@ impl<'gc> ScriptObject<'gc> { )) .into() } + + /// A special case for `newcatch` implementation. Basically a variable (q)name + /// which maps to slot 1. + pub fn catch_scope(mc: MutationContext<'gc, '_>, qname: &QName<'gc>) -> Object<'gc> { + let mut base = ScriptObjectData::custom_new(None, None); + let vt = VTable::newcatch(mc, &qname); + base.set_vtable(vt); + base.install_slot(); + base.install_slot(); + ScriptObject(GcCell::allocate(mc, base)).into() + } } impl<'gc> ScriptObjectData<'gc> { @@ -128,6 +139,10 @@ impl<'gc> ScriptObjectData<'gc> { } } + pub fn install_slot(&mut self) { + self.slots.push(Value::Undefined); + } + pub fn get_property_local( &self, multiname: &Multiname<'gc>, diff --git a/core/src/avm2/vtable.rs b/core/src/avm2/vtable.rs index b69388de1..dad29b98d 100644 --- a/core/src/avm2/vtable.rs +++ b/core/src/avm2/vtable.rs @@ -65,6 +65,33 @@ impl<'gc> VTable<'gc> { )) } + /// A special case for newcatch. A single variable (q)name that maps to slot 1. + pub fn newcatch(mc: MutationContext<'gc, '_>, vname: &QName<'gc>) -> Self { + let mut rt = PropertyMap::new(); + + rt.insert( + *vname, + Property::Slot { + slot_id: 1, + class: PropertyClass::Any, + }, + ); + + let vt = VTable(GcCell::allocate( + mc, + VTableData { + defining_class: None, + scope: None, + protected_namespace: None, + resolved_traits: rt, + method_table: vec![], + default_slots: vec![None, None], + }, + )); + + return vt; + } + pub fn duplicate(self, mc: MutationContext<'gc, '_>) -> Self { VTable(GcCell::allocate(mc, self.0.read().clone())) } diff --git a/swf/src/avm2/read.rs b/swf/src/avm2/read.rs index bd114cc93..441819966 100644 --- a/swf/src/avm2/read.rs +++ b/swf/src/avm2/read.rs @@ -29,6 +29,11 @@ impl<'a> Reader<'a> { ReadSwfExt::seek(self, data, relative_offset as isize) } + #[inline] + pub fn seek_absolute(&mut self, data: &'a [u8], pos: usize) { + ReadSwfExt::seek_absolute(self, data, pos) + } + pub fn read(&mut self) -> Result { let minor_version = self.read_u16()?; let major_version = self.read_u16()?; diff --git a/swf/src/avm2/types.rs b/swf/src/avm2/types.rs index e45a9003b..6bd373b5e 100644 --- a/swf/src/avm2/types.rs +++ b/swf/src/avm2/types.rs @@ -139,7 +139,7 @@ pub struct Exception { pub from_offset: u32, pub to_offset: u32, pub target_offset: u32, - pub variable_name: Index, + pub variable_name: Index, pub type_name: Index, } diff --git a/swf/src/extensions.rs b/swf/src/extensions.rs index 445b76d8d..ba9ba57e8 100644 --- a/swf/src/extensions.rs +++ b/swf/src/extensions.rs @@ -20,6 +20,11 @@ pub trait ReadSwfExt<'a> { *self.as_mut_slice() = &data[pos..]; } + fn seek_absolute(&mut self, data: &'a [u8], pos: usize) { + let pos = pos.min(data.len()); + *self.as_mut_slice() = &data[pos..]; + } + #[inline] fn read_u8(&mut self) -> Result { Ok(ReadBytesExt::read_u8(self.as_mut_slice())?) diff --git a/tests/tests/regression_tests.rs b/tests/tests/regression_tests.rs index f707bee10..d8c36a137 100644 --- a/tests/tests/regression_tests.rs +++ b/tests/tests/regression_tests.rs @@ -449,6 +449,7 @@ swf_tests! { (as3_string_length, "avm2/string_length", 1), (as3_string_match, "avm2/string_match", 1), (as3_string_replace, "avm2/string_replace", 1), + (as3_try_catch, "avm2/try_catch", 1), (as3_string_slice_substr_substring, "avm2/string_slice_substr_substring", 1), (as3_string_split, "avm2/string_split", 1), (as3_subtract, "avm2/subtract", 1), diff --git a/tests/tests/swfs/avm2/try_catch/Test.as b/tests/tests/swfs/avm2/try_catch/Test.as new file mode 100644 index 000000000..b16ca3927 --- /dev/null +++ b/tests/tests/swfs/avm2/try_catch/Test.as @@ -0,0 +1,24 @@ +// compiled with mxmlc + +package { + import flash.display.MovieClip; + public class Test extends MovieClip { + public function Test() { + trace("// Trying to raise and catch an error") +try { + var v = new Vector.(); + v.fixed = true; + v.push("a"); + v.push("bcd"); +} catch(foobar) { + // This cuts the error description to "RangeError" + // which is a cheat to mask the fact that the full error + // descriptions are different between Ruffle and Flash + trace(foobar.toString().slice(0,10)); +} + + } + } +} + + diff --git a/tests/tests/swfs/avm2/try_catch/output.txt b/tests/tests/swfs/avm2/try_catch/output.txt new file mode 100644 index 000000000..8e47bf0ce --- /dev/null +++ b/tests/tests/swfs/avm2/try_catch/output.txt @@ -0,0 +1,2 @@ +// Trying to raise and catch an error +RangeError diff --git a/tests/tests/swfs/avm2/try_catch/test.swf b/tests/tests/swfs/avm2/try_catch/test.swf new file mode 100644 index 000000000..c6fed927c Binary files /dev/null and b/tests/tests/swfs/avm2/try_catch/test.swf differ