From b4624fddce5a4b581244a4388adef2a9ea41bb92 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Tue, 10 Mar 2020 21:49:07 +0100 Subject: [PATCH] avm1: GetVariable and SetVariable look through the scope chain. Fixes #414 GetVariable and SetVariable attempt to resolve paths on each scope in the scope chain. --- core/src/avm1.rs | 102 +++++++++++++----- core/src/avm1/scope.rs | 5 + core/tests/regression_tests.rs | 1 + .../avm1/get_variable_in_scope/output.txt | 30 ++++++ .../swfs/avm1/get_variable_in_scope/test.fla | Bin 0 -> 5515 bytes .../swfs/avm1/get_variable_in_scope/test.swf | Bin 0 -> 892 bytes 6 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 core/tests/swfs/avm1/get_variable_in_scope/output.txt create mode 100644 core/tests/swfs/avm1/get_variable_in_scope/test.fla create mode 100644 core/tests/swfs/avm1/get_variable_in_scope/test.swf diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 2c9797989..bf4e82434 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -675,7 +675,12 @@ impl<'gc> Avm1<'gc> { // `"undefined"`, for example. let path = target.coerce_to_string(self, context)?; Ok(self - .resolve_target_path(context, start, &path)? + .resolve_target_path( + context, + start.root(), + start.object().as_object().unwrap(), + &path, + )? .and_then(|o| o.as_display_object())) } @@ -691,26 +696,24 @@ impl<'gc> Avm1<'gc> { pub fn resolve_target_path( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, - start: DisplayObject<'gc>, + root: DisplayObject<'gc>, + start: Object<'gc>, path: &str, ) -> Result>, Error> { - let root = start.root(); - // Empty path resolves immediately to start clip. if path.is_empty() { - return Ok(Some(start.object().as_object().unwrap())); + return Ok(Some(start)); } // Starting / means an absolute path starting from root. // (`/bar` means `_root.bar`) let mut path = path.as_bytes(); - let (clip, mut is_slash_path) = if path[0] == b'/' { + let (mut object, mut is_slash_path) = if path[0] == b'/' { path = &path[1..]; - (root, true) + (root.object().as_object().unwrap(), true) } else { (start, false) }; - let mut object = clip.object().as_object().unwrap(); // Iterate through each token in the path. while !path.is_empty() { @@ -794,14 +797,20 @@ impl<'gc> Avm1<'gc> { /// `_root/movieClip.foo`, `movieClip:child:_parent`, `blah` /// See the `target_path` test for many examples. /// - /// The string first tries to resolve as a variable or target path. The right-most : or . - /// delimits the variable name, with the left side identifying the target object path. + /// The string first tries to resolve as target path with a variable name, such as + /// "a/b/c:foo". The right-most : or . delimits the variable name, with the left side + /// identifying the target object path. Note that the variable name on the right can + /// contain a slash in this case. This path is resolved on the scope chain; if + /// the path does not resolve to an existing property on a scope, the parent scope is + /// searched. Undefined is returned if no path resolves successfully. /// - /// If there is no variable name, the string will try to resolve as a target path using - /// `resolve_target_path`. + /// If there is no variable name, but the path contains slashes, the path will still try + /// to resolve on the scope chain as above. If this fails to resolve, we consider + /// it a simple variable name and fall through to the variable case + /// (i.e. "a/b/c" would be a variable named "a/b/c", not a path). /// - /// Finally, if the above fails, it is a normal variable resovled via active stack frame - /// the scope chain. + /// Finally, if none of the above applies, it is a normal variable name resovled via the + /// scope chain. pub fn get_variable<'s>( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -822,26 +831,41 @@ impl<'gc> Avm1<'gc> { } _ => false, }); + let b = var_iter.next(); let a = var_iter.next(); - if let (Some(path), Some(var_name)) = (a, b) { // We have a . or :, so this is a path to an object plus a variable name. // We resolve it directly on the targeted object. let path = unsafe { std::str::from_utf8_unchecked(path) }; let var_name = unsafe { std::str::from_utf8_unchecked(var_name) }; - if let Some(object) = self.resolve_target_path(context, start, path)? { - return object.get(var_name, self, context); - } else { - return Ok(Value::Undefined.into()); + + let mut current_scope = Some(self.current_stack_frame().unwrap().read().scope_cell()); + while let Some(scope) = current_scope { + if let Some(object) = + self.resolve_target_path(context, start.root(), *scope.read().locals(), path)? + { + if object.has_property(context, var_name) { + return object.get(var_name, self, context); + } + } + current_scope = scope.read().parent_cell(); } + + return Ok(Value::Undefined.into()); } // If it doesn't have a trailing variable, it can still be a slash path. // We can skip this step if we didn't find a slash above. if has_slash { - if let Some(node) = self.resolve_target_path(context, start, path)? { - return Ok(node.into()); + let mut current_scope = Some(self.current_stack_frame().unwrap().read().scope_cell()); + while let Some(scope) = current_scope { + if let Some(object) = + self.resolve_target_path(context, start.root(), *scope.read().locals(), path)? + { + return Ok(object.into()); + } + current_scope = scope.read().parent_cell(); } } @@ -860,15 +884,21 @@ impl<'gc> Avm1<'gc> { /// `_root/movieClip.foo`, `movieClip:child:_parent`, `blah` /// See the `target_path` test for many examples. /// - /// The string first tries to resolve as a variable or target path. The right-most : or . - /// delimits the variable name, with the left side identifying the target object path. - /// - /// If there is no variable name, the entire string is considered the name, and it is - /// resovled normally via active stack frame and the scope chain. + /// The string first tries to resolve as target path with a variable name, such as + /// "a/b/c:foo". The right-most : or . delimits the variable name, with the left side + /// identifying the target object path. Note that the variable name on the right can + /// contain a slash in this case. This target path (sans variable) is resolved on the + /// scope chain; if the path does not resolve to an existing property on a scope, the + /// parent scope is searched. If the path does not resolve on any scope, the set fails + /// and returns immediately. If the path does resolve, the variable name is created + /// or overwritten on the target scope. /// /// This differs from `get_variable` because slash paths with no variable segment are invalid; /// For example, `foo/bar` sets a property named `foo/bar` on the current stack frame instead /// of drilling into the display list. + /// + /// If the string does not resolve as a path, the path is considered a normal variable + /// name and is set on the scope chain as usual. pub fn set_variable<'s>( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -894,9 +924,18 @@ impl<'gc> Avm1<'gc> { // We resolve it directly on the targeted object. let path = unsafe { std::str::from_utf8_unchecked(path) }; let var_name = unsafe { std::str::from_utf8_unchecked(var_name) }; - if let Some(object) = self.resolve_target_path(context, start, path)? { - object.set(var_name, value, self, context)?; + + let mut current_scope = Some(self.current_stack_frame().unwrap().read().scope_cell()); + while let Some(scope) = current_scope { + if let Some(object) = + self.resolve_target_path(context, start.root(), *scope.read().locals(), path)? + { + object.set(var_name, value, self, context)?; + return Ok(()); + } + current_scope = scope.read().parent_cell(); } + return Ok(()); } @@ -2302,7 +2341,12 @@ impl<'gc> Avm1<'gc> { if target.is_empty() { new_target_clip = Some(base_clip); } else if let Some(clip) = self - .resolve_target_path(context, base_clip, target)? + .resolve_target_path( + context, + base_clip.root(), + base_clip.object().as_object().unwrap(), + target, + )? .and_then(|o| o.as_display_object()) { new_target_clip = Some(clip); diff --git a/core/src/avm1/scope.rs b/core/src/avm1/scope.rs index c3fb9dfaf..1562aa214 100644 --- a/core/src/avm1/scope.rs +++ b/core/src/avm1/scope.rs @@ -220,6 +220,11 @@ impl<'gc> Scope<'gc> { } } + /// Returns a reference to the parent scope object. + pub fn parent_cell(&self) -> Option>> { + self.parent + } + /// Resolve a particular value in the scope chain. /// /// Because scopes are object chains, the same rules for `Object::get` diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 2f0124162..391b9e3c1 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -120,6 +120,7 @@ swf_tests! { (logical_ops_swf4, "avm1/logical_ops_swf4", 1), (logical_ops_swf8, "avm1/logical_ops_swf8", 1), (movieclip_depth_methods, "avm1/movieclip_depth_methods", 3), + (get_variable_in_scope, "avm1/get_variable_in_scope", 1), (greater_swf6, "avm1/greater_swf6", 1), (greater_swf7, "avm1/greater_swf7", 1), (equals_swf4, "avm1/equals_swf4", 1), diff --git a/core/tests/swfs/avm1/get_variable_in_scope/output.txt b/core/tests/swfs/avm1/get_variable_in_scope/output.txt new file mode 100644 index 000000000..11d331e91 --- /dev/null +++ b/core/tests/swfs/avm1/get_variable_in_scope/output.txt @@ -0,0 +1,30 @@ +// a.b.c +from global + +// a.b +from this + +// f() a.b +from f() + +// a.b.c +from global + +// _global.a.b.c.d +global +// _global.a.b.c.d +changed + +// _root.a.b +root +// _root.a.b +changed 2 + +// _root.a.b.c +changed 3 + +// f2() a.b +from f2() +// f2() a.b +changed 4 + diff --git a/core/tests/swfs/avm1/get_variable_in_scope/test.fla b/core/tests/swfs/avm1/get_variable_in_scope/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..d22dcf9390bad1d3cea38b15e06e931a398f14e3 GIT binary patch literal 5515 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/get_variable_in_scope/test.swf b/core/tests/swfs/avm1/get_variable_in_scope/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..d3d23e0a184e2b82eb79b6941f2f974cf51fce42 GIT binary patch literal 892 zcmV-?1B3iSS5pse2mk1mNaVSmbH498Gnyrot^p4I1bDp# zgQG(L`2E#k37{2NwsAQaiOZSmg$A3oE7K_Q4TKhpg}l&Z8r&eY*=!<3Lz*TrgcRO- z5nf7Ocv|U*vBH3aX5jddLp_nt@r2H!cBPWaE@yr&>xDA5=!D27ouMW6kt!>Q>oAbf z4+xHEa7C%x>2j1fa&b5ne+j5f!jQ>hSM2{<7bS5rcU()XHq;h+$Si1)0sA_dqA8N1 zNm}zt(~X*LDCa*as-Y+aZh}mxV-jHtN4P-NCAE2_){Sc2;M4P*yoOEb)N<_GCz0=w zVhcTR^2y$g$Dek0HbWUYj4p}&X>@pX>l1WILOKsj!r-Ur>5Rw58!wEoXOcIA4qK2N z$1+A*y+0fc6unpP4!f$_XbkG3VP73JYCWxSo)o%H&C~|eoX?0CrE1o{P%BU~wTr;H zaoGLXpxRIyyG`lzRCL*=~I>}u6ab%R8nD+!$U#2N*37N3HTgOG5i z+Lg>IZd2^9;cT!;{inh{qGoE=#|3p+(!JWSs*LJ-Tu{HNGd)&ZgZ;Z&re+ljNtWpU zvFT9>F+Z4lpZ@cew#@v7`t!hzpCHRb#3ejh$Zn|`zZjNj*ff~osDpjqbxh0!(NYSh z)ckFM-xJB^uSW~r12?9G(nqi(LVx@2?(U}@c?aN+zu*KgOu*)6Zh!v+AjCTn%M;*V zx}l{zTDoOUv3En*(*)NyE}dW(%mkUv;&C)}Le63Y*@UD`reK2wDfBv^G=jKco2i@} zqh$-SJ`ET{I94uZq2(ie4Zz~r8hqGg;Q&rywGRRaTf+9<7Mu55t8EZ=&UpK*9CxL& z4||*e@HPkU7QlNk3d+3QdY3YfSI<}{2q%vqN^$#HdG#U@d@k%KlB*IVgNrQI=gB-) zdJ)rJmJ1pO>$x)4E~G!?eb625qlRv|xzeO1n~{Ian6Ri?wM zewS3u+vjn6#2jGed@UTGJPn*@_WCT1S%97;Mt^hiI3VZe8~baHTyTk(!nd)+cN-*< SbT%1WZXd(XZ16W5HZNh9b-NA# literal 0 HcmV?d00001