avm2: Implement some of SharedObject

Our AVM2 `SharedObject` support is now *almost* equivalent
to our avm1 `SharedObject` support. We implement serialization
and deserialization for primitives, arrays, and `Object` instances
with local properties. We also implement serialization for `Date`,
but not `Xml` (since our AVM2 `Xml` class is just a stub at the moment).

This is enough to make 'This is the only level too' save level
progress to disk.

Currently, we always serialize to AMF3. When we implement
the `defaultObjectEncoding` and `objectEncoding`, we'll need
to adjust this.
This commit is contained in:
Aaron Hill 2022-08-28 15:45:10 -05:00
parent ca701c4aeb
commit bb138d9082
17 changed files with 711 additions and 101 deletions

1
Cargo.lock generated
View File

@ -3047,6 +3047,7 @@ dependencies = [
"dasp",
"downcast-rs",
"encoding_rs",
"enumset",
"flash-lso",
"flate2",
"fnv",

View File

@ -42,6 +42,7 @@ flash-lso = { git = "https://github.com/ruffle-rs/rust-flash-lso", rev = "19fecd
lzma-rs = {version = "0.2.0", optional = true }
dasp = { git = "https://github.com/RustAudio/dasp", rev = "f05a703", features = ["interpolate", "interpolate-linear", "signal"], optional = true }
symphonia = { version = "0.5.1", default-features = false, features = ["mp3"], optional = true }
enumset = "1.0.11"
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
version = "0.3.24"

View File

@ -337,7 +337,7 @@ pub fn get_local<'gc>(
}
// Check if this is referencing an existing shared object
if let Some(so) = activation.context.shared_objects.get(&full_name) {
if let Some(so) = activation.context.avm1_shared_objects.get(&full_name) {
return Ok((*so).into());
}
@ -380,7 +380,10 @@ pub fn get_local<'gc>(
Attribute::DONT_DELETE,
);
activation.context.shared_objects.insert(full_name, this);
activation
.context
.avm1_shared_objects
.insert(full_name, this);
Ok(this.into())
}

View File

@ -23,6 +23,7 @@ macro_rules! avm_debug {
}
pub mod activation;
mod amf;
mod array;
pub mod bytearray;
mod call_stack;
@ -30,7 +31,7 @@ mod class;
mod domain;
mod events;
mod function;
mod globals;
pub mod globals;
mod method;
mod multiname;
mod namespace;

239
core/src/avm2/amf.rs Normal file
View File

