Skip to content

Commit

Permalink
core: Implement LocalConnection
Browse files Browse the repository at this point in the history
  • Loading branch information
Dinnerbone committed Jun 19, 2024
1 parent 0a2528b commit 4d12e0e
Show file tree
Hide file tree
Showing 36 changed files with 2,782 additions and 155 deletions.
2 changes: 1 addition & 1 deletion core/src/avm1/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
234 changes: 217 additions & 17 deletions core/src/avm1/globals/local_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<LocalConnectionHandle>>,
}

#[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<Self> {
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<AmfValue>,
) -> 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>(
Expand All @@ -18,29 +143,104 @@ pub fn domain<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, 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<Value<'gc>, 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<Value<'gc>, 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<Value<'gc>, 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<Value<'gc>, Error<'gc>> {
this.set_native(
activation.gc(),
NativeObject::LocalConnection(LocalConnection(Gc::new(
activation.gc(),
LocalConnectionData {
handle: RefCell::new(None),
},
))),
);
Ok(this.into())
}

Expand Down
2 changes: 2 additions & 0 deletions core/src/avm1/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
31 changes: 18 additions & 13 deletions core/src/avm2/amf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
17 changes: 17 additions & 0 deletions core/src/avm2/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
3 changes: 3 additions & 0 deletions core/src/avm2/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -293,6 +294,7 @@ impl<'gc> SystemClasses<'gc> {
netstatusevent: object,
shaderfilter: object,
statusevent: object,
asyncerrorevent: object,
contextmenuevent: object,
filereference: object,
filefilter: object,
Expand Down Expand Up @@ -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),
Expand Down
16 changes: 1 addition & 15 deletions core/src/avm2/globals/flash/net/LocalConnection.as
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading

0 comments on commit 4d12e0e

Please sign in to comment.