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:
parent
ca701c4aeb
commit
bb138d9082
|
@ -3047,6 +3047,7 @@ dependencies = [
|
|||
"dasp",
|
||||
"downcast-rs",
|
||||
"encoding_rs",
|
||||
"enumset",
|
||||
"flash-lso",
|
||||
"flate2",
|
||||
"fnv",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -23,7 +23,7 @@ mod boolean;
|
|||
mod class;
|
||||
mod date;
|
||||
mod error;
|
||||
mod flash;
|
||||
pub mod flash;
|
||||
mod function;
|
||||
mod global_scope;
|
||||
mod int;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)?,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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.
Loading…
Reference in New Issue