@ -0,0 +1,239 @@
use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::object::{ByteArrayObject, TObject};
use crate::avm2::ArrayObject;
use crate::avm2::ArrayStorage;
use crate::avm2::Multiname;
use crate::avm2::Namespace;
use crate::avm2::{Activation, Error, Object, QName, Value};
use crate::string::AvmString;
use enumset::EnumSet;
use flash_lso::types::{Attribute, ClassDefinition, Value as AmfValue};
use flash_lso::types::{Element, Lso};
/// Serialize a Value to an AmfValue
fn serialize_value<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
elem: Value<'gc>,
) -> Option<AmfValue> {
match elem {
Value::Undefined => Some(AmfValue::Undefined),
Value::Null => Some(AmfValue::Null),
Value::Bool(b) => Some(AmfValue::Bool(b)),
Value::Number(f) => Some(AmfValue::Number(f)),
Value::Integer(num) => {
// NOTE - we should really be converting `Value::Integer` to `Value::Number`
// whenever it's outside this range, instead of performing this during AMF serialization.
if num >= (1 << 28) || num < -(1 << 28) {
Some(AmfValue::Number(num as f64))
} else {
Some(AmfValue::Integer(num))
}
}
Value::String(s) => Some(AmfValue::String(s.to_string())),
Value::Object(o) => {
// TODO: Find a more general rule for which object types should be skipped,
// and which turn into undefined.
if o.as_executable().is_some() {
None
} else if o.as_display_object().is_some() {
Some(AmfValue::Undefined)
} else if let Some(array) = o.as_array_storage() {
let mut values = Vec::new();
recursive_serialize(activation, o, &mut values).unwrap();
let mut dense = vec![];
let mut sparse = vec![];
for (i, elem) in (0..array.length()).zip(values.into_iter()) {
if elem.name == i.to_string() {
dense.push(elem.value.clone());
} else {
sparse.push(elem);
}
}
if sparse.is_empty() {
Some(AmfValue::StrictArray(dense))
} else {
let len = sparse.len() as u32;
Some(AmfValue::ECMAArray(dense, sparse, len))
}
} else if let Some(date) = o.as_date_object() {
date.date_time()
.map(|date_time| AmfValue::Date(date_time.timestamp_millis() as f64, None))
} else {
let is_object = o
.instance_of()
.map_or(false, |c| c == activation.avm2().classes().object);
if is_object {
let mut object_body = Vec::new();
recursive_serialize(activation, o, &mut object_body).unwrap();
Some(AmfValue::Object(
object_body,
Some(ClassDefinition {
name: "".to_string(),
attributes: EnumSet::only(Attribute::Dynamic),
static_properties: Vec::new(),
}),
))
} else {
log::warn!(
"Serialization is not implemented for class other than Object: {:?}",
o
);
None
}
}
}
}
}
/// Serialize an Object and any children to a AMF object
pub fn recursive_serialize<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
obj: Object<'gc>,
elements: &mut Vec<Element>,
) -> Result<(), Error> {
let mut last_index = obj.get_next_enumerant(0, activation)?;
while let Some(index) = last_index {
let name = obj
.get_enumerant_name(index, activation)?
.coerce_to_string(activation)?;
let value = obj.get_property(&Multiname::public(name), activation)?;
if let Some(value) = serialize_value(activation, value) {
elements.push(Element::new(name.to_utf8_lossy(), value));
}
last_index = obj.get_next_enumerant(index, activation)?;
}
Ok(())
}
/// Deserialize a AmfValue to a Value
pub fn deserialize_value<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
val: &AmfValue,
) -> Result<Value<'gc>, Error> {
Ok(match val {
AmfValue::Null => Value::Null,
AmfValue::Undefined => Value::Undefined,
AmfValue::Number(f) => (*f).into(),
AmfValue::Integer(num) => (*num).into(),
AmfValue::String(s) => Value::String(AvmString::new_utf8(activation.context.gc_context, s)),
AmfValue::Bool(b) => (*b).into(),
AmfValue::ByteArray(bytes) => {
let storage = ByteArrayStorage::from_vec(bytes.clone());
let bytearray = ByteArrayObject::from_storage(activation, storage)?;
bytearray.into()
}
AmfValue::ECMAArray(values, elements, _) => {
// First let's create an array out of `values` (dense portion), then we add the elements onto it.
let mut arr: Vec<Option<Value<'gc>>> = Vec::with_capacity(values.len());
for value in values {
arr.push(Some(deserialize_value(activation, value)?));
}
let storage = ArrayStorage::from_storage(arr);
let mut array = ArrayObject::from_storage(activation, storage)?;
// Now let's add each element as a property
for element in elements {
array.set_property(
&QName::new(
Namespace::public(),
AvmString::new_utf8(activation.context.gc_context, element.name()),
)
.into(),
deserialize_value(activation, element.value())?,
activation,
)?;
}
array.into()
}
AmfValue::StrictArray(values) => {
let mut arr: Vec<Option<Value<'gc>>> = Vec::with_capacity(values.len());
for value in values {
arr.push(Some(deserialize_value(activation, value)?));
}
let storage = ArrayStorage::from_storage(arr);
let array = ArrayObject::from_storage(activation, storage)?;
array.into()
}
AmfValue::Object(elements, class) => {
if let Some(class) = class {
if !class.name.is_empty() && class.name != "Object" {
log::warn!("Deserializing class {:?} is not supported!", class);
}
}
let mut obj = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
for entry in elements {
let value = deserialize_value(activation, entry.value())?;
obj.set_property(
&Multiname::public(AvmString::new_utf8(
activation.context.gc_context,
entry.name(),
)),
value,
activation,
)?;
}
obj.into()
}
AmfValue::Date(time, _) => activation
.avm2()
.classes()
.date
.construct(activation, &[(*time).into()])?
.into(),
AmfValue::XML(content, _) => activation
.avm2()
.classes()
.xml
.construct(
activation,
&[Value::String(AvmString::new_utf8(
activation.context.gc_context,
content,
))],
)?
.into(),
AmfValue::VectorDouble(..)
| AmfValue::VectorUInt(..)
| AmfValue::VectorInt(..)
| AmfValue::VectorObject(..)
| AmfValue::Dictionary(..)
| AmfValue::Custom(..) => {
log::error!("Deserialization not yet implemented: {:?}", val);
Value::Undefined
}
AmfValue::AMF3(val) => deserialize_value(activation, val)?,
AmfValue::Unsupported => Value::Undefined,
})
}
/// Deserializes a Lso into an object containing the properties stored
pub fn deserialize_lso<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
lso: &Lso,
) -> Result<Object<'gc>, Error> {
let mut obj = activation
.avm2()
.classes()
.object
.construct(activation, &[])?;
for child in &lso.body {
obj.set_property(
&Multiname::public(AvmString::new_utf8(
activation.context.gc_context,
&child.name,
)),
deserialize_value(activation, child.value())?,
activation,
)?;
}
Ok(obj)
}

