diff --git a/core/src/avm2/amf.rs b/core/src/avm2/amf.rs index 316261bdf8c1..80bff6efaf36 100644 --- a/core/src/avm2/amf.rs +++ b/core/src/avm2/amf.rs @@ -457,8 +457,12 @@ pub fn deserialize_value_impl<'gc>( let class = alias_to_class(activation, name)?; // Create an empty vector, as it has to exist in the map before reading children, in case they reference it - let empty_storage = - VectorStorage::new(0, *is_fixed, Some(class.inner_class_definition())); + let empty_storage = VectorStorage::new( + 0, + *is_fixed, + Some(class.inner_class_definition()), + activation, + ); let obj = VectorObject::from_vector(empty_storage, activation); object_map.insert(*id, obj.into()); diff --git a/core/src/avm2/call_stack.rs b/core/src/avm2/call_stack.rs index ebb7acb287ae..0f4d2bc98d1f 100644 --- a/core/src/avm2/call_stack.rs +++ b/core/src/avm2/call_stack.rs @@ -22,6 +22,11 @@ impl<'gc> CallStack<'gc> { self.stack.pop(); } + /// Iterate methods from most-recently-pushed to least-recently-pushed. + pub fn iter_top_down(&self) -> impl Iterator> + '_ { + self.stack.iter().rev().copied() + } + pub fn display(&self, output: &mut WString) { for method in self.stack.iter().rev() { output.push_utf8("\n\tat "); diff --git a/core/src/avm2/globals/flash/display/graphics.rs b/core/src/avm2/globals/flash/display/graphics.rs index d0924764a5a7..7580beb7109a 100644 --- a/core/src/avm2/globals/flash/display/graphics.rs +++ b/core/src/avm2/globals/flash/display/graphics.rs @@ -1347,7 +1347,7 @@ pub fn read_graphics_data<'gc>( ) -> Result, Error<'gc>> { avm2_stub_method!(activation, "flash.display.Graphics", "readGraphicsData"); let value_type = activation.avm2().class_defs().igraphicsdata; - let new_storage = VectorStorage::new(0, false, Some(value_type)); + let new_storage = VectorStorage::new(0, false, Some(value_type), activation); Ok(VectorObject::from_vector(new_storage, activation).into()) } diff --git a/core/src/avm2/globals/flash/geom/transform.rs b/core/src/avm2/globals/flash/geom/transform.rs index a5b10c2cfb78..18bbd07869fd 100644 --- a/core/src/avm2/globals/flash/geom/transform.rs +++ b/core/src/avm2/globals/flash/geom/transform.rs @@ -196,7 +196,7 @@ pub fn matrix3d_to_object<'gc>( activation: &mut Activation<'_, 'gc>, ) -> Result, Error<'gc>> { let number = activation.avm2().class_defs().number; - let mut raw_data_storage = VectorStorage::new(16, true, Some(number)); + let mut raw_data_storage = VectorStorage::new(16, true, Some(number), activation); for (i, data) in matrix.raw_data.iter().enumerate() { raw_data_storage.set(i, Value::Number(*data), activation)?; } diff --git a/core/src/avm2/globals/vector.rs b/core/src/avm2/globals/vector.rs index 2e18540a4c61..110a91c40757 100644 --- a/core/src/avm2/globals/vector.rs +++ b/core/src/avm2/globals/vector.rs @@ -79,7 +79,7 @@ pub fn call_handler<'gc>( .get_public_property(istr!("length"), activation)? .coerce_to_i32(activation)?; - let mut new_storage = VectorStorage::new(0, false, value_type); + let mut new_storage = VectorStorage::new(0, false, value_type, activation); new_storage.reserve_exact(length as usize); let value_type_for_coercion = new_storage.value_type_for_coercion(activation); @@ -269,7 +269,7 @@ pub fn filter<'gc>( .param() .expect("Receiver is parametrized vector"); // technically unreachable - let mut new_storage = VectorStorage::new(0, false, value_type); + let mut new_storage = VectorStorage::new(0, false, value_type, activation); let callback = match args.try_get_function(0) { None => return Ok(VectorObject::from_vector(new_storage, activation).into()), @@ -366,7 +366,7 @@ pub fn map<'gc>( .param() .expect("Receiver is parametrized vector"); // technically unreachable - let mut new_storage = VectorStorage::new(0, false, value_type); + let mut new_storage = VectorStorage::new(0, false, value_type, activation); let callback = match args.try_get_function(0) { None => { @@ -571,7 +571,7 @@ pub fn slice<'gc>( let from = vs.clamp_parameter_index(from); let to = vs.clamp_parameter_index(to); - let mut new_vs = VectorStorage::new(0, false, value_type); + let mut new_vs = VectorStorage::new(0, false, value_type, activation); if to > from { for value in vs.iter().skip(from).take(to - from) { diff --git a/core/src/avm2/object/vector_object.rs b/core/src/avm2/object/vector_object.rs index df753a2e1b29..b0e8f0b3f1e0 100644 --- a/core/src/avm2/object/vector_object.rs +++ b/core/src/avm2/object/vector_object.rs @@ -30,7 +30,7 @@ pub fn vector_allocator<'gc>( activation.gc(), VectorObjectData { base, - vector: RefLock::new(VectorStorage::new(0, false, param_type)), + vector: RefLock::new(VectorStorage::new(0, false, param_type, activation)), }, )) .into()) @@ -152,8 +152,8 @@ impl<'gc> VectorObject<'gc> { let type_of = self.0.vector.borrow().value_type_for_coercion(activation); let value = match value.coerce_to_type(activation, type_of)? { - Value::Undefined => self.0.vector.borrow().default(), - Value::Null => self.0.vector.borrow().default(), + Value::Undefined => self.0.vector.borrow().default(activation), + Value::Null => self.0.vector.borrow().default(activation), v => v, }; diff --git a/core/src/avm2/vector.rs b/core/src/avm2/vector.rs index 5293618d5583..ec61f983c86a 100644 --- a/core/src/avm2/vector.rs +++ b/core/src/avm2/vector.rs @@ -4,6 +4,7 @@ use crate::avm2::Error; use crate::avm2::activation::Activation; use crate::avm2::class::Class; use crate::avm2::error::{make_error_1125, make_error_1126}; +use crate::avm2::method::MethodKind; use crate::avm2::value::Value; use gc_arena::Collect; use std::cmp::{max, min}; @@ -40,7 +41,12 @@ pub struct VectorStorage<'gc> { } impl<'gc> VectorStorage<'gc> { - pub fn new(length: usize, is_fixed: bool, value_type: Option>) -> Self { + pub fn new( + length: usize, + is_fixed: bool, + value_type: Option>, + activation: &mut Activation<'_, 'gc>, + ) -> Self { let storage = Vec::new(); let mut self_vec = VectorStorage { @@ -49,7 +55,9 @@ impl<'gc> VectorStorage<'gc> { value_type, }; - self_vec.storage.resize(length, self_vec.default()); + self_vec + .storage + .resize(length, self_vec.default(activation)); self_vec } @@ -99,16 +107,18 @@ impl<'gc> VectorStorage<'gc> { activation: &mut Activation<'_, 'gc>, ) -> Result<(), Error<'gc>> { self.check_fixed(activation)?; - self.storage.resize(new_length, self.default()); + self.storage.resize(new_length, self.default(activation)); Ok(()) } /// Get the default value for this vector. - pub fn default(&self) -> Value<'gc> { + pub fn default(&self, activation: &mut Activation<'_, 'gc>) -> Value<'gc> { if let Some(value_type) = self.value_type { if value_type.is_builtin_numeric() { Value::Integer(0) + } else if value_type.is_builtin_boolean() { + bool_default(activation) } else { Value::Null } @@ -180,7 +190,7 @@ impl<'gc> VectorStorage<'gc> { activation: &mut Activation<'_, 'gc>, ) -> Result<(), Error<'gc>> { if !self.is_fixed && pos == self.length() { - self.storage.resize(pos + 1, self.default()); + self.storage.resize(pos + 1, self.default(activation)); } if let Some(v) = self.storage.get_mut(pos) { @@ -373,3 +383,33 @@ impl<'gc> VectorStorage<'gc> { &self.storage } } + +/// Determine the default value for `Vector.` holes. +/// +/// Flash returns `null` when the fill happens from a script initializer +/// (top-level code), regardless of SWF version. From any other context the +/// default is `false` for SWF version >= 14 and `null` for earlier versions. +fn bool_default<'gc>(activation: &mut Activation<'_, 'gc>) -> Value<'gc> { + let call_stack = activation.avm2().call_stack(); + let call_stack = call_stack.borrow(); + + let calling_method = call_stack + .iter_top_down() + .find(|m| matches!(m.method_kind(), MethodKind::Bytecode { .. })); + + if let Some(method) = calling_method { + let is_script_init = method + .bound_class() + .is_some_and(|c| c.is_script_traits() && c.instance_init() == Some(method)); + + if is_script_init { + return Value::Null; + } + } + + if activation.caller_movie_or_root().version() >= 14 { + Value::Bool(false) + } else { + Value::Null + } +} diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor/Test.as b/tests/tests/swfs/avm2/vector_boolean_default_constructor/Test.as new file mode 100644 index 000000000000..a7ae284b5550 --- /dev/null +++ b/tests/tests/swfs/avm2/vector_boolean_default_constructor/Test.as @@ -0,0 +1,31 @@ +// Reproduction from Ruffle issue #23317. +// https://github.com/ruffle-rs/ruffle/issues/23317 +// +// Compile with Apache Flex SDK's mxmlc. The reporter used: +// mxmlc -o test.swf -debug Test.as +// which defaults to a modern SWF version (>=14), where Flash and Ruffle +// disagree on the constructor case. To reproduce the SWF-version split, +// compile with: +// mxmlc -o test_v13.swf -debug Test.as -swf-version 13 // Flash agrees with Ruffle +// mxmlc -o test_v14.swf -debug Test.as -swf-version 14 // Flash diverges +// +// Flash output (SWF version >= 14): +// null <- top-level allocation +// false <- constructor allocation +// +// Ruffle output before the fix: +// null +// null + +package { + import flash.display.Sprite; + public class Test extends Sprite { + public function Test() { + var vec:Vector. = new Vector.(1, true); + trace(vec[0]); // false on Flash, null on Ruffle (before fix) + } + } +} + +var vec:Vector. = new Vector.(1, true); +trace(vec[0]); // null on both diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor/output.txt b/tests/tests/swfs/avm2/vector_boolean_default_constructor/output.txt new file mode 100644 index 000000000000..5bfdc2ff5dc4 --- /dev/null +++ b/tests/tests/swfs/avm2/vector_boolean_default_constructor/output.txt @@ -0,0 +1,2 @@ +null +false diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor/test.swf b/tests/tests/swfs/avm2/vector_boolean_default_constructor/test.swf new file mode 100644 index 000000000000..34ac75af933e Binary files /dev/null and b/tests/tests/swfs/avm2/vector_boolean_default_constructor/test.swf differ diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor/test.toml b/tests/tests/swfs/avm2/vector_boolean_default_constructor/test.toml new file mode 100644 index 000000000000..dbee897f5863 --- /dev/null +++ b/tests/tests/swfs/avm2/vector_boolean_default_constructor/test.toml @@ -0,0 +1 @@ +num_frames = 1 diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/Test.as b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/Test.as new file mode 100644 index 000000000000..a7ae284b5550 --- /dev/null +++ b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/Test.as @@ -0,0 +1,31 @@ +// Reproduction from Ruffle issue #23317. +// https://github.com/ruffle-rs/ruffle/issues/23317 +// +// Compile with Apache Flex SDK's mxmlc. The reporter used: +// mxmlc -o test.swf -debug Test.as +// which defaults to a modern SWF version (>=14), where Flash and Ruffle +// disagree on the constructor case. To reproduce the SWF-version split, +// compile with: +// mxmlc -o test_v13.swf -debug Test.as -swf-version 13 // Flash agrees with Ruffle +// mxmlc -o test_v14.swf -debug Test.as -swf-version 14 // Flash diverges +// +// Flash output (SWF version >= 14): +// null <- top-level allocation +// false <- constructor allocation +// +// Ruffle output before the fix: +// null +// null + +package { + import flash.display.Sprite; + public class Test extends Sprite { + public function Test() { + var vec:Vector. = new Vector.(1, true); + trace(vec[0]); // false on Flash, null on Ruffle (before fix) + } + } +} + +var vec:Vector. = new Vector.(1, true); +trace(vec[0]); // null on both diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/output.txt b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/output.txt new file mode 100644 index 000000000000..c1e4b6c175a3 --- /dev/null +++ b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/output.txt @@ -0,0 +1,2 @@ +null +null diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/test.swf b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/test.swf new file mode 100644 index 000000000000..025aa8e2334d Binary files /dev/null and b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/test.swf differ diff --git a/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/test.toml b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/test.toml new file mode 100644 index 000000000000..dbee897f5863 --- /dev/null +++ b/tests/tests/swfs/avm2/vector_boolean_default_constructor_v13/test.toml @@ -0,0 +1 @@ +num_frames = 1