diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index 3497a24bec5e..8015ca41d20a 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -36,7 +36,7 @@ pub(crate) mod glow_filter; pub(crate) mod gradient_filter; mod key; mod load_vars; -mod local_connection; +pub(crate) mod local_connection; mod math; mod matrix; pub(crate) mod mouse; diff --git a/core/src/avm1/globals/local_connection.rs b/core/src/avm1/globals/local_connection.rs index 98a58a166439..f2b0e2b908be 100644 --- a/core/src/avm1/globals/local_connection.rs +++ b/core/src/avm1/globals/local_connection.rs @@ -2,14 +2,139 @@ use crate::avm1::activation::Activation; use crate::avm1::error::Error; +use crate::avm1::globals::shared_object::{deserialize_value, serialize}; +use crate::avm1::object::TObject; use crate::avm1::property_decl::{define_properties_on, Declaration}; -use crate::avm1::{Object, ScriptObject, Value}; -use crate::context::GcContext; +use crate::avm1::{ + ActivationIdentifier, ExecutionReason, NativeObject, Object, ScriptObject, Value, +}; +use crate::context::{GcContext, UpdateContext}; use crate::display_object::TDisplayObject; +use crate::local_connection::{LocalConnectionHandle, LocalConnections}; use crate::string::AvmString; +use flash_lso::types::Value as AmfValue; +use gc_arena::{Collect, Gc}; +use std::cell::RefCell; + +#[derive(Debug, Collect)] +#[collect(require_static)] +struct LocalConnectionData { + handle: RefCell>, +} + +#[derive(Copy, Clone, Debug, Collect)] +#[collect(no_drop)] +pub struct LocalConnection<'gc>(Gc<'gc, LocalConnectionData>); + +impl<'gc> LocalConnection<'gc> { + pub fn cast(value: Value<'gc>) -> Option { + if let Value::Object(object) = value { + if let NativeObject::LocalConnection(local_connection) = object.native() { + return Some(local_connection); + } + } + None + } + + pub fn is_connected(&self) -> bool { + self.0.handle.borrow().is_some() + } + + pub fn connect( + &self, + activation: &mut Activation<'_, 'gc>, + name: AvmString<'gc>, + this: Object<'gc>, + ) -> bool { + if self.is_connected() { + return false; + } + + let connection_handle = activation.context.local_connections.connect( + &LocalConnections::get_domain(activation.context.swf.url()), + this, + &name, + ); + let result = connection_handle.is_some(); + *self.0.handle.borrow_mut() = connection_handle; + result + } + + pub fn disconnect(&self, activation: &mut Activation<'_, 'gc>) { + if let Some(conn_handle) = self.0.handle.take() { + activation.context.local_connections.close(conn_handle); + } + } + + pub fn send_status( + context: &mut UpdateContext<'_, 'gc>, + this: Object<'gc>, + status: &'static str, + ) -> Result<(), Error<'gc>> { + let Some(root_clip) = context.stage.root_clip() else { + tracing::warn!("Ignored LocalConnection callback as there's no root movie"); + return Ok(()); + }; + let mut activation = Activation::from_nothing( + context.reborrow(), + ActivationIdentifier::root("[LocalConnection onStatus]"), + root_clip, + ); + let constructor = activation.context.avm1.prototypes().object_constructor; + let event = constructor + .construct(&mut activation, &[])? + .coerce_to_object(&mut activation); + event.set("level", status.into(), &mut activation)?; + this.call_method( + "onStatus".into(), + &[event.into()], + &mut activation, + ExecutionReason::Special, + )?; + Ok(()) + } + + pub fn run_method( + context: &mut UpdateContext<'_, 'gc>, + this: Object<'gc>, + method_name: AvmString<'gc>, + amf_arguments: Vec, + ) -> Result<(), Error<'gc>> { + let Some(root_clip) = context.stage.root_clip() else { + tracing::warn!("Ignored LocalConnection callback as there's no root movie"); + return Ok(()); + }; + let mut activation = Activation::from_nothing( + context.reborrow(), + ActivationIdentifier::root("[LocalConnection call]"), + root_clip, + ); + let mut args = Vec::with_capacity(amf_arguments.len()); + for arg in amf_arguments { + let reader = flash_lso::read::Reader::default(); + let value = deserialize_value( + &mut activation, + &arg, + &reader.amf0_decoder, + &mut Default::default(), + ); + args.push(value); + } + this.call_method( + method_name, + &args, + &mut activation, + ExecutionReason::Special, + )?; + Ok(()) + } +} const PROTO_DECLS: &[Declaration] = declare_properties! { - "domain" => method(domain; DONT_DELETE | READ_ONLY); + "domain" => method(domain; DONT_DELETE | DONT_ENUM); + "connect" => method(connect; DONT_DELETE | DONT_ENUM); + "close" => method(close; DONT_DELETE | DONT_ENUM); + "send" => method(send; DONT_DELETE | DONT_ENUM); }; pub fn domain<'gc>( @@ -18,29 +143,104 @@ pub fn domain<'gc>( _args: &[Value<'gc>], ) -> Result, Error<'gc>> { let movie = activation.base_clip().movie(); + let domain = LocalConnections::get_domain(movie.url()); - let domain = if let Ok(url) = url::Url::parse(movie.url()) { - if url.scheme() == "file" { - "localhost".into() - } else if let Some(domain) = url.domain() { - AvmString::new_utf8(activation.context.gc_context, domain) - } else { - // no domain? - "localhost".into() - } - } else { - tracing::error!("LocalConnection::domain: Unable to parse movie URL"); - return Ok(Value::Null); + Ok(Value::String(AvmString::new_utf8( + activation.context.gc_context, + domain, + ))) +} + +pub fn connect<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let Some(Value::String(connection_name)) = args.get(0) else { + // This is deliberately not a coercion, Flash tests the type + return Ok(false.into()); + }; + if connection_name.is_empty() || connection_name.contains(b':') { + return Ok(false.into()); + } + + if let Some(local_connection) = LocalConnection::cast(this.into()) { + return Ok(local_connection + .connect(activation, *connection_name, this) + .into()); + } + + Ok(Value::Undefined) +} + +pub fn send<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + let Some(Value::String(connection_name)) = args.get(0) else { + // This is deliberately not a coercion, Flash tests the type + return Ok(false.into()); + }; + let Some(Value::String(method_name)) = args.get(1) else { + // This is deliberately not a coercion, Flash tests the type + return Ok(false.into()); }; - Ok(Value::String(domain)) + if connection_name.is_empty() || method_name.is_empty() { + return Ok(false.into()); + } + + if method_name == b"send" + || method_name == b"connect" + || method_name == b"close" + || method_name == b"allowDomain" + || method_name == b"allowInsecureDomain" + || method_name == b"domain" + { + return Ok(false.into()); + } + + let mut amf_arguments = Vec::with_capacity(args.len() - 2); + for arg in &args[2..] { + amf_arguments.push(serialize(activation, *arg)); + } + + activation.context.local_connections.send( + &LocalConnections::get_domain(activation.context.swf.url()), + this, + *connection_name, + *method_name, + amf_arguments, + ); + Ok(true.into()) +} + +pub fn close<'gc>( + activation: &mut Activation<'_, 'gc>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + if let Some(local_connection) = LocalConnection::cast(this.into()) { + local_connection.disconnect(activation); + } + Ok(Value::Undefined) } pub fn constructor<'gc>( - _activation: &mut Activation<'_, 'gc>, + activation: &mut Activation<'_, 'gc>, this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error<'gc>> { + this.set_native( + activation.gc(), + NativeObject::LocalConnection(LocalConnection(Gc::new( + activation.gc(), + LocalConnectionData { + handle: RefCell::new(None), + }, + ))), + ); Ok(this.into()) } diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index e42e9b19731c..e4f8bbc3b2cc 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -12,6 +12,7 @@ use crate::avm1::globals::drop_shadow_filter::DropShadowFilter; use crate::avm1::globals::file_reference::FileReferenceObject; use crate::avm1::globals::glow_filter::GlowFilter; use crate::avm1::globals::gradient_filter::GradientFilter; +use crate::avm1::globals::local_connection::LocalConnection; use crate::avm1::globals::netconnection::NetConnection; use crate::avm1::globals::shared_object::SharedObject; use crate::avm1::globals::transform::TransformObject; @@ -66,6 +67,7 @@ pub enum NativeObject<'gc> { XmlSocket(XmlSocket<'gc>), FileReference(FileReferenceObject<'gc>), NetConnection(NetConnection<'gc>), + LocalConnection(LocalConnection<'gc>), } /// Represents an object that can be directly interacted with by the AVM diff --git a/core/src/avm2/amf.rs b/core/src/avm2/amf.rs index 77e800a3bad0..2539a76e82da 100644 --- a/core/src/avm2/amf.rs +++ b/core/src/avm2/amf.rs @@ -55,22 +55,27 @@ pub fn serialize_value<'gc>( // Don't serialize properties from the vtable (we don't want a 'length' field) recursive_serialize(activation, o, &mut values, None, amf_version, object_table) .unwrap(); + let len = o.as_array_storage().unwrap().length() as u32; - 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); + 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); + } } - } - let len = o.as_array_storage().unwrap().length() as u32; - Some(AmfValue::ECMAArray(dense, sparse, len)) + Some(AmfValue::ECMAArray(dense, sparse, len)) + } else { + // TODO: is this right? + Some(AmfValue::ECMAArray(vec![], values, len)) + } } else if let Some(vec) = o.as_vector_storage() { let val_type = vec.value_type(); if val_type == Some(activation.avm2().classes().int.inner_class_definition()) { diff --git a/core/src/avm2/error.rs b/core/src/avm2/error.rs index 502d49fcd92f..57233a671553 100644 --- a/core/src/avm2/error.rs +++ b/core/src/avm2/error.rs @@ -524,6 +524,23 @@ pub fn make_error_2037<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> } } +#[inline(never)] +#[cold] +pub fn make_error_2085<'gc>(activation: &mut Activation<'_, 'gc>, param_name: &str) -> Error<'gc> { + let err = argument_error( + activation, + &format!( + "Error #2085: Parameter {} must be non-empty string.", + param_name + ), + 2007, + ); + match err { + Ok(err) => Error::AvmError(err), + Err(err) => err, + } +} + #[inline(never)] #[cold] pub fn make_error_2097<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> { diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index e2b83236bbca..d27c6ada4c49 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -165,6 +165,7 @@ pub struct SystemClasses<'gc> { pub netstatusevent: ClassObject<'gc>, pub shaderfilter: ClassObject<'gc>, pub statusevent: ClassObject<'gc>, + pub asyncerrorevent: ClassObject<'gc>, pub contextmenuevent: ClassObject<'gc>, pub filereference: ClassObject<'gc>, pub filefilter: ClassObject<'gc>, @@ -293,6 +294,7 @@ impl<'gc> SystemClasses<'gc> { netstatusevent: object, shaderfilter: object, statusevent: object, + asyncerrorevent: object, contextmenuevent: object, filereference: object, filefilter: object, @@ -804,6 +806,7 @@ fn load_playerglobal<'gc>( ("flash.events", "UncaughtErrorEvents", uncaughterrorevents), ("flash.events", "NetStatusEvent", netstatusevent), ("flash.events", "StatusEvent", statusevent), + ("flash.events", "AsyncErrorEvent", asyncerrorevent), ("flash.events", "ContextMenuEvent", contextmenuevent), ("flash.events", "FocusEvent", focusevent), ("flash.geom", "Matrix", matrix), diff --git a/core/src/avm2/globals/flash/net/LocalConnection.as b/core/src/avm2/globals/flash/net/LocalConnection.as index e41fe837bccc..39a70d581cf2 100644 --- a/core/src/avm2/globals/flash/net/LocalConnection.as +++ b/core/src/avm2/globals/flash/net/LocalConnection.as @@ -25,21 +25,7 @@ package flash.net { public native function connect(connectionName:String):void; - public function send(connectionName: String, methodName: String, ... arguments):void { - if (connectionName === null) { - throw new TypeError("Error #2007: Parameter connectionName must be non-null.", 2007); - } - if (methodName === null) { - throw new TypeError("Error #2007: Parameter methodName must be non-null.", 2007); - } - - var self = this; - setTimeout(function() { - self.send_internal(connectionName, methodName, arguments); - }, 0); - } - - private native function send_internal(connectionName: String, methodName: String, args: Array):void; + public native function send(connectionName: String, methodName: String, ... arguments):void; public function allowDomain(... domains): void { stub_method("flash.net.LocalConnection", "allowDomain"); diff --git a/core/src/avm2/globals/flash/net/local_connection.rs b/core/src/avm2/globals/flash/net/local_connection.rs index c92a89bb8f7d..7f055b58901e 100644 --- a/core/src/avm2/globals/flash/net/local_connection.rs +++ b/core/src/avm2/globals/flash/net/local_connection.rs @@ -1,12 +1,13 @@ -use crate::avm2::error::{argument_error, make_error_2007}; +use crate::avm2::amf::serialize_value; +use crate::avm2::error::{argument_error, make_error_2004, make_error_2085, Error2004Type}; use crate::avm2::object::TObject; use crate::avm2::parameters::ParametersExt; -use crate::avm2::{Activation, Avm2, Error, Object, Value}; +use crate::avm2::{Activation, Error, Object, Value}; use crate::string::AvmString; - -use crate::avm2_stub_method; +use flash_lso::types::{AMFVersion, Value as AmfValue}; pub use crate::avm2::object::local_connection_allocator; +use crate::local_connection::LocalConnections; /// Implements `domain` getter pub fn get_domain<'gc>( @@ -15,60 +16,56 @@ pub fn get_domain<'gc>( _args: &[Value<'gc>], ) -> Result, Error<'gc>> { let movie = &activation.context.swf; + let domain = LocalConnections::get_domain(movie.url()); - let domain = if let Ok(url) = url::Url::parse(movie.url()) { - if url.scheme() == "file" { - "localhost".into() - } else if let Some(domain) = url.domain() { - AvmString::new_utf8(activation.context.gc_context, domain) - } else { - // no domain? - "localhost".into() - } - } else { - tracing::error!("LocalConnection::domain: Unable to parse movie URL"); - return Ok(Value::Null); - }; - - Ok(Value::String(domain)) + Ok(Value::String(AvmString::new_utf8( + activation.context.gc_context, + domain, + ))) } /// Implements `LocalConnection.send` -pub fn send_internal<'gc>( +pub fn send<'gc>( activation: &mut Activation<'_, 'gc>, this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error<'gc>> { - // Already null-checked by the AS wrapper `LocalConnection.send` - let connection_name = args.get_value(0); - - let connection_name = connection_name.coerce_to_string(activation)?; + let connection_name = args.get_string_non_null(activation, 0, "connectionName")?; + let method_name = args.get_string_non_null(activation, 1, "methodName")?; - let event_name = if activation - .context - .local_connections - .all_by_name(connection_name) - .is_empty() + if connection_name.is_empty() { + return Err(make_error_2085(activation, "connectionName")); + } + if method_name.is_empty() { + return Err(make_error_2085(activation, "methodName")); + } + if &method_name == b"send" + || &method_name == b"connect" + || &method_name == b"close" + || &method_name == b"allowDomain" + || &method_name == b"allowInsecureDomain" + || &method_name == b"domain" { - "error" - } else { - avm2_stub_method!(activation, "flash.net.LocalConnection", "send"); - - "status" - }; + return Err(make_error_2004(activation, Error2004Type::ArgumentError)); + } - let event = activation.avm2().classes().statusevent.construct( - activation, - &[ - "status".into(), - false.into(), - false.into(), - Value::Null, - event_name.into(), - ], - )?; + 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), + ); + } - Avm2::dispatch_event(&mut activation.context, event, this); + if let Some(local_connection) = this.as_local_connection_object() { + activation.context.local_connections.send( + &LocalConnections::get_domain(activation.context.swf.url()), + (activation.domain(), local_connection), + connection_name, + method_name, + amf_arguments, + ); + } Ok(Value::Undefined) } @@ -79,22 +76,24 @@ pub fn connect<'gc>( this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error<'gc>> { - let connection_name = args.get_value(0); - if matches!(connection_name, Value::Null) { - return Err(make_error_2007(activation, "connectionName")); - }; + let connection_name = args.get_string_non_null(activation, 0, "connectionName")?; + if connection_name.is_empty() { + return Err(make_error_2085(activation, "connectionName")); + } + if connection_name.contains(b':') { + return Err(make_error_2004(activation, Error2004Type::ArgumentError)); + } if let Some(local_connection) = this.as_local_connection_object() { - if local_connection.is_connected() { + if !local_connection.connect(activation, connection_name) { + // This triggers both if this object is already connected, OR there's something else taking the name + // (The error message is misleading, in that case!) return Err(Error::AvmError(argument_error( activation, "Error #2082: Connect failed because the object is already connected.", 2082, )?)); } - - let connection_name = connection_name.coerce_to_string(activation)?; - local_connection.connect(activation, connection_name); } Ok(Value::Undefined) diff --git a/core/src/avm2/object/local_connection_object.rs b/core/src/avm2/object/local_connection_object.rs index f6a35f8b8613..989255d38bbd 100644 --- a/core/src/avm2/object/local_connection_object.rs +++ b/core/src/avm2/object/local_connection_object.rs @@ -1,11 +1,14 @@ use crate::avm2::activation::Activation; +use crate::avm2::amf::deserialize_value; use crate::avm2::object::script_object::ScriptObjectData; use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; use crate::avm2::value::Value; -use crate::avm2::Error; -use crate::local_connection::{LocalConnection, LocalConnectionHandle}; +use crate::avm2::{Avm2, Domain, Error}; +use crate::context::UpdateContext; +use crate::local_connection::{LocalConnectionHandle, LocalConnections}; use crate::string::AvmString; use core::fmt; +use flash_lso::types::Value as AmfValue; use gc_arena::{Collect, GcCell, GcWeakCell, Mutation}; use std::cell::{Ref, RefMut}; @@ -42,7 +45,7 @@ impl fmt::Debug for LocalConnectionObject<'_> { } } -#[derive(Clone, Collect)] +#[derive(Collect)] #[collect(no_drop)] pub struct LocalConnectionObjectData<'gc> { /// Base script object @@ -57,30 +60,91 @@ impl<'gc> LocalConnectionObject<'gc> { self.0.read().connection_handle.is_some() } - pub fn connection_handle(&self) -> Option { - self.0.read().connection_handle - } - - pub fn connect(&self, activation: &mut Activation<'_, 'gc>, name: AvmString<'gc>) { - assert!(!self.is_connected()); + pub fn connect(&self, activation: &mut Activation<'_, 'gc>, name: AvmString<'gc>) -> bool { + if self.is_connected() { + return false; + } - let connection_handle = activation - .context - .local_connections - .insert(LocalConnection::new(*self, name)); + let connection_handle = activation.context.local_connections.connect( + &LocalConnections::get_domain(activation.context.swf.url()), + (activation.domain(), *self), + &name, + ); + let result = connection_handle.is_some(); self.0 .write(activation.context.gc_context) - .connection_handle = Some(connection_handle); + .connection_handle = connection_handle; + result } pub fn disconnect(&self, activation: &mut Activation<'_, 'gc>) { - if let Some(conn_handle) = self.0.read().connection_handle { - activation.context.local_connections.remove(conn_handle); + if let Some(conn_handle) = self + .0 + .write(activation.context.gc_context) + .connection_handle + .take() + { + activation.context.local_connections.close(conn_handle); } + } - self.0 - .write(activation.context.gc_context) - .connection_handle = None; + pub fn send_status(&self, context: &mut UpdateContext<'_, 'gc>, status: &'static str) { + let mut activation = Activation::from_nothing(context.reborrow()); + if let Ok(event) = activation.avm2().classes().statusevent.construct( + &mut activation, + &[ + "status".into(), + false.into(), + false.into(), + Value::Null, + status.into(), + ], + ) { + Avm2::dispatch_event(&mut activation.context, event, (*self).into()); + } + } + + pub fn run_method( + &self, + context: &mut UpdateContext<'_, 'gc>, + domain: Domain<'gc>, + method_name: AvmString<'gc>, + amf_arguments: Vec, + ) { + let mut activation = Activation::from_domain(context.reborrow(), domain); + let mut arguments = Vec::with_capacity(amf_arguments.len()); + + for argument in amf_arguments { + arguments + .push(deserialize_value(&mut activation, &argument).unwrap_or(Value::Undefined)); + } + + if let Ok(client) = self + .get_public_property("client", &mut activation) + .and_then(|v| v.coerce_to_object(&mut activation)) + { + if let Err(e) = client.call_public_property(method_name, &arguments, &mut activation) { + match e { + Error::AvmError(error) => { + if let Ok(event) = activation.avm2().classes().asyncerrorevent.construct( + &mut activation, + &[ + "asyncError".into(), + false.into(), + false.into(), + error, + error, + ], + ) { + Avm2::dispatch_event(&mut activation.context, event, (*self).into()); + } + } + _ => { + tracing::error!("Unhandled error dispatching AVM2 LocalConnection method call to '{method_name}': {e}"); + } + } + } + } } } diff --git a/core/src/local_connection.rs b/core/src/local_connection.rs index 842ec6693f29..74d33a7d3f20 100644 --- a/core/src/local_connection.rs +++ b/core/src/local_connection.rs @@ -1,55 +1,130 @@ +use crate::avm1::globals::local_connection::LocalConnection as Avm1LocalConnectionObject; use crate::avm1::Object as Avm1Object; use crate::avm2::object::LocalConnectionObject; +use crate::avm2::Domain as Avm2Domain; +use crate::context::UpdateContext; use crate::string::AvmString; +use flash_lso::types::Value as AmfValue; +use fnv::FnvHashMap; use gc_arena::Collect; -use slotmap::{new_key_type, SlotMap}; +use ruffle_wstr::{WStr, WString}; +use std::borrow::Cow; -new_key_type! { - pub struct LocalConnectionHandle; -} - -#[derive(Collect)] +#[derive(Clone, Collect)] #[collect(no_drop)] pub enum LocalConnectionKind<'gc> { - Avm2(LocalConnectionObject<'gc>), + Avm2(Avm2Domain<'gc>, LocalConnectionObject<'gc>), Avm1(Avm1Object<'gc>), } -impl<'gc> From> for LocalConnectionKind<'gc> { - fn from(obj: LocalConnectionObject<'gc>) -> Self { - Self::Avm2(obj) +impl<'gc> From<(Avm2Domain<'gc>, LocalConnectionObject<'gc>)> for LocalConnectionKind<'gc> { + fn from(obj: (Avm2Domain<'gc>, LocalConnectionObject<'gc>)) -> Self { + Self::Avm2(obj.0, obj.1) + } +} + +impl<'gc> From> for LocalConnectionKind<'gc> { + fn from(obj: Avm1Object<'gc>) -> Self { + Self::Avm1(obj) + } +} + +impl<'gc> LocalConnectionKind<'gc> { + pub fn send_status(&self, context: &mut UpdateContext<'_, 'gc>, status: &'static str) { + match self { + LocalConnectionKind::Avm2(_domain, object) => { + object.send_status(context, status); + } + LocalConnectionKind::Avm1(object) => { + if let Err(e) = Avm1LocalConnectionObject::send_status(context, *object, status) { + tracing::error!("Unhandled AVM1 error during LocalConnection onStatus: {e}"); + } + } + } + } + + pub fn run_method( + &self, + context: &mut UpdateContext<'_, 'gc>, + method_name: AvmString<'gc>, + arguments: Vec, + ) { + match self { + LocalConnectionKind::Avm2(domain, object) => { + object.run_method(context, *domain, method_name, arguments); + } + LocalConnectionKind::Avm1(object) => { + if let Err(e) = + Avm1LocalConnectionObject::run_method(context, *object, method_name, arguments) + { + tracing::error!("Unhandled AVM1 error during LocalConnection onStatus: {e}"); + } + } + } } } #[derive(Collect)] #[collect(no_drop)] -pub struct LocalConnection<'gc> { - object: LocalConnectionKind<'gc>, +pub struct QueuedMessage<'gc> { + source: LocalConnectionKind<'gc>, + kind: QueuedMessageKind<'gc>, +} - connection_name: AvmString<'gc>, +#[derive(Collect)] +#[collect(no_drop)] +pub enum QueuedMessageKind<'gc> { + Failure, + Message { + #[collect(require_static)] + connection_name: WString, + method_name: AvmString<'gc>, + #[collect(require_static)] + arguments: Vec, + }, } -impl<'gc> LocalConnection<'gc> { - pub fn new( - object: impl Into>, - connection_name: AvmString<'gc>, - ) -> Self { - Self { - object: object.into(), - connection_name, +impl<'gc> QueuedMessageKind<'gc> { + pub fn deliver(self, source: LocalConnectionKind<'gc>, context: &mut UpdateContext<'_, 'gc>) { + match self { + QueuedMessageKind::Failure => { + source.send_status(context, "error"); + } + QueuedMessageKind::Message { + connection_name, + method_name, + arguments, + } => { + if let Some(receiver) = context.local_connections.find_listener(&connection_name) { + source.send_status(context, "status"); + receiver.run_method(context, method_name, arguments); + } else { + source.send_status(context, "error"); + } + } } } } +/// An opaque handle to an actively listening LocalConnection. +/// Owning this handle represents ownership of a LocalConnection; +/// However, a LocalConnection must be manually closed, you can't just Drop this handle. +#[derive(Debug)] +pub struct LocalConnectionHandle(WString); + /// Manages the collection of local connections. pub struct LocalConnections<'gc> { - connections: SlotMap>, + connections: FnvHashMap>, + messages: Vec>, } -unsafe impl<'gc> Collect for LocalConnections<'gc> { +unsafe impl Collect for LocalConnections<'_> { fn trace(&self, cc: &gc_arena::Collection) { - for (_, connection) in self.connections.iter() { - connection.trace(cc) + for (_, v) in self.connections.iter() { + v.trace(cc); + } + for m in self.messages.iter() { + m.trace(cc); } } } @@ -57,26 +132,108 @@ unsafe impl<'gc> Collect for LocalConnections<'gc> { impl<'gc> LocalConnections<'gc> { pub fn empty() -> Self { Self { - connections: SlotMap::with_key(), + connections: Default::default(), + messages: Default::default(), } } - pub fn insert(&mut self, connection: LocalConnection<'gc>) -> LocalConnectionHandle { - self.connections.insert(connection) + pub fn connect>>( + &mut self, + domain: &str, + connection: C, + name: &WStr, + ) -> Option { + let key = if name.starts_with(b'_') { + name.to_ascii_lowercase() + } else { + let mut key = WString::from_utf8(Self::get_superdomain(domain)); + key.push_char(':'); + key.push_str(name); + key.make_ascii_lowercase(); + key + }; + + if self.connections.contains_key(&key) { + None + } else { + self.connections.insert(key.to_owned(), connection.into()); + Some(LocalConnectionHandle(key.to_owned())) + } } - pub fn remove(&mut self, handle: LocalConnectionHandle) { - self.connections.remove(handle); + pub fn close(&mut self, handle: LocalConnectionHandle) { + self.connections.remove(&handle.0); } - pub fn all_by_name(&self, requested_name: AvmString<'gc>) -> Vec<&LocalConnection<'gc>> { - let mut conns = Vec::new(); - for (_, connection) in self.connections.iter() { - if connection.connection_name == requested_name { - conns.push(connection); + pub fn send>>( + &mut self, + domain: &str, + source: C, + connection_name: AvmString<'gc>, + method_name: AvmString<'gc>, + arguments: Vec, + ) { + // There's two checks for "is connected": + // 1 - At `send()` time, if there's no connections, just immediately queue up a failure + // 2 - At `update_connections()` time, if the connection couldn't be found, queue up a failure + // Even if one becomes available between send and update, it won't be used + // Similarly, if one becomes unavailable between send and update, it'll error + // If something *else* takes its place between send and update, it'll use that instead + + let mut connection_name = connection_name.to_ascii_lowercase(); + if !connection_name.contains(b':') && !connection_name.starts_with(b'_') { + let mut result = WString::from_utf8(Self::get_superdomain(domain)); + result.push_char(':'); + result.push_str(&connection_name); + connection_name = result; + } + + let kind = if self.find_listener(&connection_name).is_some() { + QueuedMessageKind::Message { + connection_name, + method_name, + arguments, } + } else { + QueuedMessageKind::Failure + }; + self.messages.push(QueuedMessage { + source: source.into(), + kind, + }); + } + + fn find_listener(&self, name: &WStr) -> Option> { + self.connections.get(name).cloned() + } + + pub fn update_connections(context: &mut UpdateContext<'_, 'gc>) { + if context.local_connections.messages.is_empty() { + return; + } + + for message in std::mem::take(&mut context.local_connections.messages) { + message.kind.deliver(message.source, context); } + } + + pub fn get_domain(url: &str) -> Cow<'static, str> { + if let Ok(url) = url::Url::parse(url) { + if url.scheme() == "file" { + Cow::Borrowed("localhost") + } else if let Some(domain) = url.domain() { + Cow::Owned(domain.to_owned()) + } else { + // no domain? + Cow::Borrowed("localhost") + } + } else { + tracing::error!("LocalConnection: Unable to parse movie URL: {url}"); + return Cow::Borrowed("unknown"); // this is surely an error but it'll hopefully highlight this case in issues for us + } + } - conns + pub fn get_superdomain(domain: &str) -> &str { + domain.rsplit_once('.').map(|(_, b)| b).unwrap_or(domain) } } diff --git a/core/src/player.rs b/core/src/player.rs index 37e79a6b05ef..e67e7c3910cd 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1715,6 +1715,7 @@ impl Player { run_all_phases_avm2(context); Avm1::run_frame(context); AudioManager::update_sounds(context); + LocalConnections::update_connections(context); // Only run the current list of callbacks - any callbacks added during callback execution // will be run at the end of the *next* frame. diff --git a/tests/tests/swfs/avm1/localconnection/CustomLocalConnection.as b/tests/tests/swfs/avm1/localconnection/CustomLocalConnection.as new file mode 100644 index 000000000000..56970b1d4da8 --- /dev/null +++ b/tests/tests/swfs/avm1/localconnection/CustomLocalConnection.as @@ -0,0 +1,15 @@ +class CustomLocalConnection extends LocalConnection { + function test() { + trace("custom.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s")); + if (arguments.length > 0) { + trace(" " + repr(arguments)); + } + } + + function throwAnError() { + trace("custom.throwAnError was called"); + //throw "aah!"; // [NA] this crashes every Flash Player I've tried + //throw {}; // [NA] this causes an error when constructing the AsyncErrorEvent + //throw new Error("aaah!"); + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm1/localconnection/avm1child/child.fla b/tests/tests/swfs/avm1/localconnection/avm1child/child.fla new file mode 100644 index 000000000000..af61bd7a8ce8 Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection/avm1child/child.fla differ diff --git a/tests/tests/swfs/avm1/localconnection/avm1child/child.swf b/tests/tests/swfs/avm1/localconnection/avm1child/child.swf new file mode 100644 index 000000000000..ac2066ecc545 Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection/avm1child/child.swf differ diff --git a/tests/tests/swfs/avm1/localconnection/avm2child/Child.as b/tests/tests/swfs/avm1/localconnection/avm2child/Child.as new file mode 100644 index 000000000000..cba2a0fcbb50 --- /dev/null +++ b/tests/tests/swfs/avm1/localconnection/avm2child/Child.as @@ -0,0 +1,111 @@ +package { + + import flash.display.MovieClip; + import flash.net.LocalConnection; + import flash.utils.getQualifiedClassName; + + + public class Child extends MovieClip { + var lc: LocalConnection = new LocalConnection(); + + public function Child() { + lc.connect("avm2_child"); + lc.client = {}; + lc.client.test = function() { + trace("avm2_child.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s")); + if (arguments.length > 0) { + trace(" " + repr(arguments)); + } + } + } + + private function getObjectId(needle: Object, haystack: Array): String { + for (var i = 0; i < haystack.length; i++) { + if (haystack[i] === needle) { + return i; + } + } + return null; + } + + public function repr(value: *, indent: String = " ", seenObjects: Array = null) { + if (seenObjects == null) { + seenObjects = []; + } + if (value === lc) { + return "lc"; + } + + if (value === undefined || value === null || value === true || value === false || value is Number) { + return String(value); + } else if (value is String) { + return escapeString(value); + } else { + var existingId = getObjectId(value, seenObjects); + if (existingId != null) { + return "*" + existingId; + } + existingId = seenObjects.length; + seenObjects.push(value); + if (value is Array) { + if (value.length == 0) { + return "*" + existingId + " []"; + } else { + var result = "*" + existingId + " [\n"; + var nextIndent = indent + " "; + for (var i = 0; i < value.length; i++) { + result += nextIndent + repr(value[i], nextIndent, seenObjects) + "\n"; + } + return result + indent + "]"; + } + } else { + var keys = []; + for (var key in value) { + keys.push(key); + } + keys.sort(); + + var result = "*" + existingId + " " + getQualifiedClassName(value) + " {"; + + if (keys.length == 0) { + return result + "}"; + } else { + result += "\n"; + var nextIndent = indent + " "; + for (var i = 0; i < keys.length; i++) { + result += nextIndent + keys[i] + " = " + repr(value[keys[i]], nextIndent, seenObjects) + "\n"; + } + return result + indent + "}"; + } + } + } + } + + public function escapeString(input: String): String { + var output:String = "\""; + for (var i:int = 0; i < input.length; i++) { + var char:String = input.charAt(i); + switch (char) { + case "\\": + output += "\\\\"; + break; + case "\"": + output += "\\\""; + break; + case "\n": + output += "\\n"; + break; + case "\r": + output += "\\r"; + break; + case "\t": + output += "\\t"; + break; + default: + output += char; + } + } + return output + "\""; + } + } +} diff --git a/tests/tests/swfs/avm1/localconnection/avm2child/child.fla b/tests/tests/swfs/avm1/localconnection/avm2child/child.fla new file mode 100644 index 000000000000..39cd99bd3d5e Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection/avm2child/child.fla differ diff --git a/tests/tests/swfs/avm1/localconnection/avm2child/child.swf b/tests/tests/swfs/avm1/localconnection/avm2child/child.swf new file mode 100644 index 000000000000..30d9aaced1cb Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection/avm2child/child.swf differ diff --git a/tests/tests/swfs/avm1/localconnection/output.txt b/tests/tests/swfs/avm1/localconnection/output.txt new file mode 100644 index 000000000000..01da9f000ba3 --- /dev/null +++ b/tests/tests/swfs/avm1/localconnection/output.txt @@ -0,0 +1,580 @@ + +-- start test: A message to nowhere! -- + +sender.send("nowhere", "test", *0 []): true + +-- end frame: A message to nowhere! -- + +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: A message to nowhere! -- + + +-- start test: Both receivers try to connect to the same channel -- + +receiver.connect("channel"): true +custom.connect("channel"): false + +-- end frame: Both receivers try to connect to the same channel -- + + +-- end test: Both receivers try to connect to the same channel -- + + +-- start test: A message to an unimplemented function -- + +sender.send("channel", "unimplemented", *0 []): true + +-- end frame: A message to an unimplemented function -- + +sender.onStatus was called + *0 Object { + level = "status" + } + +-- end test: A message to an unimplemented function -- + + +-- start test: Receiver tries to connect elsewhere, but can't -- + +receiver.connect("elsewhere"): false + +-- end frame: Receiver tries to connect elsewhere, but can't -- + + +-- end test: Receiver tries to connect elsewhere, but can't -- + + +-- start test: Receiver actually connects elsewhere, and custom is allowed to connect to channel -- + +receiver.close() +receiver.connect("elsewhere"): true +custom.connect("channel"): true + +-- end frame: Receiver actually connects elsewhere, and custom is allowed to connect to channel -- + + +-- end test: Receiver actually connects elsewhere, and custom is allowed to connect to channel -- + + +-- start test: Sender calls test() on 'channel' -- + +sender.send("channel", "test", *0 []): true + +-- end frame: Sender calls test() on 'channel' -- + +sender.onStatus was called + *0 Object { + level = "status" + } +custom.test was called with 0 argument + +-- end test: Sender calls test() on 'channel' -- + + +-- start test: Sender calls test() on 'channel'... after the listener is gone -- + +custom.close() +sender.send("channel", "test", *0 []): true + +-- end frame: Sender calls test() on 'channel'... after the listener is gone -- + +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: Sender calls test() on 'channel'... after the listener is gone -- + + +-- start test: Sender calls test() on 'elsewhere'... immediately before the listener is gone -- + +sender.send("elsewhere", "test", *0 []): true +receiver.close() + +-- end frame: Sender calls test() on 'elsewhere'... immediately before the listener is gone -- + +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: Sender calls test() on 'elsewhere'... immediately before the listener is gone -- + + +-- start test: Sender calls test() on 'channel'... before the listener connects -- + +sender.send("channel", "test", *0 []): true +custom.connect("channel"): true + +-- end frame: Sender calls test() on 'channel'... before the listener connects -- + +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: Sender calls test() on 'channel'... before the listener connects -- + + +-- start test: Sending to a channel that gets reassigned before end-of-frame -- + +sender.send("channel", "test", *0 []): true +custom.close() +receiver.connect("channel"): true + +-- end frame: Sending to a channel that gets reassigned before end-of-frame -- + +sender.onStatus was called + *0 Object { + level = "status" + } +receiver.test was called with 0 argument + +-- end test: Sending to a channel that gets reassigned before end-of-frame -- + + +-- start test: Channels reconnect and receive -- + +custom.close() +receiver.close() +receiver.connect("elsewhere"): true +sender.send("channel", "test", *0 []): true +sender.send("elsewhere", "test", *0 []): true +custom.connect("channel"): true + +-- end frame: Channels reconnect and receive -- + +sender.onStatus was called + *0 Object { + level = "error" + } +sender.onStatus was called + *0 Object { + level = "status" + } +receiver.test was called with 0 argument + +-- end test: Channels reconnect and receive -- + + +-- start test: A connected listener can also send -- + +receiver.send("channel", "test", *0 []): true +receiver.send("elsewhere", "test", *0 []): true + +-- end frame: A connected listener can also send -- + +receiver.onStatus was called + *0 Object { + level = "status" + } +custom.test was called with 0 argument +receiver.onStatus was called + *0 Object { + level = "status" + } +receiver.test was called with 0 argument + +-- end test: A connected listener can also send -- + + +-- start test: A listener throws an error -- + +sender.send("channel", "throwAnError", *0 []): true + +-- end frame: A listener throws an error -- + +sender.onStatus was called + *0 Object { + level = "status" + } +custom.throwAnError was called + +-- end test: A listener throws an error -- + + +-- start test: Close something's that's already closed -- + + +-- end frame: Close something's that's already closed -- + + +-- end test: Close something's that's already closed -- + + +-- start test: Send to funky channel names -- + +sender.send(null, "test", *0 []): false +sender.send(0, "test", *0 []): false +sender.send("", "test", *0 []): false +sender.send(" ??? ", "test", *0 []): true + +-- end frame: Send to funky channel names -- + +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: Send to funky channel names -- + + +-- start test: Send to funky methods -- + +sender.send("channel", null, *0 []): false +sender.send("channel", 0, *0 []): false +sender.send("channel", "", *0 []): false +sender.send("channel", " ??? ", *0 []): true + +-- end frame: Send to funky methods -- + +sender.onStatus was called + *0 Object { + level = "status" + } + +-- end test: Send to funky methods -- + + +-- start test: Connect to funky names -- + +sender.connect(null): false +sender.close() +sender.connect(0): false +sender.close() +sender.connect(""): false +sender.close() +sender.connect(" ??? "): true +sender.close() + +-- end frame: Connect to funky names -- + + +-- end test: Connect to funky names -- + + +-- start test: Connect to something with a prefix -- + +sender.connect("localhost:something"): false +sender.close() + +-- end frame: Connect to something with a prefix -- + + +-- end test: Connect to something with a prefix -- + + +-- start test: Send to protected methods -- + +sender.send("channel", "send", *0 []): false +sender.send("channel", "connect", *0 []): false +sender.send("channel", "close", *0 []): false +sender.send("channel", "allowDomain", *0 []): false +sender.send("channel", "allowInsecureDomain", *0 []): false +sender.send("channel", "domain", *0 []): false + +-- end frame: Send to protected methods -- + + +-- end test: Send to protected methods -- + + +-- start test: Arguments are sent -- + +sender.send("elsewhere", "test", *0 [ + 1 + "two" + *1 Object { + value = 3 + } + ]): true + +-- end frame: Arguments are sent -- + +sender.onStatus was called + *0 Object { + level = "status" + } +receiver.test was called with 3 arguments + *0 [ + 1 + "two" + *1 Object { + value = 3 + } + ] + +-- end test: Arguments are sent -- + + +-- start test: Explicit host prefix -- + +sender.send("localhost:channel", "test", *0 []): true +sender.send("notlocalhost:elsewhere", "test", *0 []): true + +-- end frame: Explicit host prefix -- + +sender.onStatus was called + *0 Object { + level = "status" + } +custom.test was called with 0 argument +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: Explicit host prefix -- + + +-- start test: Underscores in names -- + +custom.close() +custom.connect("_channel"): true +sender.send("_channel", "test", *0 []): true + +-- end frame: Underscores in names -- + +sender.onStatus was called + *0 Object { + level = "status" + } +custom.test was called with 0 argument + +-- end test: Underscores in names -- + + +-- start test: Underscores in name doesn't allow a prefix -- + +sender.send("localhost:channel", "test", *0 []): true +sender.send("localhost:_channel", "test", *0 []): true + +-- end frame: Underscores in name doesn't allow a prefix -- + +sender.onStatus was called + *0 Object { + level = "error" + } +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: Underscores in name doesn't allow a prefix -- + + +-- start test: Case sensitivity -- + +sender.send("ELSEWhere", "test", *0 []): true +sender.send("LOCalHOST:ElseWhere", "test", *0 []): true + +-- end frame: Case sensitivity -- + +sender.onStatus was called + *0 Object { + level = "status" + } +receiver.test was called with 0 argument +sender.onStatus was called + *0 Object { + level = "status" + } +receiver.test was called with 0 argument + +-- end test: Case sensitivity -- + + +-- start test: Calling an AVM2 movie -- + +sender.send("avm2_child", "test", *0 []): true + +-- end frame: Calling an AVM2 movie -- + +sender.onStatus was called + *0 Object { + level = "error" + } + +-- end test: Calling an AVM2 movie -- + + +-- start test: Calling an AVM1 movie -- + +sender.send("avm1_child", "test", *0 []): true + +-- end frame: Calling an AVM1 movie -- + +sender.onStatus was called + *0 Object { + level = "status" + } +avm1_child.test was called with 0 argument + +-- end test: Calling an AVM1 movie -- + + +-- start test: Argument translations: primitives -- + +sender.send("avm1_child", "test", *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ]): true +sender.send("avm2_child", "test", *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ]): true +sender.send("_channel", "test", *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ]): true + +-- end frame: Argument translations: primitives -- + +sender.onStatus was called + *0 Object { + level = "status" + } +avm1_child.test was called with 7 arguments + *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ] +sender.onStatus was called + *0 Object { + level = "error" + } +sender.onStatus was called + *0 Object { + level = "status" + } +custom.test was called with 7 arguments + *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ] + +-- end test: Argument translations: primitives -- + + +-- start test: Argument translations: simple object -- + +sender.send("avm1_child", "test", *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ]): true +sender.send("avm2_child", "test", *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ]): true +sender.send("_channel", "test", *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ]): true + +-- end frame: Argument translations: simple object -- + +sender.onStatus was called + *0 Object { + level = "status" + } +avm1_child.test was called with 1 arguments + *0 [ + *1 object { + nested = *2 object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ] +sender.onStatus was called + *0 Object { + level = "error" + } +sender.onStatus was called + *0 Object { + level = "status" + } +custom.test was called with 1 arguments + *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ] + +-- end test: Argument translations: simple object -- + + +-- start test: AVM1 movie throws an error -- + +sender.send("avm1_child", "throwAnError", *0 []): true + +-- end frame: AVM1 movie throws an error -- + +sender.onStatus was called + *0 Object { + level = "status" + } +avm1_child.throwAnError was called + +-- end test: AVM1 movie throws an error -- + +Finished after 117 frames diff --git a/tests/tests/swfs/avm1/localconnection/test.fla b/tests/tests/swfs/avm1/localconnection/test.fla new file mode 100644 index 000000000000..28bb4fe503b5 Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection/test.fla differ diff --git a/tests/tests/swfs/avm1/localconnection/test.swf b/tests/tests/swfs/avm1/localconnection/test.swf new file mode 100644 index 000000000000..d081e4b962ca Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection/test.swf differ diff --git a/tests/tests/swfs/avm1/localconnection/test.toml b/tests/tests/swfs/avm1/localconnection/test.toml new file mode 100644 index 000000000000..c5248f99d2fb --- /dev/null +++ b/tests/tests/swfs/avm1/localconnection/test.toml @@ -0,0 +1 @@ +num_ticks = 300 # Test may finish in less, but it'll `fscommand:exit` when it's done. diff --git a/tests/tests/swfs/avm1/localconnection_properties/output.txt b/tests/tests/swfs/avm1/localconnection_properties/output.txt new file mode 100644 index 000000000000..32526b31359d --- /dev/null +++ b/tests/tests/swfs/avm1/localconnection_properties/output.txt @@ -0,0 +1,8 @@ +/// Enumeration + +/// Known Properties +domain: DONT_ENUM | DONT_DELETE +connect: DONT_ENUM | DONT_DELETE +close: DONT_ENUM | DONT_DELETE +isPerUser NOT FOUND +send: DONT_ENUM | DONT_DELETE diff --git a/tests/tests/swfs/avm1/localconnection_properties/test.fla b/tests/tests/swfs/avm1/localconnection_properties/test.fla new file mode 100644 index 000000000000..86d13cf9cd5f Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection_properties/test.fla differ diff --git a/tests/tests/swfs/avm1/localconnection_properties/test.swf b/tests/tests/swfs/avm1/localconnection_properties/test.swf new file mode 100644 index 000000000000..ad4cbdebb635 Binary files /dev/null and b/tests/tests/swfs/avm1/localconnection_properties/test.swf differ diff --git a/tests/tests/swfs/avm1/localconnection_properties/test.toml b/tests/tests/swfs/avm1/localconnection_properties/test.toml new file mode 100644 index 000000000000..dbee897f5863 --- /dev/null +++ b/tests/tests/swfs/avm1/localconnection_properties/test.toml @@ -0,0 +1 @@ +num_frames = 1 diff --git a/tests/tests/swfs/avm2/localconnection/CustomLocalConnection.as b/tests/tests/swfs/avm2/localconnection/CustomLocalConnection.as new file mode 100644 index 000000000000..4c2d4d3f230a --- /dev/null +++ b/tests/tests/swfs/avm2/localconnection/CustomLocalConnection.as @@ -0,0 +1,26 @@ +package { + import flash.net.LocalConnection; + + public class CustomLocalConnection extends LocalConnection { + private var main: Test; + + public function CustomLocalConnection(main: Test) { + super(); + this.main = main; + } + + public function test() { + trace("custom.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s")); + if (arguments.length > 0) { + trace(" " + main.repr(arguments)); + } + } + + public function throwAnError() { + trace("custom.throwAnError was called"); + //throw "aah!"; // [NA] this crashes every Flash Player I've tried + //throw {}; // [NA] this causes an error when constructing the AsyncErrorEvent + //throw new Error("aaah!"); + } + } +} diff --git a/tests/tests/swfs/avm2/localconnection/Test.as b/tests/tests/swfs/avm2/localconnection/Test.as new file mode 100644 index 000000000000..11ac9627c564 --- /dev/null +++ b/tests/tests/swfs/avm2/localconnection/Test.as @@ -0,0 +1,449 @@ +package { + + import flash.display.MovieClip; + import flash.net.LocalConnection; + import flash.utils.getQualifiedClassName; + import flash.events.AsyncErrorEvent; + import flash.events.Event; + import flash.events.SecurityErrorEvent; + import flash.events.StatusEvent; + import flash.system.fscommand; + import flash.net.URLRequest; + import flash.display.Loader; + + + public class Test extends MovieClip { + // Just a safe value, to make things easier... + // Sometimes Flash isn't super consistant about things being done on the same frame + const TICKS_PER_TEST: uint = 3; + + var receiver: LocalConnection = new LocalConnection(); + var sender: LocalConnection = new LocalConnection(); + var custom: LocalConnection; + var recvObject: Object = {}; + var tests: Array = []; + var currentTest = null; + var frameNum: uint = 0; + var totalFrameNum: uint = 0; + var funkyNames = [null, 0, "", " ??? "]; + var protectedFunctions = ["send", "connect", "close", "allowDomain", "allowInsecureDomain", "domain"]; + + public function Test() { + custom = new CustomLocalConnection(this); + + loadMovie("avm2child/child.swf"); + loadMovie("avm1child/child.swf"); + + trace("LocalConnection.isSupported: " + repr(LocalConnection.isSupported)); + trace(""); + + recvObject.test = createTestFunction("recvObject.test"); + + setupEvents(receiver); + setupEvents(sender); + setupEvents(custom); + + addTest("A message to nowhere!", function() { + send(sender, "nowhere", "test"); + }); + + addTest("Both receivers try to connect to the same channel", function() { + connect(receiver, "channel"); + connect(custom, "channel"); + }); + + addTest("A message to an unimplemented function", function() { + send(sender, "channel", "unimplemented"); + }); + + addTest("Receiver tries to connect elsewhere, but can't", function() { + connect(receiver, "elsewhere"); + }); + + addTest("Receiver actually connects elsewhere, and custom is allowed to connect to channel", function() { + close(receiver); + connect(receiver, "elsewhere"); + connect(custom, "channel"); + }); + + addTest("Sender calls test() on 'channel'", function() { + send(sender, "channel", "test"); + }); + + addTest("Client is used", function() { + receiver.client = recvObject; + send(sender, "elsewhere", "test"); + }); + + addTest("Sender calls test() on 'channel'... after the listener is gone", function() { + close(custom); + send(sender, "channel", "test"); + }); + + addTest("Sender calls test() on 'elsewhere'... immediately before the listener is gone", function() { + send(sender, "elsewhere", "test"); + close(receiver); + }); + + addTest("Sender calls test() on 'channel'... before the listener connects", function() { + send(sender, "channel", "test"); + connect(custom, "channel"); + }); + + addTest("Sending to a channel that gets reassigned before end-of-frame", function() { + send(sender, "channel", "test"); + close(custom); + connect(receiver, "channel"); + }); + + addTest("Channels reconnect and receive", function() { + close(custom); + close(receiver); + connect(receiver, "elsewhere"); + send(sender, "channel", "test"); + send(sender, "elsewhere", "test"); + connect(custom, "channel"); + }); + + addTest("A connected listener can also send", function() { + send(receiver, "channel", "test"); + send(receiver, "elsewhere", "test"); + }); + + addTest("A listener throws an error", function() { + // Fun fact: you can crash Flash Player if the thing thrown isn't an object + send(sender, "channel", "throwAnError"); + }); + + addTest("Close something's that's already closed", function() { + sender.close(); + }); + + addTest("Send to funky channel names", function() { + for (var i = 0; i < funkyNames.length; i++) { + send(sender, funkyNames[i], "test"); + } + }); + + addTest("Send to funky methods", function() { + for (var i = 0; i < funkyNames.length; i++) { + send(sender, "channel", funkyNames[i]); + } + }); + + addTest("Connect to funky names", function() { + for (var i = 0; i < funkyNames.length; i++) { + connect(sender, funkyNames[i]); + close(sender); + } + }); + + addTest("Connect to something with a prefix", function() { + connect(sender, "localhost:something"); + close(sender); + }); + + addTest("Send to protected methods", function() { + for (var i = 0; i < protectedFunctions.length; i++) { + send(sender, "channel", protectedFunctions[i]); + } + }); + + addTest("Arguments are sent", function() { + send(sender, "elsewhere", "test", 1, "two", {value: 3}, [4, 5]); + }); + + addTest("Explicit host prefix", function() { + send(sender, "localhost:channel", "test"); + send(sender, "notlocalhost:elsewhere", "test"); + }); + + addTest("Underscores in names", function() { + close(custom); + connect(custom, "_channel"); + send(sender, "_channel", "test"); + }); + + addTest("Underscores in name doesn't allow a prefix", function() { + send(sender, "localhost:channel", "test"); + send(sender, "localhost:_channel", "test"); + }); + + addTest("Case sensitivity", function() { + send(sender, "ELSEWhere", "test"); + send(sender, "LOCalHOST:ElseWhere", "test"); + }); + + addTest("Calling an AVM2 movie", function() { + send(sender, "avm2_child", "test"); + }); + + addTest("Calling an AVM1 movie", function() { + send(sender, "avm1_child", "test"); + }); + + addTest("Argument translations: primitives", function() { + sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", 1, 1.2, true, false, "string", null, undefined); + }); + + addTest("Argument translations: simple array", function() { + sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", [1,2, "three", 4.5, NaN, Infinity]); + }); + + addTest("Argument translations: simple object", function() { + sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", {"nested": {"numbers": [1,2], "string": "hello"}}); + }); + + // [NA] broken in ruffle at time of writing + //addTest("Argument translations: self referential object", function() { + // var obj = {}; + // obj.self = obj; + // obj.nested = {root: obj}; + // sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", obj); + //}); + + //addTest("Argument translations: self referential array", function() { + // var array = []; + // array.push(array); + // sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", array); + //}); + + //addTest("Argument translations: vector", function() { + //var vector = new Vector.(); + //vector.push("hello"); + //vector.push("world"); + //sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", vector); + //}); + + addTest("AVM1 movie throws an error", function() { + send(sender, "avm1_child", "throwAnError"); + }); + + addEventListener(Event.ENTER_FRAME, onEnterFrame); + } + + function loadMovie(path: String) { + var loader = new Loader(); + loader.load(new URLRequest(path)); + addChild(loader); + } + + function onEnterFrame(event: Event) { + totalFrameNum++; + + if (frameNum == TICKS_PER_TEST) { + trace(""); + trace("-- end test: " + currentTest[0] + " --"); + trace(""); + frameNum = 0; + return; // Allow any end-of-frame cleanup before next test + } + if (frameNum == 0) { + currentTest = tests.shift(); + if (currentTest != null) { + trace(""); + trace("-- start test: " + currentTest[0] + " --"); + trace(""); + try { + currentTest[1](); + } catch (e) { + trace("! test stopped with error: " + e); + } + trace(""); + trace("-- end frame: " + currentTest[0] + " --"); + trace(""); + } + } + if (currentTest == null) { + trace("Finished after " + totalFrameNum + " frames"); + fscommand("exit"); + removeEventListener(Event.ENTER_FRAME, onEnterFrame); + return; + } + frameNum++; + } + + function connect(lc: LocalConnection, name: String) { + var doing = repr(lc) + ".connect(" + repr(name) + ")"; + try { + lc.connect(name); + trace(doing); + } catch (e) { + trace(doing + ": ! " + e); + } + } + + function send(lc: LocalConnection, connectionName: String, methodName: String, ...args) { + var doing = repr(lc) + ".send(" + repr(connectionName) + ", " + repr(methodName) + ", " + repr(args) + ")"; + try { + args.unshift(methodName); + args.unshift(connectionName); + lc.send.apply(lc, args); + trace(doing); + } catch (e) { + trace(doing + ": ! " + e); + } + } + + function sendToMany(lc: LocalConnection, connectionNames: Array, methodName: String, ...args) { + args.unshift(methodName); + args.unshift(""); + args.unshift(lc); + for (var i = 0; i < connectionNames.length; i++) { + args[1] = connectionNames[i]; + send.apply(null, args); + } + } + + function close(lc: LocalConnection) { + var doing = repr(lc) + ".close()"; + try { + lc.close(); + trace(doing); + } catch (e) { + trace(doing + ": ! " + e); + } + } + + function addTest(name: String, fn: Function) { + tests.push([name, fn]); + } + + function createTestFunction(name: String) { + return function() { + trace(name + " was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s")); + if (arguments.length > 0) { + trace(" " + repr(arguments)); + } + } + } + + function setupEvents(lc: LocalConnection) { + var name: String = repr(lc); + lc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, function(event: AsyncErrorEvent) { + trace(name + " received event AsyncErrorEvent.ASYNC_ERROR"); + trace(" bubbles: " + repr(event.bubbles)); + trace(" cancelable: " + repr(event.cancelable)); + trace(" error: " + event.error); + trace(" currentTarget: " + repr(event.currentTarget)); + trace(" target: " + repr(event.target)); + trace(""); + }); + lc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function(event: SecurityErrorEvent) { + trace(name + " received event SecurityErrorEvent.SECURITY_ERROR"); + trace(" bubbles: " + repr(event.bubbles)); + trace(" cancelable: " + repr(event.cancelable)); + trace(" text: " + repr(event.text)); + trace(" currentTarget: " + repr(event.currentTarget)); + trace(" target: " + repr(event.target)); + trace(""); + }); + lc.addEventListener(StatusEvent.STATUS, function(event: StatusEvent) { + trace(name + " received event StatusEvent.STATUS"); + trace(" bubbles: " + repr(event.bubbles)); + trace(" cancelable: " + repr(event.cancelable)); + trace(" code: " + repr(event.code)); + trace(" currentTarget: " + repr(event.currentTarget)); + trace(" level: " + repr(event.level)); + trace(" target: " + repr(event.target)); + trace(""); + }); + } + + private function getObjectId(needle: Object, haystack: Array): String { + for (var i = 0; i < haystack.length; i++) { + if (haystack[i] === needle) { + return i; + } + } + return null; + } + + public function repr(value: *, indent: String = " ", seenObjects: Array = null) { + if (seenObjects == null) { + seenObjects = []; + } + if (value === receiver) { + return "receiver"; + } else if (value === sender) { + return "sender"; + } else if (value === recvObject) { + return "recvObject"; + } else if (value === custom) { + return "custom"; + } + + if (value === undefined || value === null || value === true || value === false || value is Number) { + return String(value); + } else if (value is String) { + return escapeString(value); + } else { + var existingId = getObjectId(value, seenObjects); + if (existingId != null) { + return "*" + existingId; + } + existingId = seenObjects.length; + seenObjects.push(value); + if (value is Array) { + if (value.length == 0) { + return "*" + existingId + " []"; + } else { + var result = "*" + existingId + " [\n"; + var nextIndent = indent + " "; + for (var i = 0; i < value.length; i++) { + result += nextIndent + repr(value[i], nextIndent, seenObjects) + "\n"; + } + return result + indent + "]"; + } + } else { + var keys = []; + for (var key in value) { + keys.push(key); + } + keys.sort(); + + var result = "*" + existingId + " " + getQualifiedClassName(value) + " {"; + + if (keys.length == 0) { + return result + "}"; + } else { + result += "\n"; + var nextIndent = indent + " "; + for (var i = 0; i < keys.length; i++) { + result += nextIndent + keys[i] + " = " + repr(value[keys[i]], nextIndent, seenObjects) + "\n"; + } + return result + indent + "}"; + } + } + } + } + + public function escapeString(input: String): String { + var output:String = "\""; + for (var i:int = 0; i < input.length; i++) { + var char:String = input.charAt(i); + switch (char) { + case "\\": + output += "\\\\"; + break; + case "\"": + output += "\\\""; + break; + case "\n": + output += "\\n"; + break; + case "\r": + output += "\\r"; + break; + case "\t": + output += "\\t"; + break; + default: + output += char; + } + } + return output + "\""; + } + } + +} diff --git a/tests/tests/swfs/avm2/localconnection/avm1child/child.fla b/tests/tests/swfs/avm2/localconnection/avm1child/child.fla new file mode 100644 index 000000000000..af61bd7a8ce8 Binary files /dev/null and b/tests/tests/swfs/avm2/localconnection/avm1child/child.fla differ diff --git a/tests/tests/swfs/avm2/localconnection/avm1child/child.swf b/tests/tests/swfs/avm2/localconnection/avm1child/child.swf new file mode 100644 index 000000000000..ac2066ecc545 Binary files /dev/null and b/tests/tests/swfs/avm2/localconnection/avm1child/child.swf differ diff --git a/tests/tests/swfs/avm2/localconnection/avm2child/Child.as b/tests/tests/swfs/avm2/localconnection/avm2child/Child.as new file mode 100644 index 000000000000..cba2a0fcbb50 --- /dev/null +++ b/tests/tests/swfs/avm2/localconnection/avm2child/Child.as @@ -0,0 +1,111 @@ +package { + + import flash.display.MovieClip; + import flash.net.LocalConnection; + import flash.utils.getQualifiedClassName; + + + public class Child extends MovieClip { + var lc: LocalConnection = new LocalConnection(); + + public function Child() { + lc.connect("avm2_child"); + lc.client = {}; + lc.client.test = function() { + trace("avm2_child.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s")); + if (arguments.length > 0) { + trace(" " + repr(arguments)); + } + } + } + + private function getObjectId(needle: Object, haystack: Array): String { + for (var i = 0; i < haystack.length; i++) { + if (haystack[i] === needle) { + return i; + } + } + return null; + } + + public function repr(value: *, indent: String = " ", seenObjects: Array = null) { + if (seenObjects == null) { + seenObjects = []; + } + if (value === lc) { + return "lc"; + } + + if (value === undefined || value === null || value === true || value === false || value is Number) { + return String(value); + } else if (value is String) { + return escapeString(value); + } else { + var existingId = getObjectId(value, seenObjects); + if (existingId != null) { + return "*" + existingId; + } + existingId = seenObjects.length; + seenObjects.push(value); + if (value is Array) { + if (value.length == 0) { + return "*" + existingId + " []"; + } else { + var result = "*" + existingId + " [\n"; + var nextIndent = indent + " "; + for (var i = 0; i < value.length; i++) { + result += nextIndent + repr(value[i], nextIndent, seenObjects) + "\n"; + } + return result + indent + "]"; + } + } else { + var keys = []; + for (var key in value) { + keys.push(key); + } + keys.sort(); + + var result = "*" + existingId + " " + getQualifiedClassName(value) + " {"; + + if (keys.length == 0) { + return result + "}"; + } else { + result += "\n"; + var nextIndent = indent + " "; + for (var i = 0; i < keys.length; i++) { + result += nextIndent + keys[i] + " = " + repr(value[keys[i]], nextIndent, seenObjects) + "\n"; + } + return result + indent + "}"; + } + } + } + } + + public function escapeString(input: String): String { + var output:String = "\""; + for (var i:int = 0; i < input.length; i++) { + var char:String = input.charAt(i); + switch (char) { + case "\\": + output += "\\\\"; + break; + case "\"": + output += "\\\""; + break; + case "\n": + output += "\\n"; + break; + case "\r": + output += "\\r"; + break; + case "\t": + output += "\\t"; + break; + default: + output += char; + } + } + return output + "\""; + } + } +} diff --git a/tests/tests/swfs/avm2/localconnection/avm2child/child.fla b/tests/tests/swfs/avm2/localconnection/avm2child/child.fla new file mode 100644 index 000000000000..39cd99bd3d5e Binary files /dev/null and b/tests/tests/swfs/avm2/localconnection/avm2child/child.fla differ diff --git a/tests/tests/swfs/avm2/localconnection/avm2child/child.swf b/tests/tests/swfs/avm2/localconnection/avm2child/child.swf new file mode 100644 index 000000000000..30d9aaced1cb Binary files /dev/null and b/tests/tests/swfs/avm2/localconnection/avm2child/child.swf differ diff --git a/tests/tests/swfs/avm2/localconnection/output.txt b/tests/tests/swfs/avm2/localconnection/output.txt new file mode 100644 index 000000000000..e88e11e2e8c5 --- /dev/null +++ b/tests/tests/swfs/avm2/localconnection/output.txt @@ -0,0 +1,890 @@ +LocalConnection.isSupported: true + + +-- start test: A message to nowhere! -- + +sender.send("nowhere", "test", *0 []) + +-- end frame: A message to nowhere! -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + + +-- end test: A message to nowhere! -- + + +-- start test: Both receivers try to connect to the same channel -- + +receiver.connect("channel") +custom.connect("channel"): ! ArgumentError: Error #2082: Connect failed because the object is already connected. + +-- end frame: Both receivers try to connect to the same channel -- + + +-- end test: Both receivers try to connect to the same channel -- + + +-- start test: A message to an unimplemented function -- + +sender.send("channel", "unimplemented", *0 []) + +-- end frame: A message to an unimplemented function -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +receiver received event AsyncErrorEvent.ASYNC_ERROR + bubbles: false + cancelable: false + error: ReferenceError: Error #1069: Property unimplemented not found on flash.net.LocalConnection and there is no default value. + currentTarget: receiver + target: receiver + + +-- end test: A message to an unimplemented function -- + + +-- start test: Receiver tries to connect elsewhere, but can't -- + +receiver.connect("elsewhere"): ! ArgumentError: Error #2082: Connect failed because the object is already connected. + +-- end frame: Receiver tries to connect elsewhere, but can't -- + + +-- end test: Receiver tries to connect elsewhere, but can't -- + + +-- start test: Receiver actually connects elsewhere, and custom is allowed to connect to channel -- + +receiver.close() +receiver.connect("elsewhere") +custom.connect("channel") + +-- end frame: Receiver actually connects elsewhere, and custom is allowed to connect to channel -- + + +-- end test: Receiver actually connects elsewhere, and custom is allowed to connect to channel -- + + +-- start test: Sender calls test() on 'channel' -- + +sender.send("channel", "test", *0 []) + +-- end frame: Sender calls test() on 'channel' -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom.test was called with 0 argument + +-- end test: Sender calls test() on 'channel' -- + + +-- start test: Client is used -- + +sender.send("elsewhere", "test", *0 []) + +-- end frame: Client is used -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +recvObject.test was called with 0 argument + +-- end test: Client is used -- + + +-- start test: Sender calls test() on 'channel'... after the listener is gone -- + +custom.close() +sender.send("channel", "test", *0 []) + +-- end frame: Sender calls test() on 'channel'... after the listener is gone -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + + +-- end test: Sender calls test() on 'channel'... after the listener is gone -- + + +-- start test: Sender calls test() on 'elsewhere'... immediately before the listener is gone -- + +sender.send("elsewhere", "test", *0 []) +receiver.close() + +-- end frame: Sender calls test() on 'elsewhere'... immediately before the listener is gone -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + + +-- end test: Sender calls test() on 'elsewhere'... immediately before the listener is gone -- + + +-- start test: Sender calls test() on 'channel'... before the listener connects -- + +sender.send("channel", "test", *0 []) +custom.connect("channel") + +-- end frame: Sender calls test() on 'channel'... before the listener connects -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + + +-- end test: Sender calls test() on 'channel'... before the listener connects -- + + +-- start test: Sending to a channel that gets reassigned before end-of-frame -- + +sender.send("channel", "test", *0 []) +custom.close() +receiver.connect("channel") + +-- end frame: Sending to a channel that gets reassigned before end-of-frame -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +recvObject.test was called with 0 argument + +-- end test: Sending to a channel that gets reassigned before end-of-frame -- + + +-- start test: Channels reconnect and receive -- + +custom.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected. +receiver.close() +receiver.connect("elsewhere") +sender.send("channel", "test", *0 []) +sender.send("elsewhere", "test", *0 []) +custom.connect("channel") + +-- end frame: Channels reconnect and receive -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +recvObject.test was called with 0 argument + +-- end test: Channels reconnect and receive -- + + +-- start test: A connected listener can also send -- + +receiver.send("channel", "test", *0 []) +receiver.send("elsewhere", "test", *0 []) + +-- end frame: A connected listener can also send -- + +receiver received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: receiver + level: "status" + target: receiver + +custom.test was called with 0 argument +receiver received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: receiver + level: "status" + target: receiver + +recvObject.test was called with 0 argument + +-- end test: A connected listener can also send -- + + +-- start test: A listener throws an error -- + +sender.send("channel", "throwAnError", *0 []) + +-- end frame: A listener throws an error -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom.throwAnError was called + +-- end test: A listener throws an error -- + + +-- start test: Close something's that's already closed -- + +! test stopped with error: ArgumentError: Error #2083: Close failed because the object is not connected. + +-- end frame: Close something's that's already closed -- + + +-- end test: Close something's that's already closed -- + + +-- start test: Send to funky channel names -- + +sender.send(null, "test", *0 []): ! TypeError: Error #2007: Parameter connectionName must be non-null. +sender.send("0", "test", *0 []) +sender.send("", "test", *0 []): ! ArgumentError: Error #2085: Parameter connectionName must be non-empty string. +sender.send(" ??? ", "test", *0 []) + +-- end frame: Send to funky channel names -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + + +-- end test: Send to funky channel names -- + + +-- start test: Send to funky methods -- + +sender.send("channel", null, *0 []): ! TypeError: Error #2007: Parameter methodName must be non-null. +sender.send("channel", "0", *0 []) +sender.send("channel", "", *0 []): ! ArgumentError: Error #2085: Parameter methodName must be non-empty string. +sender.send("channel", " ??? ", *0 []) + +-- end frame: Send to funky methods -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom received event AsyncErrorEvent.ASYNC_ERROR + bubbles: false + cancelable: false + error: ReferenceError: Error #1069: Property 0 not found on CustomLocalConnection and there is no default value. + currentTarget: custom + target: custom + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom received event AsyncErrorEvent.ASYNC_ERROR + bubbles: false + cancelable: false + error: ReferenceError: Error #1069: Property ??? not found on CustomLocalConnection and there is no default value. + currentTarget: custom + target: custom + + +-- end test: Send to funky methods -- + + +-- start test: Connect to funky names -- + +sender.connect(null): ! TypeError: Error #2007: Parameter connectionName must be non-null. +sender.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected. +sender.connect("0") +sender.close() +sender.connect(""): ! ArgumentError: Error #2085: Parameter connectionName must be non-empty string. +sender.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected. +sender.connect(" ??? ") +sender.close() + +-- end frame: Connect to funky names -- + + +-- end test: Connect to funky names -- + + +-- start test: Connect to something with a prefix -- + +sender.connect("localhost:something"): ! ArgumentError: Error #2004: One of the parameters is invalid. +sender.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected. + +-- end frame: Connect to something with a prefix -- + + +-- end test: Connect to something with a prefix -- + + +-- start test: Send to protected methods -- + +sender.send("channel", "send", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid. +sender.send("channel", "connect", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid. +sender.send("channel", "close", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid. +sender.send("channel", "allowDomain", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid. +sender.send("channel", "allowInsecureDomain", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid. +sender.send("channel", "domain", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid. + +-- end frame: Send to protected methods -- + + +-- end test: Send to protected methods -- + + +-- start test: Arguments are sent -- + +sender.send("elsewhere", "test", *0 [ + 1 + "two" + *1 Object { + value = 3 + } + *2 [ + 4 + 5 + ] + ]) + +-- end frame: Arguments are sent -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +recvObject.test was called with 4 arguments + *0 [ + 1 + "two" + *1 Object { + value = 3 + } + *2 [ + 4 + 5 + ] + ] + +-- end test: Arguments are sent -- + + +-- start test: Explicit host prefix -- + +sender.send("localhost:channel", "test", *0 []) +sender.send("notlocalhost:elsewhere", "test", *0 []) + +-- end frame: Explicit host prefix -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom.test was called with 0 argument +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + + +-- end test: Explicit host prefix -- + + +-- start test: Underscores in names -- + +custom.close() +custom.connect("_channel") +sender.send("_channel", "test", *0 []) + +-- end frame: Underscores in names -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom.test was called with 0 argument + +-- end test: Underscores in names -- + + +-- start test: Underscores in name doesn't allow a prefix -- + +sender.send("localhost:channel", "test", *0 []) +sender.send("localhost:_channel", "test", *0 []) + +-- end frame: Underscores in name doesn't allow a prefix -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "error" + target: sender + + +-- end test: Underscores in name doesn't allow a prefix -- + + +-- start test: Case sensitivity -- + +sender.send("ELSEWhere", "test", *0 []) +sender.send("LOCalHOST:ElseWhere", "test", *0 []) + +-- end frame: Case sensitivity -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +recvObject.test was called with 0 argument +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +recvObject.test was called with 0 argument + +-- end test: Case sensitivity -- + + +-- start test: Calling an AVM2 movie -- + +sender.send("avm2_child", "test", *0 []) + +-- end frame: Calling an AVM2 movie -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm2_child.test was called with 0 argument + +-- end test: Calling an AVM2 movie -- + + +-- start test: Calling an AVM1 movie -- + +sender.send("avm1_child", "test", *0 []) + +-- end frame: Calling an AVM1 movie -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm1_child.test was called with 0 argument + +-- end test: Calling an AVM1 movie -- + + +-- start test: Argument translations: primitives -- + +sender.send("avm1_child", "test", *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ]) +sender.send("avm2_child", "test", *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ]) +sender.send("_channel", "test", *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ]) + +-- end frame: Argument translations: primitives -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm1_child.test was called with 7 arguments + *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ] +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm2_child.test was called with 7 arguments + *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ] +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom.test was called with 7 arguments + *0 [ + 1 + 1.2 + true + false + "string" + null + undefined + ] + +-- end test: Argument translations: primitives -- + + +-- start test: Argument translations: simple array -- + +sender.send("avm1_child", "test", *0 [ + *1 [ + 1 + 2 + "three" + 4.5 + NaN + Infinity + ] + ]) +sender.send("avm2_child", "test", *0 [ + *1 [ + 1 + 2 + "three" + 4.5 + NaN + Infinity + ] + ]) +sender.send("_channel", "test", *0 [ + *1 [ + 1 + 2 + "three" + 4.5 + NaN + Infinity + ] + ]) + +-- end frame: Argument translations: simple array -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm1_child.test was called with 1 arguments + *0 [ + *1 [ + 1 + 2 + "three" + 4.5 + NaN + Infinity + ] + ] +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm2_child.test was called with 1 arguments + *0 [ + *1 [ + 1 + 2 + "three" + 4.5 + NaN + Infinity + ] + ] +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom.test was called with 1 arguments + *0 [ + *1 [ + 1 + 2 + "three" + 4.5 + NaN + Infinity + ] + ] + +-- end test: Argument translations: simple array -- + + +-- start test: Argument translations: simple object -- + +sender.send("avm1_child", "test", *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ]) +sender.send("avm2_child", "test", *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ]) +sender.send("_channel", "test", *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ]) + +-- end frame: Argument translations: simple object -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm1_child.test was called with 1 arguments + *0 [ + *1 object { + nested = *2 object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ] +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm2_child.test was called with 1 arguments + *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ] +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +custom.test was called with 1 arguments + *0 [ + *1 Object { + nested = *2 Object { + numbers = *3 [ + 1 + 2 + ] + string = "hello" + } + } + ] + +-- end test: Argument translations: simple object -- + + +-- start test: AVM1 movie throws an error -- + +sender.send("avm1_child", "throwAnError", *0 []) + +-- end frame: AVM1 movie throws an error -- + +sender received event StatusEvent.STATUS + bubbles: false + cancelable: false + code: null + currentTarget: sender + level: "status" + target: sender + +avm1_child.throwAnError was called + +-- end test: AVM1 movie throws an error -- + +Finished after 125 frames diff --git a/tests/tests/swfs/avm2/localconnection/test.fla b/tests/tests/swfs/avm2/localconnection/test.fla new file mode 100644 index 000000000000..fd91662c6289 Binary files /dev/null and b/tests/tests/swfs/avm2/localconnection/test.fla differ diff --git a/tests/tests/swfs/avm2/localconnection/test.swf b/tests/tests/swfs/avm2/localconnection/test.swf new file mode 100644 index 000000000000..eeff5ca01f96 Binary files /dev/null and b/tests/tests/swfs/avm2/localconnection/test.swf differ diff --git a/tests/tests/swfs/avm2/localconnection/test.toml b/tests/tests/swfs/avm2/localconnection/test.toml new file mode 100644 index 000000000000..c5248f99d2fb --- /dev/null +++ b/tests/tests/swfs/avm2/localconnection/test.toml @@ -0,0 +1 @@ +num_ticks = 300 # Test may finish in less, but it'll `fscommand:exit` when it's done.