Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion core/src/avm1/globals/local_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 7 additions & 1 deletion core/src/avm1/globals/netconnection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
}
}

Expand Down
80 changes: 77 additions & 3 deletions core/src/avm1/globals/shared_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
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;
use std::collections::BTreeMap;
use std::rc::Rc;

#[derive(Default, Clone, Collect)]
#[collect(require_static)]
Expand Down Expand Up @@ -86,13 +87,61 @@
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, 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<Rc<AmfValue>> = Vec::new();
let mut associative: Vec<Element> = Vec::new();
// `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);
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)
}

/// Serialize an Object and any children to a JSON object
fn recursive_serialize<'gc>(
activation: &mut Activation<'_, 'gc>,
Expand Down Expand Up @@ -198,6 +247,31 @@
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);

Check warning on line 262 in core/src/avm1/globals/shared_object.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (262)
}

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

Check warning on line 272 in core/src/avm1/globals/shared_object.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (272)
}
}
AmfValue::Object(_, elements, _) => {
// Deserialize Object
let obj = Object::new(
Expand Down
2 changes: 1 addition & 1 deletion core/src/avm2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
107 changes: 90 additions & 17 deletions core/src/avm2/amf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,25 @@
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))
} else {
// TODO: is this right?
Some(AmfValue::ECMAArray(ObjectId::INVALID, vec![], values, 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) {
Expand Down Expand Up @@ -183,6 +183,79 @@
}
}

/// 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)

Check warning on line 218 in core/src/avm2/amf.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (216–218)
}
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)

Check warning on line 230 in core/src/avm2/amf.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (227–230)
}
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)

Check warning on line 237 in core/src/avm2/amf.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (232–237)
}
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)

Check warning on line 248 in core/src/avm2/amf.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (239–248)
}
leaf => leaf,
}
}

fn promote_rc(rc: Rc<AmfValue>) -> Rc<AmfValue> {
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>,
Expand Down
11 changes: 6 additions & 5 deletions core/src/avm2/globals/flash/net/local_connection.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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() {
Expand Down
7 changes: 5 additions & 2 deletions core/src/avm2/globals/flash/net/net_connection.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)));
}
}

Expand Down Expand Up @@ -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() {
Expand Down
12 changes: 12 additions & 0 deletions tests/tests/swfs/avm1/netconnection_serialize_arrays/README.md
Original file line number Diff line number Diff line change
@@ -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/`)
Original file line number Diff line number Diff line change
@@ -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]
29 changes: 29 additions & 0 deletions tests/tests/swfs/avm1/netconnection_serialize_arrays/server.py
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading