From a1dff9132443300ca17b0f2023c50b18a2a3c8d6 Mon Sep 17 00:00:00 2001 From: Lord-McSweeney Date: Sun, 7 Dec 2025 15:58:12 -0800 Subject: [PATCH 1/2] avm2: `Array.toLocaleString` should skip null and undefined elements This progresses `from_avmplus/ecma3/Array/toLocaleString` --- core/src/avm2/globals/Array.as | 4 +--- .../ecma3/Array/toLocaleString/output.ruffle.txt | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/core/src/avm2/globals/Array.as b/core/src/avm2/globals/Array.as index 06496c033e14..4269eb7fa612 100644 --- a/core/src/avm2/globals/Array.as +++ b/core/src/avm2/globals/Array.as @@ -103,9 +103,7 @@ package { var arrayLength:uint = a.length; for (var i:uint = 0; i < arrayLength; i ++) { - if (a[i] === void 0 || a[i] === null) { - result += a[i]; - } else { + if (a[i] !== void 0 && a[i] !== null) { result += a[i].toLocaleString(); } diff --git a/tests/tests/swfs/from_avmplus/ecma3/Array/toLocaleString/output.ruffle.txt b/tests/tests/swfs/from_avmplus/ecma3/Array/toLocaleString/output.ruffle.txt index b700ec37848a..e560c760e81a 100644 --- a/tests/tests/swfs/from_avmplus/ecma3/Array/toLocaleString/output.ruffle.txt +++ b/tests/tests/swfs/from_avmplus/ecma3/Array/toLocaleString/output.ruffle.txt @@ -1,14 +1,14 @@ Array.prototype.toLocaleString.length PASSED! (new Array()).toLocaleString() PASSED! -(new Array(2)).toLocaleString() FAILED! expected: , got: undefined,undefined +(new Array(2)).toLocaleString() PASSED! (new Array(0,1)).toLocaleString() PASSED! (new Array( Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY)).toLocaleString() PASSED! (new Array(Boolean(1), Boolean(0))).toLocaleString() PASSED! -(new Array(void 0,null)).toLocaleString() FAILED! expected: , got: undefined,null +(new Array(void 0,null)).toLocaleString() PASSED! MYARR.toLocaleString() PASSED! MYARR2.toLocaleString() PASSED! MYARRARR.toLocaleString() PASSED! -MYUNDEFARR.toLocaleString() FAILED! expected: got: undefined -MYNULLARR.toLocaleString() FAILED! expected: got: null -MYNULLARR2.toLocaleString() FAILED! expected: got: null +MYUNDEFARR.toLocaleString() PASSED! +MYNULLARR.toLocaleString() PASSED! +MYNULLARR2.toLocaleString() PASSED! MyAllArray.toLocaleString() FAILED! expected: [object String],1,2,3,100000,[object Boolean],1.79769313486231e+308 got: [object String],1,2,3,100000,[object Boolean],1.7976931348623149e+308 From 381f1ca247037912c520fb7cb0af21a40c799bd9 Mon Sep 17 00:00:00 2001 From: Lord-McSweeney Date: Sun, 7 Dec 2025 17:34:46 -0800 Subject: [PATCH 2/2] avm2: Minor fixes to Array's prototype `Array.prototype` should be an instance of `Array`, like how `Date.prototype` is an instance of `Date`. Also, prototype lookups on prototypes that are an instance of `Array` (such as `Array`'s prototype) should check the elements of the array too, not just dynamic properties on the prototype. --- core/src/avm2/globals/Array.as | 4 + core/src/avm2/globals/array.rs | 15 +++ core/src/avm2/object/array_object.rs | 21 ++++ core/src/avm2/object/script_object.rs | 23 +++- .../ecma3/Array/e15_4_4/output.ruffle.txt | 4 - .../ecma3/Array/e15_4_4/test.toml | 1 - .../regress/bug_687838/output.ruffle.txt | 111 ------------------ .../from_avmplus/regress/bug_687838/test.toml | 1 - 8 files changed, 59 insertions(+), 121 deletions(-) delete mode 100644 tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/output.ruffle.txt delete mode 100644 tests/tests/swfs/from_avmplus/regress/bug_687838/output.ruffle.txt diff --git a/core/src/avm2/globals/Array.as b/core/src/avm2/globals/Array.as index 4269eb7fa612..86aa88506ec1 100644 --- a/core/src/avm2/globals/Array.as +++ b/core/src/avm2/globals/Array.as @@ -8,10 +8,14 @@ package { public static const RETURNINDEXEDARRAY:uint = 8; public static const NUMERIC:uint = 16; + private static native function initCustomPrototype(); + // FIXME avmplus allows for calling some of these prototype functions on any // Array-like object (for example, `Array.prototype.sort.call(myVector)` works), // but currently we only support calling them on real Arrays { + initCustomPrototype(); + prototype.concat = function(...rest):Array { var a:Array = this; return a.AS3::concat.apply(a, rest); diff --git a/core/src/avm2/globals/array.rs b/core/src/avm2/globals/array.rs index 31e8b3a1995a..9ad3f9855d0d 100644 --- a/core/src/avm2/globals/array.rs +++ b/core/src/avm2/globals/array.rs @@ -16,6 +16,21 @@ use std::mem::swap; pub use crate::avm2::object::array_allocator; +pub fn init_custom_prototype<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Value<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let this = this.as_object().unwrap(); + let this = this.as_class_object().unwrap(); + + let prototype_array_object = ArrayObject::for_prototype(activation.context, this); + + this.link_prototype(activation.context, prototype_array_object); + + Ok(Value::Undefined) +} + /// Implements `Array`'s instance initializer. pub fn array_initializer<'gc>( activation: &mut Activation<'_, 'gc>, diff --git a/core/src/avm2/object/array_object.rs b/core/src/avm2/object/array_object.rs index 5e554190cf4e..ead02a7021c9 100644 --- a/core/src/avm2/object/array_object.rs +++ b/core/src/avm2/object/array_object.rs @@ -85,6 +85,27 @@ impl<'gc> ArrayObject<'gc> { )) } + pub fn for_prototype( + context: &mut UpdateContext<'gc>, + array_class: ClassObject<'gc>, + ) -> Object<'gc> { + let object_class = context.avm2.classes().object; + let base = ScriptObjectData::custom_new( + array_class.inner_class_definition(), + Some(object_class.prototype()), + array_class.instance_vtable(), + ); + + ArrayObject(Gc::new( + context.gc(), + ArrayObjectData { + base, + array: RefLock::new(ArrayStorage::new(0)), + }, + )) + .into() + } + pub fn as_array_index(local_name: &WStr) -> Option { // TODO: this should use a custom implementation instead of `parse()`, // see `script_object::maybe_int_property` diff --git a/core/src/avm2/object/script_object.rs b/core/src/avm2/object/script_object.rs index 42ada1c66cb0..a56d2c644b3d 100644 --- a/core/src/avm2/object/script_object.rs +++ b/core/src/avm2/object/script_object.rs @@ -4,7 +4,7 @@ use crate::avm2::activation::Activation; use crate::avm2::class::Class; use crate::avm2::dynamic_map::{DynamicKey, DynamicMap}; use crate::avm2::error; -use crate::avm2::object::{ClassObject, FunctionObject, Object, TObject}; +use crate::avm2::object::{ArrayObject, ClassObject, FunctionObject, Object, TObject}; use crate::avm2::value::Value; use crate::avm2::vtable::VTable; use crate::avm2::{Error, Multiname, QName}; @@ -483,14 +483,29 @@ pub fn get_dynamic_property<'gc>( // follow the prototype chain let mut proto = prototype; - while let Some(obj) = proto { - let obj = obj.base(); + while let Some(this_proto) = proto { + // First search dynamic properties + let obj = this_proto.base(); let values = obj.values(); let value = values.as_hashmap().get(&key); if let Some(value) = value { return Ok(Some(value.value)); } - proto = obj.proto(); + + // Special-case: `Array.prototype` is an instance of `Array`, so to make + // sure that property lookups work correctly it, also check dynamic + // array elements (if this prototype is an `Array`). + // + // This special-case doesn't apply to anything else (e.g. Vector elements). + if let Some(array) = this_proto.as_array_object() { + if let Some(index) = ArrayObject::as_array_index(&local_name) { + if let Some(value) = array.get_index_property(index) { + return Ok(Some(value)); + } + } + } + + proto = this_proto.proto(); } Ok(None) diff --git a/tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/output.ruffle.txt b/tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/output.ruffle.txt deleted file mode 100644 index 0fd758c6b5b3..000000000000 --- a/tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/output.ruffle.txt +++ /dev/null @@ -1,4 +0,0 @@ -Array.prototype.length FAILED! expected: 0 got: undefined -Array.length PASSED! -typeof Array.prototype PASSED! -Array.prototype.toString = Object.prototype.toString; Array.prototype.toString() FAILED! expected: [object Array] got: [object Object] diff --git a/tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/test.toml b/tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/test.toml index 29f3cef79022..cf6123969a1d 100644 --- a/tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/test.toml +++ b/tests/tests/swfs/from_avmplus/ecma3/Array/e15_4_4/test.toml @@ -1,2 +1 @@ num_ticks = 1 -known_failure = true diff --git a/tests/tests/swfs/from_avmplus/regress/bug_687838/output.ruffle.txt b/tests/tests/swfs/from_avmplus/regress/bug_687838/output.ruffle.txt deleted file mode 100644 index 8eef4d217f96..000000000000 --- a/tests/tests/swfs/from_avmplus/regress/bug_687838/output.ruffle.txt +++ /dev/null @@ -1,111 +0,0 @@ -Arr .proto start1 array named a PASSED! -Cls .proto start1 array trans named b PASSED! -Cls .proto start1 array immed named c PASSED! -Fcn .proto object named d PASSED! -Fcn .proto start1 array trans named d PASSED! -Fcn .proto object named e PASSED! -Fcn .proto start1 array immed named e PASSED! -Fcn .proto object named f PASSED! -Arr .proto start1 array ref 1 a PASSED! -Arr .proto start1 self sparse ref 1 a PASSED! -Cls .proto start1 array trans ref 1 b PASSED! -Cls .proto sparse array trans ref 1 b PASSED! -Cls .proto start1 array immed ref 1 c PASSED! -Cls .proto sparse array immed ref 1 c PASSED! -Fcn .proto object ref 1 d PASSED! -Fcn .proto start1 array trans ref 1 d PASSED! -Fcn .proto object ref 1 e PASSED! -Fcn .proto start1 array immed ref 1 e FAILED! expected: one Epost got: one A -Fcn .proto object ref 1 f PASSED! -Arr .proto start1 array ref 10 a PASSED! -Cls .proto start1 array trans ref 10 b PASSED! -Cls .proto sparse array trans ref 10 b PASSED! -Cls .proto start1 array immed ref 10 c PASSED! -Cls .proto sparse array immed ref 10 c PASSED! -Fcn .proto object ref 10 d PASSED! -Fcn .proto start1 array trans ref 10 d PASSED! -Fcn .proto object ref 10 e PASSED! -Fcn .proto start1 array immed ref 10 e FAILED! expected: ten Epost got: ten A -Fcn .proto object ref 10 f PASSED! -Arr .proto start1 array ref 1k a PASSED! -Cls .proto start1 array trans ref 1k b PASSED! -Cls .proto start1 array immed ref 1k c PASSED! -Cls .proto sparse array immed ref 1k c PASSED! -Fcn .proto object ref 1k d PASSED! -Fcn .proto object ref 1k e PASSED! -Fcn .proto start1 array trans ref 1k d PASSED! -Fcn .proto start1 array immed ref 1k e PASSED! -Fcn .proto object ref 1k f PASSED! -Arr .proto start0 array named a PASSED! -Cls .proto start0 array trans named b PASSED! -Cls .proto start0 array immed named c PASSED! -Fcn .proto object named d PASSED! -Fcn .proto start0 array trans named d PASSED! -Fcn .proto object named e PASSED! -Fcn .proto start0 array immed named e PASSED! -Fcn .proto object named f PASSED! -Arr .proto start0 array ref 1 a PASSED! -Cls .proto start0 array trans ref 1 b PASSED! -Cls .proto start0 array immed ref 1 c PASSED! -Fcn .proto object ref 1 d PASSED! -Fcn .proto object ref 1 e PASSED! -Fcn .proto start0 array trans ref 1 d PASSED! -Fcn .proto start0 array immed ref 1 e FAILED! expected: one Epost got: one A -Fcn .proto object ref 1 f PASSED! -Arr .proto start0 array ref 10 a PASSED! -Cls .proto start0 array trans ref 10 b PASSED! -Cls .proto start0 array immed ref 10 c PASSED! -Fcn .proto object ref 10 d PASSED! -Fcn .proto object ref 10 e PASSED! -Fcn .proto start0 array trans ref 10 d PASSED! -Fcn .proto start0 array immed ref 10 e FAILED! expected: ten Epost got: ten A -Fcn .proto object ref 10 f PASSED! -Arr .proto start0 array ref 1k a PASSED! -Cls .proto start0 array trans ref 1k b PASSED! -Cls .proto start0 array immed ref 1k c PASSED! -Fcn .proto object ref 1k d PASSED! -Fcn .proto object ref 1k e PASSED! -Fcn .proto start0 array trans ref 1k d PASSED! -Fcn .proto start0 array immed ref 1k e PASSED! -Fcn .proto object ref 1k f PASSED! -Arr .proto sparse array named a PASSED! -Cls .proto sparse array trans named b PASSED! -Cls .proto sparse array immed named c PASSED! -Fcn .proto object named d PASSED! -Fcn .proto sparse array trans named d PASSED! -Fcn .proto object named e PASSED! -Fcn .proto sparse array immed named e PASSED! -Fcn .proto object named f PASSED! -Arr .proto sparse array ref 1 a PASSED! -Arr .proto+self sparse ref 1 a PASSED! -Cls .proto sparse array trans ref 1 b PASSED! -Cls .proto+self sparse trans ref 1 b PASSED! -Cls .proto sparse array immed ref 1 c PASSED! -Cls .proto+self sparse immed ref 1 c PASSED! -Fcn .proto object ref 1 d PASSED! -Fcn .proto sparse array trans ref 1 d PASSED! -Fcn .proto object ref 1 e PASSED! -Fcn .proto sparse array immed ref 1 e FAILED! expected: one Epost got: one A -Fcn .proto object ref 1 f PASSED! -Arr .proto sparse array ref 10 a PASSED! -Arr .proto+self sparse ref 10 a PASSED! -Cls .proto sparse array trans ref 10 b PASSED! -Cls .proto+self sparse trans ref 10 b PASSED! -Cls .proto sparse array immed ref 10 c PASSED! -Cls .proto+self sparse immed ref 10 c PASSED! -Fcn .proto object ref 10 d PASSED! -Fcn .proto sparse array trans ref 10 d PASSED! -Fcn .proto object ref 10 e PASSED! -Fcn .proto sparse array immed ref 10 e FAILED! expected: ten Epost got: ten A -Fcn .proto object ref 10 f PASSED! -Arr .proto sparse array ref 1k a PASSED! -Arr .proto+self sparse ref 1k a PASSED! -Cls .proto sparse array trans ref 1k b PASSED! -Cls .proto+self sparse trans ref 1k b PASSED! -Cls .proto sparse array immed ref 1k c PASSED! -Cls .proto+self sparse immed ref 1k c PASSED! -Fcn .proto object ref 1k d PASSED! -Fcn .proto object ref 1k e PASSED! -Fcn .proto sparse array trans ref 1k d PASSED! -Fcn .proto sparse array immed ref 1k e FAILED! expected: thou Epost got: thou A -Fcn .proto object ref 1k f PASSED! diff --git a/tests/tests/swfs/from_avmplus/regress/bug_687838/test.toml b/tests/tests/swfs/from_avmplus/regress/bug_687838/test.toml index 29f3cef79022..cf6123969a1d 100644 --- a/tests/tests/swfs/from_avmplus/regress/bug_687838/test.toml +++ b/tests/tests/swfs/from_avmplus/regress/bug_687838/test.toml @@ -1,2 +1 @@ num_ticks = 1 -known_failure = true