From 48693e4a7a63584f635134dc1695ce9ad6759dc5 Mon Sep 17 00:00:00 2001 From: CUB3D Date: Fri, 12 Jun 2020 19:27:20 +0100 Subject: [PATCH] core: Add inital storage backend implementation Currently SharedObjects are encoded and decoded from JSON via the StorageBackend, also provided is a basic in-memory implementation --- Cargo.lock | 7 ++ core/Cargo.toml | 1 + core/src/avm1/globals/shared_object.rs | 155 ++++++++++++++++++++++--- core/src/backend.rs | 1 + core/src/backend/storage.rs | 62 ++++++++++ core/src/context.rs | 4 + core/src/player.rs | 19 +++ 7 files changed, 230 insertions(+), 19 deletions(-) create mode 100644 core/src/backend/storage.rs diff --git a/Cargo.lock b/Cargo.lock index 286a44d9a..2c7e7821c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1051,6 +1051,11 @@ dependencies = [ "wasm-bindgen 0.2.63 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1866,6 +1871,7 @@ dependencies = [ "gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", "indexmap 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "jpeg-decoder 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", + "json 0.12.4 (registry+https://github.com/rust-lang/crates.io-index)", "libflate 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "minimp3 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2919,6 +2925,7 @@ dependencies = [ "checksum jni-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" "checksum jpeg-decoder 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "5b47b4c4e017b01abdc5bcc126d2d1002e5a75bbe3ce73f9f4f311a916363704" "checksum js-sys 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "ce10c23ad2ea25ceca0093bd3192229da4c5b3c0f2de499c1ecac0d98d452177" +"checksum json 0.12.4 (registry+https://github.com/rust-lang/crates.io-index)" = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" diff --git a/core/Cargo.toml b/core/Cargo.toml index cc2208c63..47323eff4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -29,6 +29,7 @@ url = "2.1.0" weak-table = "0.2.3" percent-encoding = "2.1.0" thiserror = "1.0" +json = "0.12.4" [dependencies.jpeg-decoder] version = "0.1.19" diff --git a/core/src/avm1/globals/shared_object.rs b/core/src/avm1/globals/shared_object.rs index 88324d3ad..7fc375a8c 100644 --- a/core/src/avm1/globals/shared_object.rs +++ b/core/src/avm1/globals/shared_object.rs @@ -5,6 +5,7 @@ use enumset::EnumSet; use crate::context::UpdateContext; use crate::avm1::return_value::ReturnValue; use crate::avm1::object::Object; +use json::JsonValue; pub fn delete_all<'gc>( @@ -27,16 +28,91 @@ pub fn get_disk_usage<'gc>( Ok(Value::Undefined.into()) } +fn parse_json<'gc>(json_obj: JsonValue, avm: &mut Avm1<'gc>, object: Object<'gc>, context: &mut UpdateContext<'_, 'gc, '_>) { + for entry in json_obj.entries() { + match entry.1 { + JsonValue::Null => { + object.define_value( + context.gc_context, + entry.0, + Value::Null, + EnumSet::empty() + ); + }, + JsonValue::Short(s) => { + let val: String = s.as_str().to_string(); + object.define_value( + context.gc_context, + entry.0, + Value::String(val), + EnumSet::empty() + ); + }, + JsonValue::String(s) => { + object.define_value( + context.gc_context, + entry.0, + Value::String(s.clone()), + EnumSet::empty() + ); + }, + JsonValue::Number(f) => { + log::warn!("[SharedObject] {} = {}", entry.0, f); + let val: f64 = f.clone().into(); + object.define_value( + context.gc_context, + entry.0, + Value::Number(val), + EnumSet::empty() + ); + }, + JsonValue::Boolean(b) => { + log::warn!("[SharedObject] {} = {}", entry.0, b); + object.define_value( + context.gc_context, + entry.0, + Value::Bool(*b), + EnumSet::empty() + ); + }, + JsonValue::Object(o) => { + let so = avm.prototypes().object; + let obj = so.new(avm, context, so, &[]).unwrap(); + let _ = crate::avm1::globals::object::constructor(avm, context, obj, &[]).unwrap(); + parse_json(JsonValue::Object(o.clone()), avm,obj, context); + + object.define_value( + context.gc_context, + entry.0, + Value::Object(obj), + EnumSet::empty() + ); + }, + JsonValue::Array(_) => {}, + } + } +} + pub fn get_local<'gc>( - _avm: &mut Avm1<'gc>, - _action_context: &mut UpdateContext<'_, 'gc, '_>, + avm: &mut Avm1<'gc>, + action_context: &mut UpdateContext<'_, 'gc, '_>, _this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { - let so = _avm.prototypes().shared_object; - let obj = so.new(_avm, _action_context, so, &[])?; - let _ = constructor(_avm, _action_context, obj, &[])?; + //TODO: use args and restore existing state (after disk backend) + + let so = avm.prototypes().shared_object; + let obj = so.new(avm, action_context, so, &[])?; + let _ = constructor(avm, action_context, obj, &[])?; + + let saved = action_context.storage.get_string("tmp".to_string()); + if let Some(saved_data) = saved { + let data = obj.get("data", avm, action_context).unwrap().as_object().unwrap(); + //TODO: error handle + let js = json::parse(&saved_data).unwrap(); + parse_json(js, avm, data, action_context); + } // log::warn!("SharedObject.getLocal() not implemented"); Ok(obj.into()) @@ -120,12 +196,20 @@ pub fn create_shared_object_object<'gc>( } pub fn clear<'gc>( - _avm: &mut Avm1<'gc>, - _action_context: &mut UpdateContext<'_, 'gc, '_>, - _this: Object<'gc>, + avm: &mut Avm1<'gc>, + action_context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { - log::warn!("SharedObject.clear() not implemented"); + + //TODO: does this remove elements not in data prop (e.g. recreate entire obj) or does it just remove children of prop + + let data = this.get("data", avm, action_context).unwrap().as_object().unwrap(); + + for k in &data.get_keys(avm) { + data.delete(avm, action_context.gc_context, k); + } + Ok(Value::Undefined.into()) } @@ -149,31 +233,64 @@ pub fn connect<'gc>( Ok(Value::Undefined.into()) } +fn recursive_serialize<'gc>(avm: &mut Avm1<'gc>, action_context: &mut UpdateContext<'_, 'gc, '_>, obj: Object<'gc>, json_obj: &mut JsonValue) { + for k in &obj.get_keys(avm) { + let elem = obj.get(k, avm, action_context).unwrap(); + + match elem { + //TODO: should never happen + Value::Undefined => log::warn!(" [SharedObject] {} = Undef", k), + Value::Null => json_obj[k] = JsonValue::Null, + Value::Bool(b) => json_obj[k] = b.into(), + Value::Number(f) => json_obj[k] = f.into(), + Value::String(s) => json_obj[k] = s.into(), + Value::Object(o) => { + // Don't attempt to serialize functions, etc + if !o.is_instance_of(avm, action_context, o, avm.prototypes().function).unwrap() { + + let mut sub_data_json = JsonValue::new_object(); + recursive_serialize(avm, action_context, o, &mut sub_data_json); + json_obj[k] = sub_data_json; + } + }, + } + } +} + pub fn flush<'gc>( _avm: &mut Avm1<'gc>, _action_context: &mut UpdateContext<'_, 'gc, '_>, _this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { - log::warn!("SharedObject.flush() not implemented"); - + //TODO: consider args let data = _this.get("data", _avm, _action_context).unwrap().as_object().unwrap(); + let mut data_json = JsonValue::new_object(); - for key in &data.get_keys(_avm) { - log::warn!("[SharedObject] {} = {:?}", key, &data.get(key, _avm, _action_context)); - } + recursive_serialize(_avm, _action_context,data, &mut data_json); + //TODO: somehow need to know the name of where to save it to (hidden property?) + _action_context.storage.put_string("tmp".into(), data_json.dump()); + //TODO: return value Ok(Value::Undefined.into()) } pub fn get_size<'gc>( - _avm: &mut Avm1<'gc>, - _action_context: &mut UpdateContext<'_, 'gc, '_>, + avm: &mut Avm1<'gc>, + action_context: &mut UpdateContext<'_, 'gc, '_>, _this: Object<'gc>, - _args: &[Value<'gc>], + args: &[Value<'gc>], ) -> Result, Error> { - log::warn!("SharedObject.getSize() not implemented"); - Ok(Value::Undefined.into()) + + let name = args.get(0) + .unwrap_or(&Value::Undefined) + .to_owned() + .coerce_to_string(avm, action_context)?; + + // TODO: what does this return if the item dosent exist + let size = action_context.storage.get_size(name).unwrap_or(0) as f64; + + Ok(size.into()) } pub fn send<'gc>( diff --git a/core/src/backend.rs b/core/src/backend.rs index 888753736..020b8dd07 100644 --- a/core/src/backend.rs +++ b/core/src/backend.rs @@ -2,3 +2,4 @@ pub mod audio; pub mod input; pub mod navigator; pub mod render; +pub mod storage; diff --git a/core/src/backend/storage.rs b/core/src/backend/storage.rs new file mode 100644 index 000000000..a416877e5 --- /dev/null +++ b/core/src/backend/storage.rs @@ -0,0 +1,62 @@ +use downcast_rs::Downcast; +use std::collections::HashMap; + +pub trait StorageBackend: Downcast { + fn get_string(&self, name: String) -> Option; + + fn put_string(&mut self, name: String, value: String); + + fn get_size(&self, name: String) -> Option { + self.get_string(name).map(|x| x.as_bytes().len()) + } +} +impl_downcast!(StorageBackend); + +pub struct MemoryStorageBackend { + pub map: HashMap +} + +impl Default for MemoryStorageBackend { + fn default() -> Self { + MemoryStorageBackend { + map: HashMap::new() + } + } +} + +impl StorageBackend for MemoryStorageBackend { + fn get_string(&self, name: String) -> Option { + self.map.get(&name).map(|s| s.clone()) + } + + fn put_string(&mut self, name: String, value: String) { + self.map.insert(name, value); + } +} + +// impl SharedObjectBackend for MemoryBackend { +// } +// +// // In the desktop player +// struct DiskBackend {} +// +// impl SharedObjectBackend for DiskBackend { +// pub fn get_obj_json(name: String) -> String { +// // read from file +// } +// pub fn set_obj_json(name: String, value: String) { +// // write to file +// //TODO: if slow maybe keep and internal cache, also consider async io for fast flushing +// } +// +// pub fn clear() { +// // Delete the file, modifying the avm objects from any of these will be done in shared_object +// } +// } +// +// // for web player +// struct LocalStorageBackend {} +// +// //TODO: check the issue about this, need to prefix with url of site somehow to avoid collisions + + diff --git a/core/src/context.rs b/core/src/context.rs index acc483cd6..9977a51a5 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -18,6 +18,7 @@ use rand::rngs::SmallRng; use std::collections::BTreeMap; use std::collections::VecDeque; use std::sync::{Arc, Mutex, Weak}; +use crate::backend::storage::StorageBackend; /// `UpdateContext` holds shared data that is used by the various subsystems of Ruffle. /// `Player` crates this when it begins a tick and passes it through the call stack to @@ -64,6 +65,9 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// The input backend, used to detect user interactions. pub input: &'a mut dyn InputBackend, + /// The storage backend, used for storing persistent state + pub storage: &'a mut dyn StorageBackend, + /// The RNG, used by the AVM `RandomNumber` opcode, `Math.random(),` and `random()`. pub rng: &'a mut SmallRng, diff --git a/core/src/player.rs b/core/src/player.rs index aa27c4f5d..4c9fd1a7b 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -22,6 +22,7 @@ use std::collections::BTreeMap; use std::convert::TryFrom; use std::ops::DerefMut; use std::sync::{Arc, Mutex, Weak}; +use crate::backend::storage::{StorageBackend, MemoryStorageBackend}; static DEVICE_FONT_TAG: &[u8] = include_bytes!("../assets/noto-sans-definefont3.bin"); @@ -89,6 +90,7 @@ type Audio = Box; type Navigator = Box; type Renderer = Box; type Input = Box; +type Storage = Box; pub struct Player { /// The version of the player we're emulating. @@ -116,6 +118,8 @@ pub struct Player { view_matrix: Matrix, inverse_view_matrix: Matrix, + storage: Storage, + rng: SmallRng, gc_arena: GcArena, @@ -157,6 +161,7 @@ impl Player { navigator: Navigator, input: Input, movie: SwfMovie, + storage: Storage ) -> Result>, Error> { let movie = Arc::new(movie); @@ -188,6 +193,7 @@ impl Player { view_matrix: Default::default(), inverse_view_matrix: Default::default(), + rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms. gc_arena: GcArena::new(ArenaParameters::default(), |gc_context| { @@ -242,6 +248,7 @@ impl Player { self_reference: None, system: SystemProperties::default(), instance_counter: 0, + storage }; player.mutate_with_update_context(|avm, context| { @@ -849,7 +856,11 @@ impl Player { stage_height, player, system_properties, +<<<<<<< HEAD instance_counter, +======= + storage, +>>>>>>> 0ca1eb0... core: Add inital storage backend implementation ) = ( self.player_version, self.global_time, @@ -865,7 +876,11 @@ impl Player { Twips::from_pixels(self.movie_height.into()), self.self_reference.clone(), &mut self.system, +<<<<<<< HEAD &mut self.instance_counter, +======= + self.storage.deref_mut(), +>>>>>>> 0ca1eb0... core: Add inital storage backend implementation ); self.gc_arena.mutate(|gc_context, gc_root| { @@ -896,7 +911,11 @@ impl Player { player, load_manager, system: system_properties, +<<<<<<< HEAD instance_counter, +======= + storage +>>>>>>> 0ca1eb0... core: Add inital storage backend implementation }; let ret = f(avm, &mut update_context);