View File

@ -23,7 +23,7 @@ mod boolean;
mod class;
mod date;
mod error;
mod flash;
pub mod flash;
mod function;
mod global_scope;
mod int;

View File

@ -4,8 +4,14 @@ use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::object::TObject;
use crate::avm2::traits::Trait;
use crate::avm2::Multiname;
use crate::avm2::{Activation, Error, Namespace, Object, QName, Value};
use crate::display_object::DisplayObject;
use crate::display_object::TDisplayObject;
use crate::string::AvmString;
use flash_lso::types::{AMFVersion, Lso};
use gc_arena::{GcCell, MutationContext};
use std::borrow::Cow;
fn instance_init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
@ -39,28 +45,221 @@ fn class_init<'gc>(
Ok(Value::Undefined)
}
fn get_local<'gc>(
pub fn get_local<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
log::warn!("SharedObject.getLocal not implemented");
let class = activation.context.avm2.classes().sharedobject;
let new_shared_object = class.construct(activation, &[])?;
// TODO: It appears that Flash does some kind of escaping here:
// the name "foo\uD800" correspond to a file named "fooE#FB#FB#D.sol".
Ok(new_shared_object.into())
let name = args
.get(0)
.unwrap_or(&Value::Undefined)
.coerce_to_string(activation)?;
let name = name.to_utf8_lossy();
const INVALID_CHARS: &str = "~%&\\;:\"',<>?# ";
if name.contains(|c| INVALID_CHARS.contains(c)) {
log::error!("SharedObject::get_local: Invalid character in name");
return Ok(Value::Null);
}
let movie = if let DisplayObject::MovieClip(movie) = activation.context.stage.root_clip() {
movie
} else {
log::error!("SharedObject::get_local: Movie was None");
return Ok(Value::Null);
};
let mut movie_url = if let Some(url) = movie.movie().and_then(|m| m.url().map(|u| u.to_owned()))
{
if let Ok(url) = url::Url::parse(&url) {
url
} else {
log::error!("SharedObject::get_local: Unable to parse movie URL");
return Ok(Value::Null);
}
} else {
// No URL (loading local data). Use a dummy URL to allow SharedObjects to work.
url::Url::parse("file://localhost").unwrap()
};
movie_url.set_query(None);
movie_url.set_fragment(None);
let secure = args.get(2).unwrap_or(&Value::Undefined).coerce_to_boolean();
// Secure parameter disallows using the shared object from non-HTTPS.
if secure && movie_url.scheme() != "https" {
log::warn!(
"SharedObject.get_local: Tried to load a secure shared object from non-HTTPS origin"
);
return Ok(Value::Null);
}
// Shared objects are sandboxed per-domain.
// By default, they are keyed based on the SWF URL, but the `localHost` parameter can modify this path.
let mut movie_path = movie_url.path();
// Remove leading/trailing slashes.
movie_path = movie_path.strip_prefix('/').unwrap_or(movie_path);
movie_path = movie_path.strip_suffix('/').unwrap_or(movie_path);
let movie_host = if movie_url.scheme() == "file" {
// Remove drive letter on Windows (TODO: move this logic into DiskStorageBackend?)
if let [_, b':', b'/', ..] = movie_path.as_bytes() {
movie_path = &movie_path[3..];
}
"localhost"
} else {
movie_url.host_str().unwrap_or_default()
};
let local_path = if let Some(Value::String(local_path)) = args.get(1) {
// Empty local path always fails.
if local_path.is_empty() {
return Ok(Value::Null);
}
// Remove leading/trailing slashes.
let mut local_path = local_path.to_utf8_lossy();
if local_path.ends_with('/') {
match &mut local_path {
Cow::Owned(p) => {
p.pop();
}
Cow::Borrowed(p) => *p = &p[..p.len() - 1],
}
}
if local_path.starts_with('/') {
match &mut local_path {
Cow::Owned(p) => {
p.remove(0);
}
Cow::Borrowed(p) => *p = &p[1..],
}
}
// Verify that local_path is a prefix of the SWF path.
if movie_path.starts_with(local_path.as_ref())
&& (local_path.is_empty()
|| movie_path.len() == local_path.len()
|| movie_path[local_path.len()..].starts_with('/'))
{
local_path
} else {
log::warn!("SharedObject.get_local: localPath parameter does not match SWF path");
return Ok(Value::Null);
}
} else {
Cow::Borrowed(movie_path)
};
// Final SO path: foo.com/folder/game.swf/SOName
// SOName may be a path containing slashes. In this case, prefix with # to mimic Flash Player behavior.
let prefix = if name.contains('/') { "#" } else { "" };
let full_name = format!("{}/{}/{}{}", movie_host, local_path, prefix, name);
// Avoid any paths with `..` to prevent SWFs from crawling the file system on desktop.
// Flash will generally fail to save shared objects with a path component starting with `.`,
// so let's disallow them altogether.
if full_name.split('/').any(|s| s.starts_with('.')) {
log::error!("SharedObject.get_local: Invalid path with .. segments");
return Ok(Value::Null);
}
// Check if this is referencing an existing shared object
if let Some(so) = activation.context.avm2_shared_objects.get(&full_name) {
return Ok((*so).into());
}
// Data property only should exist when created with getLocal/Remote
let constructor = activation.avm2().classes().sharedobject;
let mut this = constructor.construct(activation, &[])?;
// Set the internal name
let ruffle_name: Multiname = QName::new(
Namespace::Private(AvmString::new_utf8(activation.context.gc_context, "")),
"_ruffleName",
)
.into();
this.set_property(
&ruffle_name,
AvmString::new_utf8(activation.context.gc_context, &full_name).into(),
activation,
)?;
let mut data = Value::Undefined;
// Load the data object from storage if it existed prior
if let Some(saved) = activation.context.storage.get(&full_name) {
if let Ok(lso) = flash_lso::read::Reader::default().parse(&saved) {
data = crate::avm2::amf::deserialize_lso(activation, &lso)?.into();
}
}
if data == Value::Undefined {
// No data; create a fresh data object.
data = activation
.avm2()
.classes()
.object
.construct(activation, &[])?
.into();
}
this.set_property(&Multiname::public("data"), data, activation)?;
activation
.context
.avm2_shared_objects
.insert(full_name, this);
Ok(this.into())
}
fn flush<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
pub fn flush<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
log::warn!("SharedObject.flush not implemented");
if let Some(this) = this {
let data = this
.get_property(&Multiname::public("data"), activation)?
.coerce_to_object(activation)?;
let ruffle_name: Multiname = QName::new(
Namespace::Private(AvmString::new_utf8(activation.context.gc_context, "")),
"_ruffleName",
)
.into();
let name = this
.get_property(&ruffle_name, activation)?
.coerce_to_string(activation)?;
let name = name.to_utf8_lossy();
let mut elements = Vec::new();
crate::avm2::amf::recursive_serialize(activation, data, &mut elements)?;
let mut lso = Lso::new(
elements,
&name
.split('/')
.last()
.map(|e| e.to_string())
.unwrap_or_else(|| "<unknown>".to_string()),
AMFVersion::AMF3,
);
let bytes = flash_lso::write::write_to_bytes(&mut lso).unwrap_or_default();
return Ok(activation.context.storage.put(&name, &bytes).into());
}
Ok(Value::Undefined)
}
/// Construct `SharedObject`'s class.
/// NOTE: We currently always use AMF3 serialization.
/// If you implement the `defaultObjectEncoding` or `objectEncoding`,
/// you will need to adjust the serialization and deserialization code
/// to work with AMF0.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
QName::new(Namespace::package("flash.net"), "SharedObject"),
@ -79,6 +278,12 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
None,
));
write.define_instance_trait(Trait::from_slot(
QName::new(Namespace::private(""), "_ruffleName"),
QName::new(Namespace::public(), "String").into(),
None,
));
const PUBLIC_CLASS_METHODS: &[(&str, NativeMethodImpl)] = &[("getLocal", get_local)];
write.define_public_builtin_class_methods(mc, PUBLIC_CLASS_METHODS);

