diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 5737b5209f9c..28f97be41c0b 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -211,7 +211,6 @@ impl<'gc> Avm1<'gc> { active_clip, clip_obj.into(), None, - None, ); if let Err(e) = child_activation.run_actions(code) { root_error_handler(&mut child_activation, e); @@ -251,7 +250,6 @@ impl<'gc> Avm1<'gc> { active_clip, clip_obj.into(), None, - None, ); function(&mut activation) } @@ -300,7 +298,6 @@ impl<'gc> Avm1<'gc> { active_clip, clip_obj.into(), None, - None, ); if let Err(e) = child_activation.run_actions(code) { root_error_handler(&mut child_activation, e); diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 16b5f712bd31..5d6b585da452 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -194,14 +194,20 @@ pub struct Activation<'a, 'gc: 'a, 'gc_context: 'a> { constant_pool: GcCell<'gc, Vec>>, /// The immutable value of `this`. + /// + /// This differs from Flash Player, where `this` is mutable and seems + /// to be part of the scope chain (e.g. a function with the `suppress_this` flag + /// set can modify the `this` value of its closure). + /// + /// Fortunately, ActionScript syntax prevents mutating `this` altogether, so + /// observing this behavior requires manually-crafted bytecode. + /// + /// TODO: implement correct semantics for mutable `this`. this: Value<'gc>, /// The function object being called. pub callee: Option>, - /// The arguments this function was called by. - pub arguments: Option>, - /// Local registers, if any. /// /// None indicates a function executing out of the global register set. @@ -254,7 +260,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { base_clip: DisplayObject<'gc>, this: Value<'gc>, callee: Option>, - arguments: Option>, ) -> Self { avm_debug!(context.avm1, "START {}", id); Self { @@ -268,7 +273,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { base_clip_unloaded: base_clip.removed(), this, callee, - arguments, local_registers: None, actions_since_timeout_check: 0, } @@ -293,7 +297,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { base_clip_unloaded: self.base_clip_unloaded, this: self.this, callee: self.callee, - arguments: self.arguments, local_registers: self.local_registers, actions_since_timeout_check: 0, } @@ -329,7 +332,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { base_clip_unloaded: base_clip.removed(), this: globals.into(), callee: None, - arguments: None, local_registers: None, actions_since_timeout_check: 0, } @@ -383,7 +385,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { active_clip, clip_obj.into(), None, - None, ); child_activation.run_actions(code) } @@ -421,7 +422,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { active_clip, clip_obj.into(), None, - None, ); function(&mut activation) } @@ -2131,7 +2131,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { self.base_clip, self.this, self.callee, - self.arguments, ); match catch_vars { @@ -2737,12 +2736,6 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { return Ok(CallableValue::UnCallable(self.this_cell())); } - if &name == b"arguments" && self.arguments.is_some() { - return Ok(CallableValue::UnCallable(Value::Object( - self.arguments.unwrap(), - ))); - } - self.scope_cell().read().resolve(name, self) } diff --git a/core/src/avm1/function.rs b/core/src/avm1/function.rs index b34a59ca4231..a315af63756d 100644 --- a/core/src/avm1/function.rs +++ b/core/src/avm1/function.rs @@ -143,6 +143,131 @@ impl<'gc> Avm1Function<'gc> { pub fn register_count(&self) -> u8 { self.register_count } + + fn debug_string_for_call(&self, name: ExecutionName<'gc>, args: &[Value<'gc>]) -> String { + let mut result = match self.name.map(ExecutionName::Dynamic).unwrap_or(name) { + ExecutionName::Static(n) => n.to_owned(), + ExecutionName::Dynamic(n) => n.to_utf8_lossy().into_owned(), + }; + result.push('('); + for i in 0..args.len() { + result.push_str(args.get(i).unwrap().type_of()); + if i < args.len() - 1 { + result.push_str(", "); + } + } + result.push(')'); + result + } + + fn load_this(&self, frame: &mut Activation<'_, 'gc, '_>, this: Value<'gc>, preload_r: &mut u8) { + let preload = self.flags.contains(FunctionFlags::PRELOAD_THIS); + let suppress = self.flags.contains(FunctionFlags::SUPPRESS_THIS); + + if preload { + // The register is set to undefined if both flags are set. + let this = if suppress { Value::Undefined } else { this }; + frame.set_local_register(*preload_r, this); + *preload_r += 1; + } + } + + fn load_arguments( + &self, + frame: &mut Activation<'_, 'gc, '_>, + args: &[Value<'gc>], + caller: Option>, + preload_r: &mut u8, + ) { + let preload = self.flags.contains(FunctionFlags::PRELOAD_ARGUMENTS); + let suppress = self.flags.contains(FunctionFlags::SUPPRESS_ARGUMENTS); + + if suppress && !preload { + return; + } + + let arguments = ArrayObject::new( + frame.context.gc_context, + frame.context.avm1.prototypes().array, + args.iter().cloned(), + ); + + arguments.define_value( + frame.context.gc_context, + "callee", + frame.callee.unwrap().into(), + Attribute::DONT_ENUM, + ); + + arguments.define_value( + frame.context.gc_context, + "caller", + caller.map(Value::from).unwrap_or(Value::Null), + Attribute::DONT_ENUM, + ); + + let arguments = Value::from(arguments); + + // Contrarily to `this` and `super`, setting both flags is equivalent to just setting `preload`. + if preload { + frame.set_local_register(*preload_r, arguments); + *preload_r += 1; + } else { + frame.force_define_local("arguments".into(), arguments); + } + } + + fn load_super( + &self, + frame: &mut Activation<'_, 'gc, '_>, + this: Option>, + depth: u8, + preload_r: &mut u8, + ) { + let preload = self.flags.contains(FunctionFlags::PRELOAD_SUPER); + let suppress = self.flags.contains(FunctionFlags::SUPPRESS_SUPER); + + // TODO: `super` should only be defined if this was a method call (depth > 0?) + // `f[""]()` emits a CallMethod op, causing `this` to be undefined, but `super` is a function; what is it? + let zuper = this + .filter(|_| !suppress) + .map(|this| SuperObject::new(frame, this, depth).into()); + + if preload { + // The register is set to undefined if both flags are set. + frame.set_local_register(*preload_r, zuper.unwrap_or(Value::Undefined)); + } else if let Some(zuper) = zuper { + frame.force_define_local("super".into(), zuper); + } + } + + fn load_root(&self, frame: &mut Activation<'_, 'gc, '_>, preload_r: &mut u8) { + if self.flags.contains(FunctionFlags::PRELOAD_ROOT) { + let root = self.base_clip.avm1_root().object(); + frame.set_local_register(*preload_r, root); + *preload_r += 1; + } + } + + fn load_parent(&self, frame: &mut Activation<'_, 'gc, '_>, preload_r: &mut u8) { + if self.flags.contains(FunctionFlags::PRELOAD_PARENT) { + // If _parent is undefined (because this is a root timeline), it actually does not get pushed, + // and _global ends up incorrectly taking _parent's register. + // See test for more info. + if let Some(parent) = self.base_clip.avm1_parent() { + frame.set_local_register(*preload_r, parent.object()); + *preload_r += 1; + } + } + } + + fn load_global(&self, frame: &mut Activation<'_, 'gc, '_>, preload_r: &mut u8) { + if self.flags.contains(FunctionFlags::PRELOAD_GLOBAL) { + let global = frame.context.avm1.global_object(); + frame.set_local_register(*preload_r, global); + *preload_r += 1; + } + } } #[derive(Debug, Clone, Collect)] @@ -218,200 +343,124 @@ impl<'gc> Executable<'gc> { reason: ExecutionReason, callee: Object<'gc>, ) -> Result, Error<'gc>> { - match self { + let af = match self { Executable::Native(nf) => { // TODO: Change NativeFunction to accept `this: Value`. let this = this.coerce_to_object(activation); - nf(activation, this, args) + return nf(activation, this, args); } - Executable::Action(af) => { - let this_obj = match this { - Value::Object(obj) => Some(obj), - _ => None, - }; - - let target = activation.target_clip_or_root(); - let is_closure = activation.swf_version() >= 6; - let base_clip = if (is_closure || reason == ExecutionReason::Special) - && !af.base_clip.removed() - { - af.base_clip - } else { - this_obj - .and_then(|this| this.as_display_object()) - .unwrap_or(target) - }; - let (swf_version, parent_scope) = if is_closure { - // Function calls in a v6+ SWF are proper closures, and "close" over the scope that defined the function: - // * Use the SWF version from the SWF that defined the function. - // * Use the base clip from when the function was defined. - // * Close over the scope from when the function was defined. - (af.swf_version(), af.scope()) - } else { - // Function calls in a v5 SWF are *not* closures, and will use the settings of - // `this`, regardless of the function's origin: - // * Use the SWF version of `this`. - // * Use the base clip of `this`. - // * Allocate a new scope using the given base clip. No previous scope is closed over. - let swf_version = base_clip.swf_version(); - let base_clip_obj = match base_clip.object() { - Value::Object(o) => o, - _ => unreachable!(), - }; - // TODO: It would be nice to avoid these extra Scope allocs. - let scope = GcCell::allocate( - activation.context.gc_context, - Scope::new( - GcCell::allocate( - activation.context.gc_context, - Scope::from_global_object(activation.context.avm1.globals), - ), - super::scope::ScopeClass::Target, - base_clip_obj, - ), - ); - (swf_version, scope) - }; - - let child_scope = GcCell::allocate( - activation.context.gc_context, - Scope::new_local_scope(parent_scope, activation.context.gc_context), - ); - - let arguments = if af.flags.contains(FunctionFlags::SUPPRESS_ARGUMENTS) { - ArrayObject::empty(activation) - } else { - ArrayObject::new( + Executable::Action(af) => af, + }; + + let this_obj = match this { + Value::Object(obj) => Some(obj), + _ => None, + }; + + let target = activation.target_clip_or_root(); + let is_closure = activation.swf_version() >= 6; + let base_clip = + if (is_closure || reason == ExecutionReason::Special) && !af.base_clip.removed() { + af.base_clip + } else { + this_obj + .and_then(|this| this.as_display_object()) + .unwrap_or(target) + }; + let (swf_version, parent_scope) = if is_closure { + // Function calls in a v6+ SWF are proper closures, and "close" over the scope that defined the function: + // * Use the SWF version from the SWF that defined the function. + // * Use the base clip from when the function was defined. + // * Close over the scope from when the function was defined. + (af.swf_version(), af.scope()) + } else { + // Function calls in a v5 SWF are *not* closures, and will use the settings of + // `this`, regardless of the function's origin: + // * Use the SWF version of `this`. + // * Use the base clip of `this`. + // * Allocate a new scope using the given base clip. No previous scope is closed over. + let swf_version = base_clip.swf_version(); + let base_clip_obj = match base_clip.object() { + Value::Object(o) => o, + _ => unreachable!(), + }; + // TODO: It would be nice to avoid these extra Scope allocs. + let scope = GcCell::allocate( + activation.context.gc_context, + Scope::new( + GcCell::allocate( activation.context.gc_context, - activation.context.avm1.prototypes().array, - args.iter().cloned(), - ) - }; - arguments.define_value( - activation.context.gc_context, - "callee", - callee.into(), - Attribute::DONT_ENUM, - ); - // The caller is the previous callee. - arguments.define_value( - activation.context.gc_context, - "caller", - activation.callee.map(Value::from).unwrap_or(Value::Null), - Attribute::DONT_ENUM, - ); - - // TODO: `super` should only be defined if this was a method call (depth > 0?) - // `f[""]()` emits a CallMethod op, causing `this` to be undefined, but `super` is a function; what is it? - let super_object: Option> = this_obj.and_then(|this| { - if !af.flags.contains(FunctionFlags::SUPPRESS_SUPER) { - Some(SuperObject::new(activation, this, depth).into()) - } else { - None - } - }); - - let name = if cfg!(feature = "avm_debug") { - let mut result = match af.name.map(ExecutionName::Dynamic).unwrap_or(name) { - ExecutionName::Static(n) => n.to_owned(), - ExecutionName::Dynamic(n) => n.to_utf8_lossy().into_owned(), - }; - - result.push('('); - for i in 0..args.len() { - result.push_str(args.get(i).unwrap().type_of()); - if i < args.len() - 1 { - result.push_str(", "); - } - } - result.push(')'); - - Cow::Owned(result) - } else { - Cow::Borrowed("[Anonymous]") - }; - - let max_recursion_depth = activation.context.avm1.max_recursion_depth(); - let mut frame = Activation::from_action( - activation.context.reborrow(), - activation.id.function(name, reason, max_recursion_depth)?, - swf_version, - child_scope, - af.constant_pool, - base_clip, - this, - Some(callee), - Some(arguments.into()), - ); - - frame.allocate_local_registers(af.register_count(), frame.context.gc_context); - - let mut preload_r = 1; - - if af.flags.contains(FunctionFlags::PRELOAD_THIS) { - //TODO: What happens if you specify both suppress and - //preload for this? - frame.set_local_register(preload_r, this); - preload_r += 1; - } + Scope::from_global_object(activation.context.avm1.globals), + ), + super::scope::ScopeClass::Target, + base_clip_obj, + ), + ); + (swf_version, scope) + }; - if af.flags.contains(FunctionFlags::PRELOAD_ARGUMENTS) { - //TODO: What happens if you specify both suppress and - //preload for arguments? - frame.set_local_register(preload_r, arguments.into()); - preload_r += 1; - } + let child_scope = GcCell::allocate( + activation.context.gc_context, + Scope::new_local_scope(parent_scope, activation.context.gc_context), + ); - if let Some(super_object) = super_object { - if af.flags.contains(FunctionFlags::PRELOAD_SUPER) { - frame.set_local_register(preload_r, super_object.into()); - //TODO: What happens if you specify both suppress and - //preload for super? - preload_r += 1; - } else { - frame.force_define_local("super".into(), super_object.into()); - } - } + // The caller is the previous callee. + let arguments_caller = activation.callee; - if af.flags.contains(FunctionFlags::PRELOAD_ROOT) { - frame.set_local_register(preload_r, af.base_clip.avm1_root().object()); - preload_r += 1; - } + let name = if cfg!(feature = "avm_debug") { + Cow::Owned(af.debug_string_for_call(name, args)) + } else { + Cow::Borrowed("[Anonymous]") + }; - if af.flags.contains(FunctionFlags::PRELOAD_PARENT) { - // If _parent is undefined (because this is a root timeline), it actually does not get pushed, - // and _global ends up incorrectly taking _parent's register. - // See test for more info. - if let Some(parent) = af.base_clip.avm1_parent() { - frame.set_local_register(preload_r, parent.object()); - preload_r += 1; - } - } + let is_this_inherited = af + .flags + .intersects(FunctionFlags::PRELOAD_THIS | FunctionFlags::SUPPRESS_THIS); + let local_this = if is_this_inherited { + activation.this_cell() + } else { + this + }; - if af.flags.contains(FunctionFlags::PRELOAD_GLOBAL) { - let global = frame.context.avm1.global_object(); - frame.set_local_register(preload_r, global); - } + let max_recursion_depth = activation.context.avm1.max_recursion_depth(); + let mut frame = Activation::from_action( + activation.context.reborrow(), + activation.id.function(name, reason, max_recursion_depth)?, + swf_version, + child_scope, + af.constant_pool, + base_clip, + local_this, + Some(callee), + ); - // Any unassigned args are set to undefined to prevent assignments from leaking to the parent scope (#2166) - let args_iter = args - .iter() - .cloned() - .chain(std::iter::repeat(Value::Undefined)); - - //TODO: What happens if the argument registers clash with the - //preloaded registers? What gets done last? - for (param, value) in af.params.iter().zip(args_iter) { - if let Some(register) = param.register { - frame.set_local_register(register.get(), value); - } else { - frame.force_define_local(param.name, value); - } - } + frame.allocate_local_registers(af.register_count(), frame.context.gc_context); + + let mut preload_r = 1; + af.load_this(&mut frame, this, &mut preload_r); + af.load_arguments(&mut frame, args, arguments_caller, &mut preload_r); + af.load_super(&mut frame, this_obj, depth, &mut preload_r); + af.load_root(&mut frame, &mut preload_r); + af.load_parent(&mut frame, &mut preload_r); + af.load_global(&mut frame, &mut preload_r); - Ok(frame.run_actions(af.data.clone())?.value()) + // Any unassigned args are set to undefined to prevent assignments from leaking to the parent scope (#2166) + let args_iter = args + .iter() + .cloned() + .chain(std::iter::repeat(Value::Undefined)); + + //TODO: What happens if the argument registers clash with the + //preloaded registers? What gets done last? + for (param, value) in af.params.iter().zip(args_iter) { + if let Some(register) = param.register { + frame.set_local_register(register.get(), value); + } else { + frame.force_define_local(param.name, value); } } + + Ok(frame.run_actions(af.data.clone())?.value()) } } diff --git a/tests/tests/regression_tests.rs b/tests/tests/regression_tests.rs index 1ae965449452..6cd9cfa31033 100644 --- a/tests/tests/regression_tests.rs +++ b/tests/tests/regression_tests.rs @@ -577,6 +577,7 @@ swf_tests! { (function_as_function, "avm1/function_as_function", 1), (function_base_clip_removed, "avm1/function_base_clip_removed", 3), (function_base_clip, "avm1/function_base_clip", 2), + (function_suppress_and_preload, "avm1/function_suppress_and_preload", 1), (funky_function_calls, "avm1/funky_function_calls", 1), (get_bytes_total, "avm1/get_bytes_total", 1), (getproperty_swf4, "avm1/getproperty_swf4", 1), diff --git a/tests/tests/swfs/avm1/function_suppress_and_preload/output.txt b/tests/tests/swfs/avm1/function_suppress_and_preload/output.txt new file mode 100644 index 000000000000..92c81372583e --- /dev/null +++ b/tests/tests/swfs/avm1/function_suppress_and_preload/output.txt @@ -0,0 +1,28 @@ +// base +this: THIS +super: [object Object] +arguments: 1,2 +reg 1: undefined +reg 2: undefined +reg 3: undefined +// suppress +this: _level0 +super: undefined +arguments: undefined +reg 1: undefined +reg 2: undefined +reg 3: undefined +// preload +this: _level0 +super: undefined +arguments: undefined +reg 1: THIS +reg 2: 1,2 +reg 3: [object Object] +// suppress_preload +this: _level0 +super: undefined +arguments: undefined +reg 1: undefined +reg 2: 1,2 +reg 3: undefined diff --git a/tests/tests/swfs/avm1/function_suppress_and_preload/test.swf b/tests/tests/swfs/avm1/function_suppress_and_preload/test.swf new file mode 100644 index 000000000000..70bb9714681f Binary files /dev/null and b/tests/tests/swfs/avm1/function_suppress_and_preload/test.swf differ