From a2fdbaf03aa7b5fd40464dc1438cb6e49ed16b14 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Thu, 21 May 2026 02:44:36 -0700 Subject: [PATCH 1/6] avm2: Emit StrictArray for dense AS3 Arrays in AMF0 (close #16381) Signed-off-by: Onyeka Obi --- core/src/avm2/amf.rs | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/core/src/avm2/amf.rs b/core/src/avm2/amf.rs index 26a58d877ebe..4930e493bbf9 100644 --- a/core/src/avm2/amf.rs +++ b/core/src/avm2/amf.rs @@ -51,24 +51,34 @@ pub fn serialize_value<'gc>( recursive_serialize(activation, o, &mut values, None, amf_version, object_table) .unwrap(); - if amf_version == AMFVersion::AMF3 { - let mut dense = vec![]; - let mut sparse = vec![]; - // ActionScript `Array`s can have non-number properties, and these properties - // are confirmed and tested to also be serialized, so do not limit the values - // iterated over by the length of the internal array data. - for (i, elem) in values.into_iter().enumerate() { - if elem.name == i.to_string() { - dense.push(elem.value.clone()); - } else { - sparse.push(elem); - } + // ActionScript `Array`s can have non-number properties, and these properties + // are confirmed and tested to also be serialized, so do not limit the values + // iterated over by the length of the internal array data. + let mut dense = vec![]; + let mut sparse = vec![]; + for (i, elem) in values.into_iter().enumerate() { + if elem.name == i.to_string() { + dense.push(elem.value.clone()); + } else { + sparse.push(elem); } + } - Some(AmfValue::ECMAArray(ObjectId::INVALID, dense, sparse, len)) + if amf_version == AMFVersion::AMF0 && sparse.is_empty() { + // Dense-only AS3 `Array` in AMF0 must be serialized as + // `StrictArray` (marker `0x0A`) so that Flash decoders see + // an indexed array `["a", "b"]` rather than an associative + // object `{0: "a", 1: "b"}`. Before this fix the AMF0 path + // unconditionally emitted `ECMAArray` (marker `0x08`), + // which broke real-world Flash Remoting endpoints that + // round-trip AS3 arrays as call arguments (#16381). + Some(AmfValue::StrictArray(ObjectId::INVALID, dense)) } else { - // TODO: is this right? - Some(AmfValue::ECMAArray(ObjectId::INVALID, vec![], values, len)) + // Mixed dense + sparse arrays use ECMAArray for both AMF0 + // and AMF3. AMF3 has no `StrictArray` marker and uses + // ECMAArray with both dense and sparse parts for every + // AS3 `Array`. + Some(AmfValue::ECMAArray(ObjectId::INVALID, dense, sparse, len)) } } else if let Some(vec) = o.as_vector_storage() { let val_type = vec.value_type(); From 4b5d980f8eb7d35b145c7cfee315ba7aa95275ce Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Thu, 21 May 2026 07:25:08 -0700 Subject: [PATCH 2/6] avm2: Promote StrictArray only at NetConnection/LocalConnection call sites Signed-off-by: Onyeka Obi --- core/src/avm1/globals/shared_object.rs | 25 +++++ core/src/avm2/amf.rs | 95 +++++++++++++++---- .../globals/flash/net/local_connection.rs | 11 ++- .../avm2/globals/flash/net/net_connection.rs | 7 +- 4 files changed, 115 insertions(+), 23 deletions(-) diff --git a/core/src/avm1/globals/shared_object.rs b/core/src/avm1/globals/shared_object.rs index 183726f9ec91..3995b1482a92 100644 --- a/core/src/avm1/globals/shared_object.rs +++ b/core/src/avm1/globals/shared_object.rs @@ -198,6 +198,31 @@ pub fn deserialize_value<'gc>( Value::Undefined } } + AmfValue::StrictArray(_, values) => { + // Real Flash uses `StrictArray` (AMF0 marker `0x0A`) for dense AS3 + // `Array`s sent over `NetConnection.call` / `LocalConnection.send` + // (#16381), so the AVM1 receiver has to reconstruct an `Array` + // here rather than falling through to `Undefined`. + let array_constructor = activation.prototypes().array_constructor; + if let Ok(Value::Object(obj)) = + array_constructor.construct(activation, &[(values.len() as i32).into()]) + { + let v: Value<'gc> = obj.into(); + + if let Some(reference) = lso.as_reference(val) { + reference_cache.insert(reference, v); + } + + for (i, item) in values.iter().enumerate() { + let value = deserialize_value(activation, item, lso, reference_cache); + obj.set_element(activation, i as i32, value).unwrap(); + } + + v + } else { + Value::Undefined + } + } AmfValue::Object(_, elements, _) => { // Deserialize Object let obj = Object::new( diff --git a/core/src/avm2/amf.rs b/core/src/avm2/amf.rs index 4930e493bbf9..52425685d965 100644 --- a/core/src/avm2/amf.rs +++ b/core/src/avm2/amf.rs @@ -64,22 +64,12 @@ pub fn serialize_value<'gc>( } } - if amf_version == AMFVersion::AMF0 && sparse.is_empty() { - // Dense-only AS3 `Array` in AMF0 must be serialized as - // `StrictArray` (marker `0x0A`) so that Flash decoders see - // an indexed array `["a", "b"]` rather than an associative - // object `{0: "a", 1: "b"}`. Before this fix the AMF0 path - // unconditionally emitted `ECMAArray` (marker `0x08`), - // which broke real-world Flash Remoting endpoints that - // round-trip AS3 arrays as call arguments (#16381). - Some(AmfValue::StrictArray(ObjectId::INVALID, dense)) - } else { - // Mixed dense + sparse arrays use ECMAArray for both AMF0 - // and AMF3. AMF3 has no `StrictArray` marker and uses - // ECMAArray with both dense and sparse parts for every - // AS3 `Array`. - Some(AmfValue::ECMAArray(ObjectId::INVALID, dense, sparse, len)) - } + // AS3 `Array` is serialized as `ECMAArray` for both AMF0 and AMF3. + // Callers that need `StrictArray` semantics for dense AS3 `Array`s + // (e.g. `NetConnection.call` / `LocalConnection.send` argument + // encoding, see #16381) post-process the returned tree with + // `promote_dense_ecma_to_strict_array`. + Some(AmfValue::ECMAArray(ObjectId::INVALID, dense, sparse, len)) } else if let Some(vec) = o.as_vector_storage() { let val_type = vec.value_type(); if val_type == Some(activation.avm2().class_defs().int) { @@ -193,6 +183,79 @@ pub fn serialize_value<'gc>( } } +/// Recursively rewrite dense `ECMAArray` nodes in `value` as `StrictArray`. +/// +/// Real Flash encodes the argument tree for `NetConnection.call` and +/// `LocalConnection.send` with `StrictArray` (marker `0x0A`) for every dense +/// AS3 `Array`, while `ByteArray.writeObject` and `Socket.writeObject` keep +/// the `ECMAArray` (marker `0x08`) encoding for those same arrays. The default +/// `serialize_value` output uses `ECMAArray` to match the latter; the Flash +/// Remoting code paths call this helper to convert the tree into the form +/// real Flash sends on the wire (#16381). +/// +/// An `ECMAArray` is treated as dense and promoted when its sparse portion is +/// empty and its declared length matches the dense portion exactly. All other +/// nodes are walked so that nested dense arrays inside objects, vectors, +/// dictionaries and AMF3 wrappers are promoted too. +pub fn promote_dense_ecma_to_strict_array(value: AmfValue) -> AmfValue { + match value { + AmfValue::ECMAArray(_, dense, sparse, len) + if sparse.is_empty() && dense.len() as u32 == len => + { + let promoted = dense.into_iter().map(promote_rc).collect(); + AmfValue::StrictArray(ObjectId::INVALID, promoted) + } + AmfValue::ECMAArray(id, dense, sparse, len) => { + let promoted_dense = dense.into_iter().map(promote_rc).collect(); + let promoted_sparse = sparse + .into_iter() + .map(|e| Element::new(e.name, promote_rc(e.value))) + .collect(); + AmfValue::ECMAArray(id, promoted_dense, promoted_sparse, len) + } + AmfValue::StrictArray(id, values) => { + let promoted = values.into_iter().map(promote_rc).collect(); + AmfValue::StrictArray(id, promoted) + } + AmfValue::Object(id, elements, class) => { + let promoted = elements + .into_iter() + .map(|e| Element::new(e.name, promote_rc(e.value))) + .collect(); + AmfValue::Object(id, promoted, class) + } + AmfValue::AMF3(inner) => AmfValue::AMF3(promote_rc(inner)), + AmfValue::VectorObject(id, values, name, fixed) => { + let promoted = values.into_iter().map(promote_rc).collect(); + AmfValue::VectorObject(id, promoted, name, fixed) + } + AmfValue::Dictionary(id, body, weak_keys) => { + let promoted = body + .into_iter() + .map(|(k, v)| (promote_rc(k), promote_rc(v))) + .collect(); + AmfValue::Dictionary(id, promoted, weak_keys) + } + AmfValue::Custom(custom_elements, regular_elements, class) => { + let promoted_custom = custom_elements + .into_iter() + .map(|e| Element::new(e.name, promote_rc(e.value))) + .collect(); + let promoted_regular = regular_elements + .into_iter() + .map(|e| Element::new(e.name, promote_rc(e.value))) + .collect(); + AmfValue::Custom(promoted_custom, promoted_regular, class) + } + leaf => leaf, + } +} + +fn promote_rc(rc: Rc) -> Rc { + let owned = Rc::try_unwrap(rc).unwrap_or_else(|shared| (*shared).clone()); + Rc::new(promote_dense_ecma_to_strict_array(owned)) +} + fn alias_to_class<'gc>( activation: &mut Activation<'_, 'gc>, alias: AvmString<'gc>, diff --git a/core/src/avm2/globals/flash/net/local_connection.rs b/core/src/avm2/globals/flash/net/local_connection.rs index 704ff76bcc68..7a51f00be182 100644 --- a/core/src/avm2/globals/flash/net/local_connection.rs +++ b/core/src/avm2/globals/flash/net/local_connection.rs @@ -1,4 +1,4 @@ -use crate::avm2::amf::serialize_value; +use crate::avm2::amf::{promote_dense_ecma_to_strict_array, serialize_value}; use crate::avm2::error::{ Error2004Type, make_error_2004, make_error_2082, make_error_2083, make_error_2085, }; @@ -51,10 +51,11 @@ pub fn send<'gc>( let mut amf_arguments = Vec::with_capacity(args.len() - 2); for arg in &args[2..] { - amf_arguments.push( - serialize_value(activation, *arg, AMFVersion::AMF0, &mut Default::default()) - .unwrap_or(AmfValue::Undefined), - ); + let value = serialize_value(activation, *arg, AMFVersion::AMF0, &mut Default::default()) + .unwrap_or(AmfValue::Undefined); + // Real Flash sends dense AS3 `Array` arguments as `StrictArray` over + // the LocalConnection wire (#16381). + amf_arguments.push(promote_dense_ecma_to_strict_array(value)); } if let Some(local_connection) = this.as_local_connection_object() { diff --git a/core/src/avm2/globals/flash/net/net_connection.rs b/core/src/avm2/globals/flash/net/net_connection.rs index 665aabac8cf6..598b7ddc0f3d 100644 --- a/core/src/avm2/globals/flash/net/net_connection.rs +++ b/core/src/avm2/globals/flash/net/net_connection.rs @@ -1,4 +1,4 @@ -use crate::avm2::amf::serialize_value; +use crate::avm2::amf::{promote_dense_ecma_to_strict_array, serialize_value}; use crate::avm2::error::make_error_2126; pub use crate::avm2::object::net_connection_allocator; use crate::avm2::parameters::ParametersExt; @@ -284,7 +284,9 @@ pub fn call<'gc>( for arg in &args[2..] { if let Some(value) = serialize_value(activation, *arg, AMFVersion::AMF0, &mut object_table) { - arguments.push(Rc::new(value)); + // Real Flash sends dense AS3 `Array` arguments as `StrictArray` on + // the NetConnection.call wire (#16381). + arguments.push(Rc::new(promote_dense_ecma_to_strict_array(value))); } } @@ -341,6 +343,7 @@ pub fn add_header<'gc>( AMFVersion::AMF0, &mut Default::default(), ) + .map(promote_dense_ecma_to_strict_array) .unwrap_or(AMFValue::Null); if let Some(handle) = connection.handle() { From 6aa45b9109609836359486bfa9c8e5352b0b199a Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Thu, 21 May 2026 11:29:21 -0700 Subject: [PATCH 3/6] =?UTF-8?q?avm1:=20Mirror=20AS3=20Array=20=E2=86=92=20?= =?UTF-8?q?StrictArray=20promotion=20for=20AVM1=20NetConnection/LocalConne?= =?UTF-8?q?ction=20(#16381)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onyeka Obi --- core/src/avm1/globals/local_connection.rs | 7 +++- core/src/avm1/globals/netconnection.rs | 8 +++- core/src/avm1/globals/shared_object.rs | 49 +++++++++++++++++++++-- core/src/avm2.rs | 2 +- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/core/src/avm1/globals/local_connection.rs b/core/src/avm1/globals/local_connection.rs index f67edaa8a55d..e487cc613623 100644 --- a/core/src/avm1/globals/local_connection.rs +++ b/core/src/avm1/globals/local_connection.rs @@ -6,6 +6,7 @@ use crate::avm1::globals::shared_object::{deserialize_value, serialize}; use crate::avm1::property_decl::{DeclContext, StaticDeclarations, SystemClass}; use crate::avm1::{ActivationIdentifier, ExecutionReason, NativeObject, Object, Value}; use crate::avm1_stub; +use crate::avm2::amf::promote_dense_ecma_to_strict_array; use crate::context::UpdateContext; use crate::display_object::TDisplayObject; use crate::local_connection::{LocalConnectionHandle, LocalConnections}; @@ -226,7 +227,11 @@ pub fn send<'gc>( let mut amf_arguments = Vec::with_capacity(args.len() - 2); for arg in &args[2..] { - amf_arguments.push(serialize(activation, *arg)); + // Real Flash sends dense AS1/AS2 `Array` arguments as `StrictArray` + // over the LocalConnection wire (#16381). Mirrors the same promotion + // done in `NetConnection.call`. + let value = serialize(activation, *arg); + amf_arguments.push(promote_dense_ecma_to_strict_array(value)); } activation.context.local_connections.send( diff --git a/core/src/avm1/globals/netconnection.rs b/core/src/avm1/globals/netconnection.rs index 2919cbe52bb3..2c4fc08f2b19 100644 --- a/core/src/avm1/globals/netconnection.rs +++ b/core/src/avm1/globals/netconnection.rs @@ -4,6 +4,7 @@ use crate::avm1::{ Activation, ActivationIdentifier, Error, ExecutionReason, NativeObject, Object, Value, }; use crate::avm1_stub; +use crate::avm2::amf::promote_dense_ecma_to_strict_array; use crate::context::UpdateContext; use crate::net_connection::{NetConnectionHandle, NetConnections, ResponderCallback}; use crate::string::AvmString; @@ -274,7 +275,12 @@ fn call<'gc>( if args.len() > 2 { for arg in &args[2..] { - arguments.push(Rc::new(serialize(activation, *arg))); + // Real Flash sends dense AS1/AS2 `Array` arguments as `StrictArray` + // on the NetConnection.call wire (#16381). Nested dense arrays + // produced by `recursive_serialize`'s `ECMAArray` path are promoted + // here as well. + let value = serialize(activation, *arg); + arguments.push(Rc::new(promote_dense_ecma_to_strict_array(value))); } } diff --git a/core/src/avm1/globals/shared_object.rs b/core/src/avm1/globals/shared_object.rs index 3995b1482a92..400c21dc029b 100644 --- a/core/src/avm1/globals/shared_object.rs +++ b/core/src/avm1/globals/shared_object.rs @@ -5,7 +5,7 @@ use crate::display_object::TDisplayObject; use crate::string::AvmString; use flash_lso::amf0::read::AMF0Decoder; use flash_lso::amf0::writer::{Amf0Writer, CacheKey, ObjWriter}; -use flash_lso::types::{Lso, ObjectId, Reference, Value as AmfValue}; +use flash_lso::types::{Element, Lso, ObjectId, Reference, Value as AmfValue}; use gc_arena::{Collect, Gc}; use ruffle_macros::istr; use std::borrow::Cow; @@ -86,13 +86,56 @@ pub fn serialize<'gc>(activation: &mut Activation<'_, 'gc>, value: Value<'gc>) - Value::Number(number) => AmfValue::Number(number), Value::String(string) => AmfValue::String(string.to_string()), Value::Object(object) => { - let lso = new_lso(activation, "root", object); - AmfValue::Object(ObjectId::INVALID, lso.into_iter().collect(), None) + if let NativeObject::Array(_) = object.native() { + // AS1/AS2 `Array` values are serialized using the AMF0 array + // markers so that Flash Remoting / LocalConnection endpoints + // round-trip them as arrays rather than as anonymous objects + // with `"0"`/`"1"`/... keys. Callers that wire the result + // onto a path Flash sends as `StrictArray` (NetConnection.call + // / LocalConnection.send argument trees, #16381) post-process + // with `crate::avm2::amf::promote_dense_ecma_to_strict_array`. + serialize_array(activation, object) + } else { + let lso = new_lso(activation, "root", object); + AmfValue::Object(ObjectId::INVALID, lso.into_iter().collect(), None) + } } Value::MovieClip(_) => AmfValue::Undefined, } } +/// Serialize an AS1/AS2 `Array` into the AMF0 array form. Dense indices fill +/// the first slot of the returned `ECMAArray`; non-numeric or out-of-range +/// keys go into the associative part. The wrapping `ECMAArray` matches what +/// flash-lso's `ObjWriter::array`/`ArrayWriter::commit` produces for nested +/// arrays inside `recursive_serialize`, so behavior is uniform between +/// top-level and nested `Array` serialization. +fn serialize_array<'gc>(activation: &mut Activation<'_, 'gc>, array: Object<'gc>) -> AmfValue { + let length = array.length(activation).unwrap_or(0).max(0) as u32; + let mut dense: Vec> = (0..length) + .map(|_| std::rc::Rc::new(AmfValue::Undefined)) + .collect(); + let mut associative: Vec = Vec::new(); + // Reversed to match Flash Player enumeration order, mirroring + // `recursive_serialize`. + for key in array.get_keys(activation, false).into_iter().rev() { + let key_str = key.to_utf8_lossy(); + let value = array + .get(key, activation) + .map(|v| serialize(activation, v)) + .unwrap_or(AmfValue::Undefined); + match key_str.parse::() { + Ok(idx) if idx < length => { + dense[idx as usize] = std::rc::Rc::new(value); + } + _ => { + associative.push(Element::new(key_str.to_string(), std::rc::Rc::new(value))); + } + } + } + AmfValue::ECMAArray(ObjectId::INVALID, dense, associative, length) +} + /// Serialize an Object and any children to a JSON object fn recursive_serialize<'gc>( activation: &mut Activation<'_, 'gc>, diff --git a/core/src/avm2.rs b/core/src/avm2.rs index 40083888b156..43b8fe77be58 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -41,7 +41,7 @@ macro_rules! avm_debug { } pub mod activation; -mod amf; +pub(crate) mod amf; pub mod api_version; mod array; pub mod bytearray; From ccd43eab22e91a59840384e94b6f088638c43704 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sat, 23 May 2026 13:13:30 -0700 Subject: [PATCH 4/6] tests: Add avm1/netconnection_serialize_arrays (#16381) Verifies NetConnection.call serializes genuine arrays as StrictArray, fake arrays (Object with numeric keys + length) as Object, and mixed arrays as insertion-ordered ECMAArray, against a real Flash Player byte capture. Co-authored-by: Daniel Jacobs Signed-off-by: Onyeka Obi --- .../netconnection_serialize_arrays/README.md | 12 +++++++ .../netconnection_serialize_arrays/output.txt | 6 ++++ .../netconnection_serialize_arrays/server.py | 29 ++++++++++++++++ .../netconnection_serialize_arrays/test.as | 32 ++++++++++++++++++ .../netconnection_serialize_arrays/test.swf | Bin 0 -> 701 bytes .../netconnection_serialize_arrays/test.toml | 8 +++++ 6 files changed, 87 insertions(+) create mode 100644 tests/tests/swfs/avm1/netconnection_serialize_arrays/README.md create mode 100644 tests/tests/swfs/avm1/netconnection_serialize_arrays/output.txt create mode 100644 tests/tests/swfs/avm1/netconnection_serialize_arrays/server.py create mode 100644 tests/tests/swfs/avm1/netconnection_serialize_arrays/test.as create mode 100644 tests/tests/swfs/avm1/netconnection_serialize_arrays/test.swf create mode 100644 tests/tests/swfs/avm1/netconnection_serialize_arrays/test.toml diff --git a/tests/tests/swfs/avm1/netconnection_serialize_arrays/README.md b/tests/tests/swfs/avm1/netconnection_serialize_arrays/README.md new file mode 100644 index 000000000000..e355bad2e0de --- /dev/null +++ b/tests/tests/swfs/avm1/netconnection_serialize_arrays/README.md @@ -0,0 +1,12 @@ + # Running the test + + +To verify the actual data sent over the network, run 'server.py' from this directory. Then, run 'test.swf' in either Flash Player or the Ruffle Desktop player. + +When running under flash player, you'll need to allow the SWF to make network connections. On Linux, this can be done by creating the file `/etc/adobe/FlashPlayerTrust/test.cfg` with the following contents: + +``` +/ancestor/of/swf/path +``` + +where `ancestor/of/swf/path` is any path that's an ancestor of the path of `test.swf` (e.g. `/home/username/`) diff --git a/tests/tests/swfs/avm1/netconnection_serialize_arrays/output.txt b/tests/tests/swfs/avm1/netconnection_serialize_arrays/output.txt new file mode 100644 index 000000000000..3a2cbb3a9732 --- /dev/null +++ b/tests/tests/swfs/avm1/netconnection_serialize_arrays/output.txt @@ -0,0 +1,6 @@ +--- Testing NetConnection Array Serialization --- +Navigator::fetch: + URL: http://localhost:8000/ + Method: POST + Mime-Type: application/x-amf + Body: [00, 00, 00, 00, 00, 01, 00, 0B, 74, 65, 73, 74, 2E, 61, 72, 72, 61, 79, 73, 00, 02, 2F, 31, 00, 00, 00, B3, 0A, 00, 00, 00, 03, 0A, 00, 00, 00, 02, 02, 00, 06, 72, 65, 61, 6C, 5F, 30, 02, 00, 06, 72, 65, 61, 6C, 5F, 31, 03, 00, 01, 30, 02, 00, 06, 66, 61, 6B, 65, 5F, 30, 00, 01, 31, 02, 00, 06, 66, 61, 6B, 65, 5F, 31, 00, 06, 6C, 65, 6E, 67, 74, 68, 00, 40, 00, 00, 00, 00, 00, 00, 00, 00, 00, 09, 08, 00, 00, 00, 02, 00, 06, 61, 5F, 70, 72, 6F, 70, 02, 00, 07, 76, 61, 6C, 75, 65, 5F, 61, 00, 01, 30, 02, 00, 07, 6D, 69, 78, 65, 64, 5F, 30, 00, 06, 6D, 5F, 70, 72, 6F, 70, 02, 00, 07, 76, 61, 6C, 75, 65, 5F, 6D, 00, 01, 31, 02, 00, 07, 6D, 69, 78, 65, 64, 5F, 31, 00, 06, 7A, 5F, 70, 72, 6F, 70, 02, 00, 07, 76, 61, 6C, 75, 65, 5F, 7A, 00, 06, 62, 5F, 70, 72, 6F, 70, 02, 00, 07, 76, 61, 6C, 75, 65, 5F, 62, 00, 00, 09] diff --git a/tests/tests/swfs/avm1/netconnection_serialize_arrays/server.py b/tests/tests/swfs/avm1/netconnection_serialize_arrays/server.py new file mode 100644 index 000000000000..3344dd8424f7 --- /dev/null +++ b/tests/tests/swfs/avm1/netconnection_serialize_arrays/server.py @@ -0,0 +1,29 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler + +class MyHandler(BaseHTTPRequestHandler): + def do_POST(self): + print("") + print("Navigator::fetch:") + print(f" URL: http://localhost:8000{self.path}") + print(" Method: POST") + print(f" Mime-Type: {self.headers.get('Content-Type')}") + + request = self.rfile.read(int(self.headers['Content-Length'])) + # Format as uppercase hex bytes + request_hex = ", ".join([f"{byte:02X}" for byte in request]) + + print(f" Body: [{request_hex}]") + print("") + + self.send_response(200) + self.end_headers() + self.wfile.write(b"") + +def run(server_class=HTTPServer, handler_class=MyHandler): + server_address = ('', 8000) + httpd = server_class(server_address, handler_class) + print("Running server on port 8000...") + httpd.serve_forever() + +if __name__ == '__main__': + run() diff --git a/tests/tests/swfs/avm1/netconnection_serialize_arrays/test.as b/tests/tests/swfs/avm1/netconnection_serialize_arrays/test.as new file mode 100644 index 000000000000..c8ba0665c701 --- /dev/null +++ b/tests/tests/swfs/avm1/netconnection_serialize_arrays/test.as @@ -0,0 +1,32 @@ +var nc = new NetConnection(); +nc.connect("http://localhost:8000/"); + +// Callback function +var responder = new Object(); +responder.onResult = function(result) { + trace("Received result"); +}; + +// 1. Genuine Array +var realArray = new Array(); +realArray[0] = "real_0"; +realArray[1] = "real_1"; + +// 2. Fake Array +var fakeArray = new Object(); +fakeArray[0] = "fake_0"; +fakeArray[1] = "fake_1"; +fakeArray.length = 2; + +// 3. Mixed Array (Genuine Array with a custom string property) +var mixedArray = new Array(); +mixedArray["a_prop"] = "value_a"; +mixedArray[0] = "mixed_0"; +mixedArray["m_prop"] = "value_m"; +mixedArray[1] = "mixed_1"; +mixedArray["z_prop"] = "value_z"; +mixedArray["b_prop"] = "value_b"; + +trace("--- Testing NetConnection Array Serialization ---"); +// Pass all three arrays to the server +nc.call("test.arrays", responder, realArray, fakeArray, mixedArray); diff --git a/tests/tests/swfs/avm1/netconnection_serialize_arrays/test.swf b/tests/tests/swfs/avm1/netconnection_serialize_arrays/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..bb30705eb8a5873a80c8e0fabc71cc6d649783e0 GIT binary patch literal 701 zcmY+BOHaZ;6opTD*iljNi4Qbgu|ptkj4@H8aVZIyxNL?pLbbG{9Srgd+?%j*;~(+2 z;hh-?(8aXpoUixx-ab6H*l!9LA?pJ9AK<{k{TArDOHHTK>+~0;pXb&oLJyBZ-bG<3 zJn2UvdQuK9tJNU#c+iUyd3jN*)vE9efmkGiDC~$BjrJ?Of+%c?WEe;^g(v)X(Qyb- zIu<;*jbr`+b1LoDwC&dM!rz4P)f4&pR!1PhuI!=jk3`4h+#STx0Pj2)3YVLN`2M2Z z|F`QHFWO_Y7iAljO2v5+iS)y+voyA&dpRu;`#kW+T&=<(8j|QUuEt0}8woI31%s_t z$*h|zrBb$~6iSm6Sh`tMUd9kvBFN0>XyrQxSZaf6Ju6U&U2Z6x8GO~?tTeIzTdXw{ zWenYAUGur6^0_&`p?PCyesgZHyiI={x Date: Sat, 23 May 2026 13:13:43 -0700 Subject: [PATCH 5/6] avm1: Preserve enumeration order when serializing Arrays to AMF0 serialize_array bucketed numeric keys into the ECMAArray dense slot and the rest into the associative slot, reordering mixed arrays (custom string properties interleaved with numeric indices) on the wire. Real Flash emits every enumerable property in insertion order, so mirror AVM2's serialize_value heuristic: keys that line up with the enumeration index form the dense prefix, and the first key that breaks the run sends the remainder to the associative slot. Signed-off-by: Onyeka Obi --- core/src/avm1/globals/shared_object.rs | 46 +++++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/core/src/avm1/globals/shared_object.rs b/core/src/avm1/globals/shared_object.rs index 400c21dc029b..66462084b007 100644 --- a/core/src/avm1/globals/shared_object.rs +++ b/core/src/avm1/globals/shared_object.rs @@ -10,6 +10,7 @@ use gc_arena::{Collect, Gc}; use ruffle_macros::istr; use std::borrow::Cow; use std::collections::BTreeMap; +use std::rc::Rc; #[derive(Default, Clone, Collect)] #[collect(require_static)] @@ -104,33 +105,38 @@ pub fn serialize<'gc>(activation: &mut Activation<'_, 'gc>, value: Value<'gc>) - } } -/// Serialize an AS1/AS2 `Array` into the AMF0 array form. Dense indices fill -/// the first slot of the returned `ECMAArray`; non-numeric or out-of-range -/// keys go into the associative part. The wrapping `ECMAArray` matches what -/// flash-lso's `ObjWriter::array`/`ArrayWriter::commit` produces for nested -/// arrays inside `recursive_serialize`, so behavior is uniform between -/// top-level and nested `Array` serialization. +/// Serialize an AS1/AS2 `Array` into the AMF0 array form, matching real Flash's +/// wire encoding for `NetConnection.call` / `LocalConnection.send` arguments +/// (#16381). Keys that line up with the enumeration index (i.e. the array +/// starts with a dense `0..n` prefix) fill the `ECMAArray`'s dense slot; +/// every later key falls into the associative slot. This mirrors AVM2's +/// `serialize_value` Array branch and, combined with `promote_dense_ecma_to_strict_array` +/// at NetConnection/LocalConnection call sites, yields `StrictArray` for pure +/// dense arrays and an insertion-ordered `ECMAArray` for arrays with custom +/// properties. fn serialize_array<'gc>(activation: &mut Activation<'_, 'gc>, array: Object<'gc>) -> AmfValue { let length = array.length(activation).unwrap_or(0).max(0) as u32; - let mut dense: Vec> = (0..length) - .map(|_| std::rc::Rc::new(AmfValue::Undefined)) - .collect(); + let mut dense: Vec> = Vec::new(); let mut associative: Vec = Vec::new(); - // Reversed to match Flash Player enumeration order, mirroring - // `recursive_serialize`. - for key in array.get_keys(activation, false).into_iter().rev() { - let key_str = key.to_utf8_lossy(); + // `PropertyMap::iter` yields most-recently-added first, so `.rev()` + // restores Flash's enumeration (insertion) order. Preserving this order + // is what lets the associative slot serialize as the exact byte sequence + // real Flash sends for mixed arrays. + for (i, key) in array + .get_keys(activation, false) + .into_iter() + .rev() + .enumerate() + { + let name = key.to_utf8_lossy().to_string(); let value = array .get(key, activation) .map(|v| serialize(activation, v)) .unwrap_or(AmfValue::Undefined); - match key_str.parse::() { - Ok(idx) if idx < length => { - dense[idx as usize] = std::rc::Rc::new(value); - } - _ => { - associative.push(Element::new(key_str.to_string(), std::rc::Rc::new(value))); - } + if name == i.to_string() { + dense.push(Rc::new(value)); + } else { + associative.push(Element::new(name, Rc::new(value))); } } AmfValue::ECMAArray(ObjectId::INVALID, dense, associative, length) From 66afc9c8067d8235555fe9801449dbc1fe0fd684 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Wed, 27 May 2026 17:21:00 -0700 Subject: [PATCH 6/6] tests: add avm2/netconnection_serialize_arrays Signed-off-by: Onyeka Obi --- .../netconnection_serialize_arrays/README.md | 19 ++++++++++ .../netconnection_serialize_arrays/Test.as | 35 ++++++++++++++++++ .../netconnection_serialize_arrays/output.txt | 6 +++ .../netconnection_serialize_arrays/server.py | 29 +++++++++++++++ .../netconnection_serialize_arrays/test.swf | Bin 0 -> 1168 bytes .../netconnection_serialize_arrays/test.toml | 5 +++ 6 files changed, 94 insertions(+) create mode 100644 tests/tests/swfs/avm2/netconnection_serialize_arrays/README.md create mode 100644 tests/tests/swfs/avm2/netconnection_serialize_arrays/Test.as create mode 100644 tests/tests/swfs/avm2/netconnection_serialize_arrays/output.txt create mode 100644 tests/tests/swfs/avm2/netconnection_serialize_arrays/server.py create mode 100644 tests/tests/swfs/avm2/netconnection_serialize_arrays/test.swf create mode 100644 tests/tests/swfs/avm2/netconnection_serialize_arrays/test.toml diff --git a/tests/tests/swfs/avm2/netconnection_serialize_arrays/README.md b/tests/tests/swfs/avm2/netconnection_serialize_arrays/README.md new file mode 100644 index 000000000000..38eade9b382e --- /dev/null +++ b/tests/tests/swfs/avm2/netconnection_serialize_arrays/README.md @@ -0,0 +1,19 @@ +# Running the test + +To verify the actual data sent over the network, run 'server.py' from this directory. Then, run 'test.swf' in either Flash Player or the Ruffle Desktop player. + +When running under flash player, you'll need to allow the SWF to make network connections. On Linux, this can be done by creating the file `/etc/adobe/FlashPlayerTrust/test.cfg` with the following contents: + +``` +/ancestor/of/swf/path +``` + +where `ancestor/of/swf/path` is any path that's an ancestor of the path of `test.swf` (e.g. `/home/username/`) + +# Recompiling test.swf + +`Test.as` is an AS3 (AVM2) document class. Recompile with the Apache Flex SDK (see CONTRIBUTING.md#apache-flex-sdk): + +``` +mxmlc -o test.swf -debug Test.as +``` diff --git a/tests/tests/swfs/avm2/netconnection_serialize_arrays/Test.as b/tests/tests/swfs/avm2/netconnection_serialize_arrays/Test.as new file mode 100644 index 000000000000..0ae341d2377e --- /dev/null +++ b/tests/tests/swfs/avm2/netconnection_serialize_arrays/Test.as @@ -0,0 +1,35 @@ +package { + import flash.display.MovieClip; + import flash.net.NetConnection; + import flash.net.ObjectEncoding; + import flash.net.Responder; + + public class Test extends MovieClip { + public function Test() { + var nc:NetConnection = new NetConnection(); + + // This test targets the AMF0 wire format. At time of writing Ruffle + // doesn't support AMF3 properly, and the StrictArray fix (#16381) is + // on the AMF0 path, so pin the encoding to AMF0 like the sibling + // netconnection_send_remote test does. + nc.objectEncoding = ObjectEncoding.AMF0; + nc.connect("http://localhost:8000/"); + + var responder:Responder = new Responder(onResult, null); + + // A genuine dense AS3 Array. Real Flash sends this as an AMF0 + // StrictArray (marker 0x0A); before #16381 Ruffle emitted an + // ECMAArray (marker 0x08) with "0"/"1" string keys instead. + var realArray:Array = new Array(); + realArray[0] = "real_0"; + realArray[1] = "real_1"; + + trace("--- Testing NetConnection Array Serialization ---"); + nc.call("test.arrays", responder, realArray); + } + + private function onResult(result:*):void { + trace("Received result"); + } + } +} diff --git a/tests/tests/swfs/avm2/netconnection_serialize_arrays/output.txt b/tests/tests/swfs/avm2/netconnection_serialize_arrays/output.txt new file mode 100644 index 000000000000..97129e7484de --- /dev/null +++ b/tests/tests/swfs/avm2/netconnection_serialize_arrays/output.txt @@ -0,0 +1,6 @@ +--- Testing NetConnection Array Serialization --- +Navigator::fetch: + URL: http://localhost:8000/ + Method: POST + Mime-Type: application/x-amf + Body: [00, 00, 00, 00, 00, 01, 00, 0B, 74, 65, 73, 74, 2E, 61, 72, 72, 61, 79, 73, 00, 02, 2F, 31, 00, 00, 00, 1C, 0A, 00, 00, 00, 01, 0A, 00, 00, 00, 02, 02, 00, 06, 72, 65, 61, 6C, 5F, 30, 02, 00, 06, 72, 65, 61, 6C, 5F, 31] diff --git a/tests/tests/swfs/avm2/netconnection_serialize_arrays/server.py b/tests/tests/swfs/avm2/netconnection_serialize_arrays/server.py new file mode 100644 index 000000000000..3344dd8424f7 --- /dev/null +++ b/tests/tests/swfs/avm2/netconnection_serialize_arrays/server.py @@ -0,0 +1,29 @@ +from http.server import HTTPServer, BaseHTTPRequestHandler + +class MyHandler(BaseHTTPRequestHandler): + def do_POST(self): + print("") + print("Navigator::fetch:") + print(f" URL: http://localhost:8000{self.path}") + print(" Method: POST") + print(f" Mime-Type: {self.headers.get('Content-Type')}") + + request = self.rfile.read(int(self.headers['Content-Length'])) + # Format as uppercase hex bytes + request_hex = ", ".join([f"{byte:02X}" for byte in request]) + + print(f" Body: [{request_hex}]") + print("") + + self.send_response(200) + self.end_headers() + self.wfile.write(b"") + +def run(server_class=HTTPServer, handler_class=MyHandler): + server_address = ('', 8000) + httpd = server_class(server_address, handler_class) + print("Running server on port 8000...") + httpd.serve_forever() + +if __name__ == '__main__': + run() diff --git a/tests/tests/swfs/avm2/netconnection_serialize_arrays/test.swf b/tests/tests/swfs/avm2/netconnection_serialize_arrays/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..0409e2577f47090b7297f8f4a1051fa2a24b5b8f GIT binary patch literal 1168 zcmV;B1aJF8S5qc12LJ$g0fkguZ`(E$B_-LkocJSOPU57Cx~9vr+Lr7LLF1<0jJO$E zpzRQ^*igVQ7>SCtDxx6KcG8C}zhsa58v_RHVQ=Y*ZSTV#`WEjObfn}Y_4+UalE}+* z4(~me^a)V@0wDV%080?GW|jc}-_N~)0PF^qT|a2;YbVF97uNag{zi{6zpm@U;jl8? zs?gv_ukP&Z=tfPi)yf=E4o^LXPs(0+Yoiefv`AJ6oZ$KWL)Qs=B)HtCO>H*B z0pF1JJuAvIB3=Up%?t=;^nXd0FhOy#chtv6q(Qvyvv#C;6^bgrj5HqOQ?0gL(rQL+ zJI+@`cXeU?JFOT;cy}8ho9{0_d0PIm*=~RR^q>XKbNmQQ{95@p@-Enu-oAbNJT3F3 zB!86ri(j|+F#+JtcYoVI@X;^nuSm$iPxC)30Em@nng5XQ(9v7e z?8j{m`Zjl4XPh67-{H^>bv!t(=^kMw^*myVu6H@z!LIX?ba4>i)9~IsAzZj$55MJ-4?L4vj(4OqAMYDkIuR?34}wcg?DlBL z>URyp(9t*>_26>16!rMH@3KU+Wm0V3H6lBBV(VL<-W)4oiyo4SNsskrdrPkw#6Z$Sbtv{v%hwQnIKR+37`+>vA`ed*blj4GStqeOJ3Dzb-4?RW#%sm{C z7`QUIJ|VIiuaSYcg!KpFBi1qFHz|nAt0owiBh_-btcebA548!awa5#tJ?>X@6Eu#V z5?!vu%~QCst}5udcyVRnV(x&Lgy(5#@q8fy@X81LFxM}=RtD6u7Q5Xe+GVtB5!>;c z=!Eo$>&re3m1=Ffnwh9lz4A2~n~?iYZPV`-o|*7u<~;`s6YE#TC*vlqUZ!U9i~e4I zwd!5Dy1g@#fQ3{+O3W@wsktRsgvI&dLa|U>EG`w73mb(yQU=OOB8gIJdTKh8T|g2- zl8WRslBbZIK~fgM9Fp@Z5Xmb@{s76hkPK1oVGe7&&0ux3{CXXhihQurNtagl)10&Vs&wPG*ZWiQ=I92`K$4{N z)Dq8N?cBJ48dw`?9C|Z?-sJ1D#IybE+~5dAKqqJCj85Lp8=V<@#^}u2vqopmo-;c0 i_Po)_+S%y!4Y(2idKu13a2a*(>Obsr?#922WiKl)3Q&*$ literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm2/netconnection_serialize_arrays/test.toml b/tests/tests/swfs/avm2/netconnection_serialize_arrays/test.toml new file mode 100644 index 000000000000..bfb882672871 --- /dev/null +++ b/tests/tests/swfs/avm2/netconnection_serialize_arrays/test.toml @@ -0,0 +1,5 @@ +# This test verifies the bytes sent over the network, see README.md. +# AS3 sends a dense Array as an AMF0 StrictArray over NetConnection.call (#16381). + +num_ticks = 10 +log_fetch = true