From 771f5685093cc668e48c097b67e124dda7788f9a Mon Sep 17 00:00:00 2001 From: CUB3D Date: Fri, 14 Jul 2023 21:30:32 +0100 Subject: [PATCH] core: Trying to access a named shape should resolve to the parent as they have no object representation --- core/src/avm1/activation.rs | 54 +++++++++++++----- core/src/avm1/globals/array.rs | 4 +- core/src/avm1/globals/bitmap_data.rs | 30 +++++----- core/src/avm1/globals/bitmap_filter.rs | 2 +- .../avm1/globals/displacement_map_filter.rs | 4 +- core/src/avm1/globals/matrix.rs | 12 ++-- core/src/avm1/globals/movie_clip.rs | 10 ++-- core/src/avm1/object.rs | 38 +++++++++--- core/src/avm1/object/script_object.rs | 1 + core/src/avm1/object/stage_object.rs | 13 ++++- core/src/avm1/object/super_object.rs | 3 +- core/src/avm1/scope.rs | 4 +- tests/tests/swfs/avm1/named_shapes/output.txt | 14 +++++ tests/tests/swfs/avm1/named_shapes/test.swf | Bin 0 -> 883 bytes tests/tests/swfs/avm1/named_shapes/test.toml | 1 + 15 files changed, 133 insertions(+), 57 deletions(-) create mode 100644 tests/tests/swfs/avm1/named_shapes/output.txt create mode 100644 tests/tests/swfs/avm1/named_shapes/test.swf create mode 100644 tests/tests/swfs/avm1/named_shapes/test.toml diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 710cf266e..bf14f7cfb 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -1127,7 +1127,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { let object_val = self.context.avm1.pop(); let object = object_val.coerce_to_object(self); - let result = object.get(name, self)?; + let result = object.get_non_slash_path(name, self)?; self.stack_push(result); Ok(FrameControl::Continue) @@ -2563,7 +2563,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { let root = start.avm1_root(); let start = start.object().coerce_to_object(self); Ok(self - .resolve_target_path(root, start, &path, false)? + .resolve_target_path(root, start, &path, false, true)? .and_then(|o| o.as_display_object())) } @@ -2582,6 +2582,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { start: Object<'gc>, mut path: &WStr, mut first_element: bool, + path_has_slash: bool, ) -> Result>, Error<'gc>> { // Empty path resolves immediately to start clip. if path.is_empty() { @@ -2656,10 +2657,25 @@ impl<'a, 'gc> Activation<'a, 'gc> { .and_then(|o| o.as_container()) .and_then(|o| o.child_by_name(name, case_sensitive)) { - child.object() + // If an object doesn't have an object representation, e.g. Graphic, then trying to access it + // Returns the parent instead + if path_has_slash { + child.object() + } else if let crate::display_object::DisplayObject::Graphic(_) = child { + child + .parent() + .map(|p| p.object()) + .unwrap_or(Value::Undefined) + } else { + child.object() + } } else { let name = AvmString::new(self.context.gc_context, name); - object.get(name, self).unwrap() + if path_has_slash { + object.get(name, self).unwrap() + } else { + object.get_non_slash_path(name, self).unwrap() + } } } }; @@ -2689,6 +2705,8 @@ impl<'a, 'gc> Activation<'a, 'gc> { start: DisplayObject<'gc>, path: &'s WStr, ) -> Result, &'s WStr)>, Error<'gc>> { + let path_has_slash = path.contains(b'/'); + // Find the right-most : or . in the path. // If we have one, we must resolve as a target path. if let Some(separator) = path.rfind(b":.".as_ref()) { @@ -2699,9 +2717,13 @@ impl<'a, 'gc> Activation<'a, 'gc> { let mut current_scope = Some(self.scope()); while let Some(scope) = current_scope { let avm1_root = start.avm1_root(); - if let Some(object) = - self.resolve_target_path(avm1_root, *scope.locals(), path, true)? - { + if let Some(object) = self.resolve_target_path( + avm1_root, + *scope.locals(), + path, + true, + path_has_slash, + )? { return Ok(Some((object, var_name))); } current_scope = scope.parent(); @@ -2744,6 +2766,8 @@ impl<'a, 'gc> Activation<'a, 'gc> { // Resolve a variable path for a GetVariable action. let start = self.target_clip_or_root(); + let path_has_slash = path.contains(b'/'); + // Find the right-most : or . in the path. // If we have one, we must resolve as a target path. if let Some(separator) = path.rfind(b":.".as_ref()) { @@ -2754,9 +2778,13 @@ impl<'a, 'gc> Activation<'a, 'gc> { let mut current_scope = Some(self.scope()); while let Some(scope) = current_scope { let avm1_root = start.avm1_root(); - if let Some(object) = - self.resolve_target_path(avm1_root, *scope.locals(), path, true)? - { + if let Some(object) = self.resolve_target_path( + avm1_root, + *scope.locals(), + path, + true, + path_has_slash, + )? { let var_name = AvmString::new(self.context.gc_context, var_name); if object.has_property(self, var_name) { return Ok(CallableValue::Callable(object, object.get(var_name, self)?)); @@ -2774,7 +2802,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { while let Some(scope) = current_scope { let avm1_root = start.avm1_root(); if let Some(object) = - self.resolve_target_path(avm1_root, *scope.locals(), &path, false)? + self.resolve_target_path(avm1_root, *scope.locals(), &path, false, true)? { return Ok(CallableValue::UnCallable(object.into())); } @@ -2841,7 +2869,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { while let Some(scope) = current_scope { let avm1_root = start.avm1_root(); if let Some(object) = - self.resolve_target_path(avm1_root, *scope.locals(), path, true)? + self.resolve_target_path(avm1_root, *scope.locals(), path, true, true)? { let var_name = AvmString::new(self.context.gc_context, var_name); object.set(var_name, value, self)?; @@ -3075,7 +3103,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { if target.is_empty() { new_target_clip = Some(base_clip); } else if let Some(clip) = self - .resolve_target_path(root, start, target, false)? + .resolve_target_path(root, start, target, false, true)? .and_then(|o| o.as_display_object()) .filter(|_| !self.base_clip.avm1_removed()) // All properties invalid if base clip is removed. diff --git a/core/src/avm1/globals/array.rs b/core/src/avm1/globals/array.rs index 15f0a3e52..b75989207 100644 --- a/core/src/avm1/globals/array.rs +++ b/core/src/avm1/globals/array.rs @@ -582,10 +582,10 @@ fn sort_on_compare<'a, 'gc>(fields: &'a [(AvmString<'gc>, SortOptions)]) -> Comp if let [Value::Object(a), Value::Object(b)] = [a, b] { for (field_name, options) in fields { let a_prop = a - .get_local_stored(*field_name, activation) + .get_local_stored(*field_name, activation, false) .unwrap_or(Value::Undefined); let b_prop = b - .get_local_stored(*field_name, activation) + .get_local_stored(*field_name, activation, false) .unwrap_or(Value::Undefined); let result = sort_compare(activation, &a_prop, &b_prop, *options)?; diff --git a/core/src/avm1/globals/bitmap_data.rs b/core/src/avm1/globals/bitmap_data.rs index 01b89c12a..43e7c8a60 100644 --- a/core/src/avm1/globals/bitmap_data.rs +++ b/core/src/avm1/globals/bitmap_data.rs @@ -391,7 +391,7 @@ fn clone<'gc>( if !bitmap_data.disposed() { return Ok(new_bitmap_data( activation.context.gc_context, - this.get_local_stored("__proto__", activation), + this.get_local_stored("__proto__", activation, false), operations::clone(bitmap_data), ) .into()); @@ -756,8 +756,8 @@ fn hit_test<'gc>( .unwrap_or(&Value::Undefined) .coerce_to_object(activation); let top_left = if let (Some(x), Some(y)) = ( - first_point.get_local_stored("x", activation), - first_point.get_local_stored("y", activation), + first_point.get_local_stored("x", activation, false), + first_point.get_local_stored("y", activation, false), ) { (x.coerce_to_i32(activation)?, y.coerce_to_i32(activation)?) } else { @@ -787,8 +787,8 @@ fn hit_test<'gc>( .unwrap_or(&Value::Undefined) .coerce_to_object(activation); let second_point = if let (Some(x), Some(y)) = ( - second_point.get_local_stored("x", activation), - second_point.get_local_stored("y", activation), + second_point.get_local_stored("x", activation, false), + second_point.get_local_stored("y", activation, false), ) { (x.coerce_to_i32(activation)?, y.coerce_to_i32(activation)?) } else { @@ -814,10 +814,10 @@ fn hit_test<'gc>( // Determine what kind of Object we have, point or rectangle. // Duck-typed dumb objects are allowed. let compare_fields = ( - compare_object.get_local_stored("x", activation), - compare_object.get_local_stored("y", activation), - compare_object.get_local_stored("width", activation), - compare_object.get_local_stored("height", activation), + compare_object.get_local_stored("x", activation, false), + compare_object.get_local_stored("y", activation, false), + compare_object.get_local_stored("width", activation, false), + compare_object.get_local_stored("height", activation, false), ); match compare_fields { // BitmapData vs. point @@ -1144,10 +1144,10 @@ fn pixel_dissolve<'gc>( .coerce_to_object(activation); let (src_min_x, src_min_y, src_width, src_height) = if let (Some(x), Some(y), Some(width), Some(height)) = ( - source_rect.get_local_stored("x", activation), - source_rect.get_local_stored("y", activation), - source_rect.get_local_stored("width", activation), - source_rect.get_local_stored("height", activation), + source_rect.get_local_stored("x", activation, false), + source_rect.get_local_stored("y", activation, false), + source_rect.get_local_stored("width", activation, false), + source_rect.get_local_stored("height", activation, false), ) { ( x.coerce_to_f64(activation)? as i32, @@ -1365,7 +1365,7 @@ fn compare<'gc>( match operations::compare(this_bitmap_data, other_bitmap_data) { Some(bitmap_data) => Ok(new_bitmap_data( activation.context.gc_context, - this.get_local_stored("__proto__", activation), + this.get_local_stored("__proto__", activation, false), bitmap_data, ) .into()), @@ -1406,7 +1406,7 @@ fn load_bitmap<'gc>( ); Ok(new_bitmap_data( activation.context.gc_context, - this.get_local_stored("prototype", activation), + this.get_local_stored("prototype", activation, false), bitmap_data, ) .into()) diff --git a/core/src/avm1/globals/bitmap_filter.rs b/core/src/avm1/globals/bitmap_filter.rs index 7b9c428e0..f1bf9e2a6 100644 --- a/core/src/avm1/globals/bitmap_filter.rs +++ b/core/src/avm1/globals/bitmap_filter.rs @@ -67,7 +67,7 @@ pub fn clone<'gc>( ), _ => return Ok(Value::Undefined), }; - let proto = this.get_local_stored("__proto__", activation); + let proto = this.get_local_stored("__proto__", activation, false); Ok(create_instance(activation, native, proto).into()) } diff --git a/core/src/avm1/globals/displacement_map_filter.rs b/core/src/avm1/globals/displacement_map_filter.rs index c29df7269..4a36108c0 100644 --- a/core/src/avm1/globals/displacement_map_filter.rs +++ b/core/src/avm1/globals/displacement_map_filter.rs @@ -177,9 +177,9 @@ impl<'gc> DisplacementMapFilter<'gc> { value: Option<&Value<'gc>>, ) -> Result<(), Error<'gc>> { if let Some(Value::Object(object)) = value { - if let Some(x) = object.get_local_stored("x", activation) { + if let Some(x) = object.get_local_stored("x", activation, false) { let x = x.coerce_to_f64(activation)?.clamp_to_i32(); - if let Some(y) = object.get_local_stored("y", activation) { + if let Some(y) = object.get_local_stored("y", activation, false) { let y = y.coerce_to_f64(activation)?.clamp_to_i32(); self.0.write(activation.context.gc_context).map_point = Point::new(x, y); } diff --git a/core/src/avm1/globals/matrix.rs b/core/src/avm1/globals/matrix.rs index 0d7c2f769..0eca59541 100644 --- a/core/src/avm1/globals/matrix.rs +++ b/core/src/avm1/globals/matrix.rs @@ -114,12 +114,12 @@ pub fn object_to_matrix_or_default<'gc>( ) -> Result> { if let (Some(a), Some(b), Some(c), Some(d), Some(tx), Some(ty)) = ( // These lookups do not search the prototype chain and ignore virtual properties. - object.get_local_stored("a", activation), - object.get_local_stored("b", activation), - object.get_local_stored("c", activation), - object.get_local_stored("d", activation), - object.get_local_stored("tx", activation), - object.get_local_stored("ty", activation), + object.get_local_stored("a", activation, false), + object.get_local_stored("b", activation, false), + object.get_local_stored("c", activation, false), + object.get_local_stored("d", activation, false), + object.get_local_stored("tx", activation, false), + object.get_local_stored("ty", activation, false), ) { let a = a.coerce_to_f64(activation)? as f32; let b = b.coerce_to_f64(activation)? as f32; diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index e6d91cb90..a2fb9773e 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -148,7 +148,7 @@ fn object_to_rectangle<'gc>( const NAMES: &[&str] = &["x", "y", "width", "height"]; let mut values = [0; 4]; for (&name, value) in NAMES.iter().zip(&mut values) { - *value = match object.get_local_stored(name, activation) { + *value = match object.get_local_stored(name, activation, false) { Some(value) => value.coerce_to_i32(activation)?, None => return Ok(None), } @@ -1371,10 +1371,10 @@ fn local_to_global<'gc>( // It does not search the prototype chain and ignores virtual properties. if let (Value::Number(x), Value::Number(y)) = ( point - .get_local_stored("x", activation) + .get_local_stored("x", activation, false) .unwrap_or(Value::Undefined), point - .get_local_stored("y", activation) + .get_local_stored("y", activation, false) .unwrap_or(Value::Undefined), ) { let local = Point::from_pixels(x, y); @@ -1537,10 +1537,10 @@ fn global_to_local<'gc>( // It does not search the prototype chain and ignores virtual properties. if let (Value::Number(x), Value::Number(y)) = ( point - .get_local_stored("x", activation) + .get_local_stored("x", activation, false) .unwrap_or(Value::Undefined), point - .get_local_stored("y", activation) + .get_local_stored("y", activation, false) .unwrap_or(Value::Undefined), ) { let global = Point::from_pixels(x, y); diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index 620bf2998..cb6d867a4 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -89,8 +89,28 @@ pub trait TObject<'gc>: 'gc + Collect + Into> + Clone + Copy { &self, name: impl Into>, activation: &mut Activation<'_, 'gc>, + is_slash_path: bool, ) -> Option> { - self.raw_script_object().get_local_stored(name, activation) + self.raw_script_object() + .get_local_stored(name, activation, is_slash_path) + } + + /// Retrieve a named property from the object, or its prototype. + fn get_non_slash_path( + &self, + name: impl Into>, + activation: &mut Activation<'_, 'gc>, + ) -> Result, Error<'gc>> { + // TODO: Extract logic to a `lookup` function. + let (this, proto) = if let Some(super_object) = self.as_super_object() { + (super_object.this(), super_object.proto(activation)) + } else { + ((*self).into(), Value::Object((*self).into())) + }; + match search_prototype(proto, name.into(), activation, this, false)? { + Some((value, _depth)) => Ok(value), + None => Ok(Value::Undefined), + } } /// Retrieve a named property from the object, or its prototype. @@ -105,7 +125,7 @@ pub trait TObject<'gc>: 'gc + Collect + Into> + Clone + Copy { } else { ((*self).into(), Value::Object((*self).into())) }; - match search_prototype(proto, name.into(), activation, this)? { + match search_prototype(proto, name.into(), activation, this, true)? { Some((value, _depth)) => Ok(value), None => Ok(Value::Undefined), } @@ -127,7 +147,7 @@ pub trait TObject<'gc>: 'gc + Collect + Into> + Clone + Copy { return Err(Error::PrototypeRecursionLimit); } - if let Some(value) = p.get_local_stored(name, activation) { + if let Some(value) = p.get_local_stored(name, activation, true) { return Ok(value); } @@ -257,10 +277,11 @@ pub trait TObject<'gc>: 'gc + Collect + Into> + Clone + Copy { } } - let (method, depth) = match search_prototype(Value::Object(this), name, activation, this)? { - Some((Value::Object(method), depth)) => (method, depth), - _ => return Ok(Value::Undefined), - }; + let (method, depth) = + match search_prototype(Value::Object(this), name, activation, this, false)? { + Some((Value::Object(method), depth)) => (method, depth), + _ => return Ok(Value::Undefined), + }; // If the method was found on the object itself, change `depth` as-if // the method was found on the object's prototype. @@ -669,6 +690,7 @@ pub fn search_prototype<'gc>( name: AvmString<'gc>, activation: &mut Activation<'_, 'gc>, this: Object<'gc>, + is_slash_path: bool, ) -> Result, u8)>, Error<'gc>> { let mut depth = 0; @@ -697,7 +719,7 @@ pub fn search_prototype<'gc>( } } - if let Some(value) = p.get_local_stored(name, activation) { + if let Some(value) = p.get_local_stored(name, activation, is_slash_path) { return Ok(Some((value, depth))); } diff --git a/core/src/avm1/object/script_object.rs b/core/src/avm1/object/script_object.rs index e2b0819d7..ba47e9ad6 100644 --- a/core/src/avm1/object/script_object.rs +++ b/core/src/avm1/object/script_object.rs @@ -155,6 +155,7 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> { &self, name: impl Into>, activation: &mut Activation<'_, 'gc>, + _is_slash_path: bool, ) -> Option> { self.0 .read() diff --git a/core/src/avm1/object/stage_object.rs b/core/src/avm1/object/stage_object.rs index c95dc9ee3..c99185bea 100644 --- a/core/src/avm1/object/stage_object.rs +++ b/core/src/avm1/object/stage_object.rs @@ -193,13 +193,14 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { &self, name: impl Into>, activation: &mut Activation<'_, 'gc>, + is_slash_path: bool, ) -> Option> { let name = name.into(); let obj = self.0.read(); // Property search order for DisplayObjects: // 1) Actual properties on the underlying object - if let Some(value) = obj.base.get_local_stored(name, activation) { + if let Some(value) = obj.base.get_local_stored(name, activation, is_slash_path) { return Some(value); } @@ -217,7 +218,15 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { .as_container() .and_then(|o| o.child_by_name(&name, activation.is_case_sensitive())) { - return Some(child.object()); + return if is_slash_path { + Some(child.object()) + // If an object doesn't have an object representation, e.g. Graphic, then trying to access it + // Returns the parent instead + } else if let crate::display_object::DisplayObject::Graphic(_) = child { + child.parent().map(|p| p.object()) + } else { + Some(child.object()) + }; } // 4) Display object properties such as `_x`, `_y` (never case sensitive) diff --git a/core/src/avm1/object/super_object.rs b/core/src/avm1/object/super_object.rs index b63f7ea47..7f03ab9f3 100644 --- a/core/src/avm1/object/super_object.rs +++ b/core/src/avm1/object/super_object.rs @@ -75,6 +75,7 @@ impl<'gc> TObject<'gc> for SuperObject<'gc> { &self, _name: impl Into>, _activation: &mut Activation<'_, 'gc>, + _is_slash_path: bool, ) -> Option> { None } @@ -124,7 +125,7 @@ impl<'gc> TObject<'gc> for SuperObject<'gc> { ) -> Result, Error<'gc>> { let this = self.0.this; let (method, depth) = - match search_prototype(self.proto(activation), name, activation, this)? { + match search_prototype(self.proto(activation), name, activation, this, false)? { Some((Value::Object(method), depth)) => (method, depth), _ => return Ok(Value::Undefined), }; diff --git a/core/src/avm1/scope.rs b/core/src/avm1/scope.rs index 72ba2ec1a..cd0eb083d 100644 --- a/core/src/avm1/scope.rs +++ b/core/src/avm1/scope.rs @@ -140,7 +140,7 @@ impl<'gc> Scope<'gc> { if self.locals().has_property(activation, name) { return self .locals() - .get(name, activation) + .get_non_slash_path(name, activation) .map(|v| CallableValue::Callable(self.locals_cell(), v)); } if let Some(scope) = self.parent() { @@ -155,7 +155,7 @@ impl<'gc> Scope<'gc> { return activation .root_object() .coerce_to_object(activation) - .get(name, activation) + .get_non_slash_path(name, activation) .map(|v| CallableValue::Callable(self.locals_cell(), v)); } } diff --git a/tests/tests/swfs/avm1/named_shapes/output.txt b/tests/tests/swfs/avm1/named_shapes/output.txt new file mode 100644 index 000000000..6a2b15580 --- /dev/null +++ b/tests/tests/swfs/avm1/named_shapes/output.txt @@ -0,0 +1,14 @@ +root: _level0 +_root.foo: foo1 +_root.real: _level0.real +_root.real.foo: foo2 +get(root.real.foo): foo2 +get(root.real/foo): undefined +get(root.real:foo): foo2 +_root.empty: _level0 +_root.empty.foo: foo1 +get(root.empty.foo): foo1 +get(root.empty/foo): undefined +get(root.empty:foo): foo1 +_root.nothere: undefined +_root.nothere.foo: undefined diff --git a/tests/tests/swfs/avm1/named_shapes/test.swf b/tests/tests/swfs/avm1/named_shapes/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..b3288176c496fe1b21eb443a2c2d2f5df151ad3d GIT binary patch literal 883 zcmZuwy-ve07(7$a6bUFlf-D^(1_VnHj8zdJeiw=a10$-a3zZ6v5(|kpKw@FabMZ)g zpPLxRb(SdSyZi2J=j!nx$s53p_`wM13tX&&o@fmi=A$uw+h^VrwFEPO@8*txfP72! zCr%m(*?cV!UPQoCrj$mfSOmsOp=g@-A?^r?mYGCICWlin;H$QQXIj+qlD%)$$3&h_ zCn@e@*Sm5UcvFsON{ienK`(LBT<;buWwb7(sxMY117fXKEKcar>{Q}yxV(U+<+|on z?nUZ7j@M;>*6s5U+H$jP7Db0<`1FGyozWn@7o;<4=OxJx2l*W7-;S) U-0pO<`9I;So@