diff --git a/Cargo.lock b/Cargo.lock index 4e55ce142..bea22d3f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3047,6 +3047,7 @@ dependencies = [ "dasp", "downcast-rs", "encoding_rs", + "enumset", "flash-lso", "flate2", "fnv", diff --git a/core/Cargo.toml b/core/Cargo.toml index f361b2ed3..c8e841700 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -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" diff --git a/core/src/avm1/globals/shared_object.rs b/core/src/avm1/globals/shared_object.rs index 73ee67b20..b5901d7c7 100644 --- a/core/src/avm1/globals/shared_object.rs +++ b/core/src/avm1/globals/shared_object.rs @@ -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()) } diff --git a/core/src/avm2.rs b/core/src/avm2.rs index 2a8c81356..01c6bc445 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -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; diff --git a/core/src/avm2/amf.rs b/core/src/avm2/amf.rs new file mode 100644 index 000000000..1a85302de --- /dev/null +++ b/core/src/avm2/amf.rs @@ -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 { + 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, +) -> 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, 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>> = 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>> = 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, 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) +} diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index 273e71fb1..f75ca2a61 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -23,7 +23,7 @@ mod boolean; mod class; mod date; mod error; -mod flash; +pub mod flash; mod function; mod global_scope; mod int; diff --git a/core/src/avm2/globals/flash/net/sharedobject.rs b/core/src/avm2/globals/flash/net/sharedobject.rs index 8f839e376..59d78614e 100644 --- a/core/src/avm2/globals/flash/net/sharedobject.rs +++ b/core/src/avm2/globals/flash/net/sharedobject.rs @@ -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>, - _args: &[Value<'gc>], + args: &[Value<'gc>], ) -> Result, 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>, +pub fn flush<'gc>( + activation: &mut Activation<'_, 'gc, '_>, + this: Option>, _args: &[Value<'gc>], ) -> Result, 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(|| "".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); diff --git a/core/src/avm2/globals/flash/utils/bytearray.rs b/core/src/avm2/globals/flash/utils/bytearray.rs index ee0d5bf6a..7fa336aff 100644 --- a/core/src/avm2/globals/flash/utils/bytearray.rs +++ b/core/src/avm2/globals/flash/utils/bytearray.rs @@ -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, 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>> = 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>> = 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)?, + ) } }; diff --git a/core/src/context.rs b/core/src/context.rs index eb47207ea..75e532b5a 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -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>, + pub avm1_shared_objects: &'a mut HashMap>, + + /// Shared objects cache + pub avm2_shared_objects: &'a mut HashMap>, /// Text fields with unbound variable bindings. pub unbound_text_fields: &'a mut Vec>, @@ -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, diff --git a/core/src/player.rs b/core/src/player.rs index 90cb9f017..dea3783c2 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -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>, + avm1_shared_objects: HashMap>, + + avm2_shared_objects: HashMap>, /// Text fields with unbound variable bindings. unbound_text_fields: Vec>, @@ -156,6 +158,7 @@ impl<'gc> GcRootData<'gc> { &mut Option>, &mut LoadManager<'gc>, &mut HashMap>, + &mut HashMap>, &mut Vec>, &mut Timers<'gc>, &mut Option>, @@ -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(), diff --git a/tests/tests/regression_tests.rs b/tests/tests/regression_tests.rs index 569b4e1c9..59bfe2503 100644 --- a/tests/tests/regression_tests.rs +++ b/tests/tests/regression_tests.rs @@ -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 = + 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(); diff --git a/tests/tests/swfs/avm2/shared_object/RuffleTest.sol b/tests/tests/swfs/avm2/shared_object/RuffleTest.sol new file mode 100644 index 000000000..96506951d Binary files /dev/null and b/tests/tests/swfs/avm2/shared_object/RuffleTest.sol differ diff --git a/tests/tests/swfs/avm2/shared_object/Test.as b/tests/tests/swfs/avm2/shared_object/Test.as new file mode 100644 index 000000000..c11570ac8 --- /dev/null +++ b/tests/tests/swfs/avm2/shared_object/Test.as @@ -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") + ]; + 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); + } + + } +} diff --git a/tests/tests/swfs/avm2/shared_object/output1.txt b/tests/tests/swfs/avm2/shared_object/output1.txt new file mode 100644 index 000000000..80d5d9d9c --- /dev/null +++ b/tests/tests/swfs/avm2/shared_object/output1.txt @@ -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 diff --git a/tests/tests/swfs/avm2/shared_object/output2.txt b/tests/tests/swfs/avm2/shared_object/output2.txt new file mode 100644 index 000000000..a52d3653b --- /dev/null +++ b/tests/tests/swfs/avm2/shared_object/output2.txt @@ -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 diff --git a/tests/tests/swfs/avm2/shared_object/test.fla b/tests/tests/swfs/avm2/shared_object/test.fla new file mode 100644 index 000000000..9a135c82c Binary files /dev/null and b/tests/tests/swfs/avm2/shared_object/test.fla differ diff --git a/tests/tests/swfs/avm2/shared_object/test.swf b/tests/tests/swfs/avm2/shared_object/test.swf new file mode 100644 index 000000000..ab5e5c7db Binary files /dev/null and b/tests/tests/swfs/avm2/shared_object/test.swf differ