View File

@ -1,9 +1,8 @@
use crate::avm2::activation::Activation;
use crate::avm2::array::ArrayStorage;
use crate::avm2::bytearray::{ByteArrayStorage, CompressionAlgorithm, Endian, ObjectEncoding};
use crate::avm2::bytearray::{CompressionAlgorithm, Endian, ObjectEncoding};
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::object::{bytearray_allocator, ArrayObject, ByteArrayObject, Object, TObject};
use crate::avm2::object::{bytearray_allocator, Object, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::avm2::Namespace;
@ -14,78 +13,8 @@ use encoding_rs::Encoding;
use encoding_rs::UTF_8;
use flash_lso::amf0::read::AMF0Decoder;
use flash_lso::amf3::read::AMF3Decoder;
use flash_lso::types::Value as AmfValue;
use gc_arena::{GcCell, MutationContext};
pub fn deserialize_value<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
value: &AmfValue,
) -> Result<Value<'gc>, Error> {
Ok(match value {
AmfValue::Undefined => Value::Undefined,
AmfValue::Null => Value::Null,
AmfValue::Bool(b) => Value::Bool(*b),
AmfValue::Integer(i) => Value::Integer(*i),
AmfValue::Number(n) => Value::Number(*n),
AmfValue::String(s) => Value::String(AvmString::new_utf8(activation.context.gc_context, s)),
AmfValue::ByteArray(bytes) => {
let storage = ByteArrayStorage::from_vec(bytes.clone());
let bytearray = ByteArrayObject::from_storage(activation, storage)?;
bytearray.into()
}
AmfValue::StrictArray(values) => {
let mut arr: Vec<Option<Value<'gc>>> = Vec::with_capacity(values.len());
for value in values {
arr.push(Some(deserialize_value(activation, value)?));
}
let storage = ArrayStorage::from_storage(arr);
let array = ArrayObject::from_storage(activation, storage)?;
array.into()
}
AmfValue::ECMAArray(values, elements, _) => {
// First lets create an array out of `values` (dense portion), then we add the elements onto it.
let mut arr: Vec<Option<Value<'gc>>> = Vec::with_capacity(values.len());
for value in values {
arr.push(Some(deserialize_value(activation, value)?));
}
let storage = ArrayStorage::from_storage(arr);
let mut array = ArrayObject::from_storage(activation, storage)?;
// Now lets add each element as a property
for element in elements {
array.set_property(
&QName::new(
Namespace::public(),
AvmString::new_utf8(activation.context.gc_context, element.name()),
)
.into(),
deserialize_value(activation, element.value())?,
activation,
)?;
}
array.into()
}
AmfValue::Object(properties, _class_definition) => {
let obj_class = activation.avm2().classes().object;
let mut obj = obj_class.construct(activation, &[])?;
for property in properties {
obj.set_property(
&QName::new(
Namespace::public(),
AvmString::new_utf8(activation.context.gc_context, property.name()),
)
.into(),
deserialize_value(activation, property.value())?,
activation,
)?;
}
obj.into()
// TODO: Handle class_defintion
}
// TODO: Dictionary, Vector, XML, Date, etc...
_ => Value::Undefined,
})
}
/// Implements `flash.utils.ByteArray`'s instance constructor.
pub fn instance_init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
@ -849,14 +778,20 @@ pub fn read_object<'gc>(
let (extra, amf) = decoder
.parse_single_element(bytes)
.map_err(|_| "Error: Invalid object")?;
(extra.len(), deserialize_value(activation, &amf)?)
(
extra.len(),
crate::avm2::amf::deserialize_value(activation, &amf)?,
)
}
ObjectEncoding::Amf3 => {
let mut decoder = AMF3Decoder::default();
let (extra, amf) = decoder
.parse_single_element(bytes)
.map_err(|_| "Error: Invalid object")?;
(extra.len(), deserialize_value(activation, &amf)?)
(
extra.len(),
crate::avm2::amf::deserialize_value(activation, &amf)?,
)
}
};

View File

@ -127,7 +127,10 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> {
pub instance_counter: &'a mut i32,
/// Shared objects cache
pub shared_objects: &'a mut HashMap<String, Avm1Object<'gc>>,
pub avm1_shared_objects: &'a mut HashMap<String, Avm1Object<'gc>>,
/// Shared objects cache
pub avm2_shared_objects: &'a mut HashMap<String, Avm2Object<'gc>>,
/// Text fields with unbound variable bindings.
pub unbound_text_fields: &'a mut Vec<EditText<'gc>>,
@ -319,7 +322,8 @@ impl<'a, 'gc, 'gc_context> UpdateContext<'a, 'gc, 'gc_context> {
load_manager: self.load_manager,
system: self.system,
instance_counter: self.instance_counter,
shared_objects: self.shared_objects,
avm1_shared_objects: self.avm1_shared_objects,
avm2_shared_objects: self.avm2_shared_objects,
unbound_text_fields: self.unbound_text_fields,
timers: self.timers,
current_context_menu: self.current_context_menu,

View File

@ -7,7 +7,7 @@ use crate::avm1::{Activation, ActivationIdentifier};
use crate::avm1::{ScriptObject, TObject, Value};
use crate::avm2::{
object::LoaderInfoObject, object::TObject as _, Activation as Avm2Activation, Avm2, CallStack,
Domain as Avm2Domain, EventObject as Avm2EventObject,
Domain as Avm2Domain, EventObject as Avm2EventObject, Object as Avm2Object,
};
use crate::backend::{
audio::{AudioBackend, AudioManager},
@ -121,7 +121,9 @@ struct GcRootData<'gc> {
/// data in the GC arena.
load_manager: LoadManager<'gc>,
shared_objects: HashMap<String, Object<'gc>>,
avm1_shared_objects: HashMap<String, Object<'gc>>,
avm2_shared_objects: HashMap<String, Avm2Object<'gc>>,
/// Text fields with unbound variable bindings.
unbound_text_fields: Vec<EditText<'gc>>,
@ -156,6 +158,7 @@ impl<'gc> GcRootData<'gc> {
&mut Option<DragObject<'gc>>,
&mut LoadManager<'gc>,
&mut HashMap<String, Object<'gc>>,
&mut HashMap<String, Avm2Object<'gc>>,
&mut Vec<EditText<'gc>>,
&mut Timers<'gc>,
&mut Option<ContextMenuState<'gc>>,
@ -170,7 +173,8 @@ impl<'gc> GcRootData<'gc> {
&mut self.avm2,
&mut self.drag_object,
&mut self.load_manager,
&mut self.shared_objects,
&mut self.avm1_shared_objects,
&mut self.avm2_shared_objects,
&mut self.unbound_text_fields,
&mut self.timers,
&mut self.current_context_menu,
@ -1547,7 +1551,8 @@ impl Player {
avm2,
drag_object,
load_manager,
shared_objects,
avm1_shared_objects,
avm2_shared_objects,
unbound_text_fields,
timers,
current_context_menu,
@ -1579,7 +1584,8 @@ impl Player {
storage: self.storage.deref_mut(),
log: self.log.deref_mut(),
video: self.video.deref_mut(),
shared_objects,
avm1_shared_objects,
avm2_shared_objects,
unbound_text_fields,
timers,
current_context_menu,
@ -1677,11 +1683,24 @@ impl Player {
pub fn flush_shared_objects(&mut self) {
self.update(|context| {
let mut activation =
let mut avm1_activation =
Activation::from_stub(context.reborrow(), ActivationIdentifier::root("[Flush]"));
let shared_objects = activation.context.shared_objects.clone();
for so in shared_objects.values() {
let _ = crate::avm1::flush(&mut activation, *so, &[]);
for so in avm1_activation.context.avm1_shared_objects.clone().values() {
if let Err(e) = crate::avm1::flush(&mut avm1_activation, *so, &[]) {
log::error!("Error flushing AVM1 shared object `{:?}`: {:?}", so, e);
}
}
let mut avm2_activation =
Avm2Activation::from_nothing(avm1_activation.context.reborrow());
for so in avm2_activation.context.avm2_shared_objects.clone().values() {
if let Err(e) = crate::avm2::globals::flash::net::sharedobject::flush(
&mut avm2_activation,
Some(*so),
&[],
) {
log::error!("Error flushing AVM2 shared object `{:?}`: {:?}", so, e);
}
}
});
}
@ -1996,7 +2015,8 @@ impl PlayerBuilder {
load_manager: LoadManager::new(),
mouse_hovered_object: None,
mouse_pressed_object: None,
shared_objects: HashMap::new(),
avm1_shared_objects: HashMap::new(),
avm2_shared_objects: HashMap::new(),
stage: Stage::empty(gc_context, self.fullscreen),
timers: Timers::new(),
unbound_text_fields: Vec::new(),

View File

@ -1066,6 +1066,60 @@ fn shared_object_avm1() -> Result<(), Error> {
Ok(())
}
#[test]
fn shared_object_avm2() -> Result<(), Error> {
set_logger();
// Test SharedObject persistence. Run an SWF that saves data
// to a shared object twice and verify that the data is saved.
let mut memory_storage_backend: Box<dyn StorageBackend> =
Box::new(MemoryStorageBackend::default());
// Initial run; no shared object data.
test_swf_with_hooks(
"tests/swfs/avm2/shared_object/test.swf",
1,
"tests/swfs/avm2/shared_object/input1.json",
"tests/swfs/avm2/shared_object/output1.txt",
|_player| Ok(()),
|player| {
// Save the storage backend for next run.
let mut player = player.lock().unwrap();
std::mem::swap(player.storage_mut(), &mut memory_storage_backend);
Ok(())
},
false,
false,
)?;
// Verify that the flash cookie matches the expected one
let expected = std::fs::read("tests/swfs/avm2/shared_object/RuffleTest.sol")?;
assert_eq!(
expected,
memory_storage_backend
.get("localhost//RuffleTest")
.unwrap_or_default()
);
// Re-run the SWF, verifying that the shared object persists.
test_swf_with_hooks(
"tests/swfs/avm2/shared_object/test.swf",
1,
"tests/swfs/avm2/shared_object/input2.json",
"tests/swfs/avm2/shared_object/output2.txt",
|player| {
// Swap in the previous storage backend.
let mut player = player.lock().unwrap();
std::mem::swap(player.storage_mut(), &mut memory_storage_backend);
Ok(())
},
|_player| Ok(()),
false,
false,
)?;
Ok(())
}
#[test]
fn timeout_avm1() -> Result<(), Error> {
set_logger();

Binary file not shown.

View File

@ -0,0 +1,108 @@
package {
import flash.net.SharedObject;
public class Test {
static function storeData(data: Object) {
var sparse = new Array(5);
sparse[0] = "elem0";
sparse[4] = "elem4";
sparse[-1] = "elem negative one";
var dense = new Array(3);
dense[0] = 1;
dense[1] = 2;
dense[2] = 3;
// Store everything in an array to work around Ruffle's incorrect
// object property serialization order
data.props = [
true,
"hello",
"something else",
[uint(10), uint((1 << 28) - 2), uint((1 << 28) - 1), uint((1 << 28)), uint((1 << 28) + 1), uint((1 << 28) + 2)],
[int(10), int((1 << 28) - 2), int((1 << 28) - 1), int((1 << 28)), int((1 << 28) + 1), int((1 << 28) + 2),
int(-(1 << 28) - 2), int(-(1 << 28) - 1), int(-(1 << 28)), int(-(1 << 28) + 1), int(-(1 << 28) + 2)],
-5.1,
sparse,
dense,
new Date(2147483647),
// FIXME - enable this when Ruffle fully implements AVM2 XML
// new XML("<test>Test</test>")
];
data.hidden = "Some hidden value";
data.setPropertyIsEnumerable("hidden", false);
}
public static function main() {
// The serialization order for object keys in Flash player depends
// on the enumeration order of the 'data' object, which in turn
// depends on the placement of objects on the heap. This appears to
// be deterministic between runs of flash player, but the effect of
// adding or removing a property has an unpredictable effect on the order.
// Since Ruffle doesn't implement Flash's hash-based enumeration order,
// the AMF '.sol' file we write out may have object properties written
// in a different order (though it should still deserialize to the same object).
//
// To work around this, we create two SharedObjects
// 1. RuffleTest only stores an array of simple objects, so the AMF output should match exactly between Ruffle and Flash Player
// 2. RuffleTestNonDeterministic stores objects with several properties, so we don't compare the AMF (but we do print the deserialized object)
var obj = SharedObject.getLocal("RuffleTest", "/");
var otherObj = SharedObject.getLocal("RuffleTestNonDeterministic", "/")
trace("typeof obj =" + typeof obj.data);
trace("typeof otherObj =" + typeof otherObj.data);
if(obj.data.props === undefined) {
trace("No data found. Initializing...");
storeData(obj.data)
storeData(otherObj.data)
//Only set this on the object that we *don't* compare with flash,
//since we don't yet match the object property serialization order correctly.
otherObj.data.o = {a: "a", b: "b"};
trace("otherObj.data.props:")
dump(otherObj.data.props);
obj.flush();
otherObj.flush();
} else {
trace("obj.data.hidden: " + obj.data.hidden);
trace("otherObj.data.hidden: " + otherObj.data.hidden);
trace()
trace("obj dump:")
dump(obj.data.props);
trace()
trace("otherObj dump:")
dump(otherObj.data.props)
}
}
}
}
function dump(obj:Object) {
var keys = [];
for (var key in obj) {
keys.push(key);
}
keys.sort();
for (var i in keys) {
var k = keys[i];
var val = obj[k];
if (val instanceof Date) {
// Printing 'val.toString()' depends on the system time zone,
// so use UTC to make the output reproducible
trace(k, "= (UTC) ", val.toUTCString());
} else {
trace(k, "=", val.toString(), "type", typeof val);
}
}
}

View File

@ -0,0 +1,13 @@
typeof obj =object
typeof otherObj =object
No data found. Initializing...
otherObj.data.props:
0 = true type boolean
1 = hello type string
2 = something else type string
3 = 10,268435454,268435455,268435456,268435457,268435458 type object
4 = 10,268435454,268435455,268435456,268435457,268435458,-268435458,-268435457,-268435456,-268435455,-268435454 type object
5 = -5.1 type number
6 = elem0,,,,elem4 type object
7 = 1,2,3 type object
8 = (UTC) Sun Jan 25 20:31:23 1970 UTC

View File

@ -0,0 +1,26 @@
typeof obj =object
typeof otherObj =object
obj.data.hidden: undefined
otherObj.data.hidden: undefined
obj dump:
0 = true type boolean
1 = hello type string
2 = something else type string
3 = 10,268435454,268435455,268435456,268435457,268435458 type object
4 = 10,268435454,268435455,268435456,268435457,268435458,-268435458,-268435457,-268435456,-268435455,-268435454 type object
5 = -5.1 type number
6 = elem0,,,,elem4 type object
7 = 1,2,3 type object
8 = (UTC) Sun Jan 25 20:31:23 1970 UTC
otherObj dump:
0 = true type boolean
1 = hello type string
2 = something else type string
3 = 10,268435454,268435455,268435456,268435457,268435458 type object
4 = 10,268435454,268435455,268435456,268435457,268435458,-268435458,-268435457,-268435456,-268435455,-268435454 type object
5 = -5.1 type number
6 = elem0,,,,elem4 type object
7 = 1,2,3 type object
8 = (UTC) Sun Jan 25 20:31:23 1970 UTC

Binary file not shown.

Binary file not shown.