From 31b84c5f19c316719829c0df9f9268e384dfac35 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Fri, 13 Dec 2019 15:50:58 +0100 Subject: [PATCH] core: Made arrays a storage property of objects, not a unique object type. Added more corner case tests. --- core/src/avm1.rs | 14 +- core/src/avm1/array_object.rs | 191 ----------------- core/src/avm1/globals/array.rs | 160 ++++++-------- core/src/avm1/globals/object.rs | 15 +- core/src/avm1/object.rs | 44 +++- core/src/avm1/script_object.rs | 200 ++++++++++++++++-- core/src/avm1/stage_object.rs | 27 ++- core/tests/regression_tests.rs | 2 + .../swfs/avm1/array_prototyping/output.txt | 12 ++ .../swfs/avm1/array_prototyping/test.fla | Bin 0 -> 4709 bytes .../swfs/avm1/array_prototyping/test.swf | Bin 0 -> 720 bytes .../avm1/array_vs_object_length/output.txt | 100 +++++++++ .../swfs/avm1/array_vs_object_length/test.fla | Bin 0 -> 4768 bytes .../swfs/avm1/array_vs_object_length/test.swf | Bin 0 -> 788 bytes 14 files changed, 434 insertions(+), 331 deletions(-) delete mode 100644 core/src/avm1/array_object.rs create mode 100644 core/tests/swfs/avm1/array_prototyping/output.txt create mode 100644 core/tests/swfs/avm1/array_prototyping/test.fla create mode 100644 core/tests/swfs/avm1/array_prototyping/test.swf create mode 100644 core/tests/swfs/avm1/array_vs_object_length/output.txt create mode 100644 core/tests/swfs/avm1/array_vs_object_length/test.fla create mode 100644 core/tests/swfs/avm1/array_vs_object_length/test.swf diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 55435b643..62b37ea4a 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -18,7 +18,6 @@ use crate::tag_utils::SwfSlice; mod test_utils; mod activation; -pub mod array_object; mod fscommand; mod function; pub mod globals; @@ -34,8 +33,6 @@ mod value; mod tests; use activation::Activation; -pub use array_object::ArrayObject; -use enumset::EnumSet; pub use globals::SystemPrototypes; pub use object::{Object, ObjectPtr, TObject}; use scope::Scope; @@ -1282,19 +1279,12 @@ impl<'gc> Avm1<'gc> { fn action_init_array(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> { let num_elements = self.pop()?.as_i64()?; - let array = ArrayObject::array(context.gc_context, Some(self.prototypes.array)); + let array = ScriptObject::array(context.gc_context, Some(self.prototypes.array)); for i in 0..num_elements { - array.define_value( - context.gc_context, - &(i as i32).to_string(), - self.pop()?, - EnumSet::empty(), - ); + array.set_array_element(i as usize, self.pop()?, context.gc_context); } - array.set_length(context.gc_context, num_elements as i32); - self.push(Value::Object(array.into())); Ok(()) } diff --git a/core/src/avm1/array_object.rs b/core/src/avm1/array_object.rs deleted file mode 100644 index 1a768cb04..000000000 --- a/core/src/avm1/array_object.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::avm1::function::Executable; -use crate::avm1::property::Attribute; -use crate::avm1::return_value::ReturnValue; -use crate::avm1::{Avm1, Error, Object, ObjectPtr, ScriptObject, TObject, Value}; -use crate::context::UpdateContext; -use crate::display_object::DisplayObject; - -use enumset::EnumSet; -use gc_arena::{Collect, GcCell, MutationContext}; -use std::collections::hash_map::RandomState; -use std::collections::HashSet; - -#[derive(Debug, Copy, Clone, Collect)] -#[collect(no_drop)] -pub struct ArrayObject<'gc>(GcCell<'gc, ArrayObjectData<'gc>>); - -#[derive(Debug, Collect)] -#[collect(no_drop)] -pub struct ArrayObjectData<'gc> { - base: ScriptObject<'gc>, -} - -fn get_length<'gc>( - _avm: &mut Avm1<'gc>, - _action_context: &mut UpdateContext<'_, 'gc, '_>, - this: Object<'gc>, - _args: &[Value<'gc>], -) -> Result, Error> { - Ok(this.get_length().into()) -} - -impl<'gc> ArrayObject<'gc> { - pub fn array( - gc_context: MutationContext<'gc, '_>, - proto: Option>, - ) -> ArrayObject<'gc> { - let base = ScriptObject::object(gc_context, proto); - base.add_property( - gc_context, - "length", - Executable::Native(get_length), - None, - Attribute::DontDelete | Attribute::DontEnum, - ); - - ArrayObject(GcCell::allocate(gc_context, ArrayObjectData { base })) - } -} - -impl<'gc> TObject<'gc> for ArrayObject<'gc> { - fn get_local( - &self, - name: &str, - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - this: Object<'gc>, - ) -> Result, Error> { - self.0.read().base.get_local(name, avm, context, this) - } - - fn set( - &self, - name: &str, - value: Value<'gc>, - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - ) -> Result<(), Error> { - if let Ok(index) = name.parse::() { - if index >= self.get_length() { - self.set_length(context.gc_context, index + 1); - } - } else if name == "length" { - self.set_length( - context.gc_context, - value - .as_number(avm, context) - .map(|v| v.abs() as i32) - .unwrap_or(0), - ); - } - self.0.read().base.set(name, value, avm, context) - } - - fn call( - &self, - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - this: Object<'gc>, - args: &[Value<'gc>], - ) -> Result, Error> { - self.0.read().base.call(avm, context, this, args) - } - - #[allow(clippy::new_ret_no_self)] - fn new( - &self, - _avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - this: Object<'gc>, - _args: &[Value<'gc>], - ) -> Result, Error> { - Ok(ArrayObject::array(context.gc_context, Some(this)).into()) - } - - fn delete(&self, gc_context: MutationContext<'gc, '_>, name: &str) -> bool { - self.0.read().base.delete(gc_context, name) - } - - fn proto(&self) -> Option> { - self.0.read().base.proto() - } - - fn define_value( - &self, - gc_context: MutationContext<'gc, '_>, - name: &str, - value: Value<'gc>, - attributes: EnumSet, - ) { - self.0 - .read() - .base - .define_value(gc_context, name, value, attributes) - } - - fn add_property( - &self, - gc_context: MutationContext<'gc, '_>, - name: &str, - get: Executable<'gc>, - set: Option>, - attributes: EnumSet, - ) { - self.0 - .read() - .base - .add_property(gc_context, name, get, set, attributes) - } - - fn has_property(&self, name: &str) -> bool { - self.0.read().base.has_property(name) - } - - fn has_own_property(&self, name: &str) -> bool { - self.0.read().base.has_own_property(name) - } - - fn is_property_overwritable(&self, name: &str) -> bool { - self.0.read().base.is_property_overwritable(name) - } - - fn is_property_enumerable(&self, name: &str) -> bool { - self.0.read().base.is_property_enumerable(name) - } - - fn get_keys(&self) -> HashSet { - self.0.read().base.get_keys() - } - - fn get_length(&self) -> i32 { - self.0.read().base.get_length() - } - - fn set_length(&self, gc_context: MutationContext<'gc, '_>, length: i32) { - self.0.read().base.set_length(gc_context, length) - } - - fn as_string(&self) -> String { - self.0.read().base.as_string() - } - - fn type_of(&self) -> &'static str { - self.0.read().base.type_of() - } - - fn as_script_object(&self) -> Option> { - self.0.read().base.as_script_object() - } - - fn as_display_object(&self) -> Option> { - self.0.read().base.as_display_object() - } - - fn as_executable(&self) -> Option> { - self.0.read().base.as_executable() - } - - fn as_ptr(&self) -> *const ObjectPtr { - self.0.read().base.as_ptr() - } -} diff --git a/core/src/avm1/globals/array.rs b/core/src/avm1/globals/array.rs index 7a437cf59..d99c50389 100644 --- a/core/src/avm1/globals/array.rs +++ b/core/src/avm1/globals/array.rs @@ -3,7 +3,7 @@ use crate::avm1::property::Attribute; use crate::avm1::return_value::ReturnValue; -use crate::avm1::{ArrayObject, Avm1, Error, Object, TObject, UpdateContext, Value}; +use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Value}; use enumset::EnumSet; use gc_arena::MutationContext; @@ -20,8 +20,10 @@ pub fn constructor<'gc>( if args.len() == 1 { let arg = args.get(0).unwrap(); if let Ok(length) = arg.as_number(avm, context) { - this.set_length(context.gc_context, length as i32); - consumed = true; + if length >= 0.0 { + this.set_length(context.gc_context, length as usize); + consumed = true; + } } } @@ -49,80 +51,67 @@ pub fn push<'gc>( args: &[Value<'gc>], ) -> Result, Error> { let old_length = this.get_length(); - let new_length = old_length + args.len() as i32; + let new_length = old_length + args.len(); + this.set_length(context.gc_context, new_length); for i in 0..args.len() { - this.define_value( - context.gc_context, - &(old_length + i as i32).to_string(), + this.set_array_element( + old_length + i, args.get(i).unwrap().to_owned(), - EnumSet::empty(), + context.gc_context, ); } - this.set_length(context.gc_context, new_length); - - Ok(new_length.into()) + Ok((new_length as f64).into()) } pub fn unshift<'gc>( - avm: &mut Avm1<'gc>, + _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error> { let old_length = this.get_length(); - let new_length = old_length + args.len() as i32; + let new_length = old_length + args.len(); let offset = new_length - old_length; for i in (old_length - 1..new_length).rev() { - let old = this - .get(&(i - offset).to_string(), avm, context) - .and_then(|v| v.resolve(avm, context)) - .unwrap_or(Value::Undefined); - this.define_value(context.gc_context, &i.to_string(), old, EnumSet::empty()); + this.set_array_element( + i, + this.get_array_element(dbg!(i - offset)), + context.gc_context, + ); } for i in 0..args.len() { - this.define_value( - context.gc_context, - &(i as i32).to_string(), - args.get(i).unwrap().to_owned(), - EnumSet::empty(), - ); + this.set_array_element(i, args.get(i).unwrap().to_owned(), context.gc_context); } this.set_length(context.gc_context, new_length); - Ok(new_length.into()) + Ok((new_length as f64).into()) } pub fn shift<'gc>( - avm: &mut Avm1<'gc>, + _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { let old_length = this.get_length(); - if old_length <= 0 { + if old_length == 0 { return Ok(Value::Undefined.into()); } let new_length = old_length - 1; - let removed = this - .get("0", avm, context) - .and_then(|v| v.resolve(avm, context)) - .unwrap_or(Value::Undefined); + let removed = this.get_array_element(0); for i in 0..new_length { - let old = this - .get(&(i + 1).to_string(), avm, context) - .and_then(|v| v.resolve(avm, context)) - .unwrap_or(Value::Undefined); - this.define_value(context.gc_context, &i.to_string(), old, EnumSet::empty()); + this.set_array_element(i, this.get_array_element(i + 1), context.gc_context); } + this.delete_array_element(new_length, context.gc_context); this.delete(context.gc_context, &new_length.to_string()); this.set_length(context.gc_context, new_length); @@ -131,22 +120,20 @@ pub fn shift<'gc>( } pub fn pop<'gc>( - avm: &mut Avm1<'gc>, + _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { let old_length = this.get_length(); - if old_length <= 0 { + if old_length == 0 { return Ok(Value::Undefined.into()); } let new_length = old_length - 1; - let removed = this - .get(&new_length.to_string(), avm, context) - .and_then(|v| v.resolve(avm, context)) - .unwrap_or(Value::Undefined); + let removed = this.get_array_element(new_length); + this.delete_array_element(new_length, context.gc_context); this.delete(context.gc_context, &new_length.to_string()); this.set_length(context.gc_context, new_length); @@ -155,29 +142,16 @@ pub fn pop<'gc>( } pub fn reverse<'gc>( - avm: &mut Avm1<'gc>, + _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { let length = this.get_length(); - let mut values = Vec::with_capacity(length as usize); + let mut values = this.get_array().to_vec(); for i in 0..length { - values.push( - this.get(&i.to_string(), avm, context) - .and_then(|v| v.resolve(avm, context)) - .unwrap_or(Value::Undefined), - ); - } - - for i in 0..length { - this.define_value( - context.gc_context, - &i.to_string(), - values.pop().unwrap(), - EnumSet::empty(), - ); + this.set_array_element(i, values.pop().unwrap(), context.gc_context); } Ok(Value::Undefined.into()) @@ -189,27 +163,33 @@ pub fn join<'gc>( this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error> { - let length = this.get_length(); - if length < 0 { - return Ok("".into()); - } - let separator = args .get(0) .and_then(|v| v.to_owned().coerce_to_string(avm, context).ok()) .unwrap_or_else(|| ",".to_owned()); - let mut values = Vec::with_capacity(length as usize); + let values: Vec> = this.get_array(); - for i in 0..length { - values.push( - this.get(&i.to_string(), avm, context) - .and_then(|v| v.resolve(avm, context)) - .and_then(|v| v.coerce_to_string(avm, context)) - .unwrap_or_else(|_| "undefined".to_string()), - ); + Ok(values + .iter() + .map(|v| { + v.to_owned() + .coerce_to_string(avm, context) + .unwrap_or_else(|_| "undefined".to_string()) + }) + .collect::>() + .join(&separator) + .into()) +} + +fn make_index_absolute(mut index: i32, length: usize) -> usize { + if index < 0 { + index += length as i32; + } + if index < 0 { + 0 + } else { + index as usize } - - Ok(values.join(&separator).into()) } pub fn slice<'gc>( @@ -218,34 +198,26 @@ pub fn slice<'gc>( this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error> { - let mut start = args + let start = args .get(0) .and_then(|v| v.as_number(avm, context).ok()) - .map(|v| v as i32) + .map(|v| make_index_absolute(v as i32, this.get_length())) .unwrap_or(0); - let mut end = args + let end = args .get(1) .and_then(|v| v.as_number(avm, context).ok()) - .map(|v| v as i32) + .map(|v| make_index_absolute(v as i32, this.get_length())) .unwrap_or_else(|| this.get_length()); - if start < 0 { - start += this.get_length(); - } - if end < 0 { - end += this.get_length(); - } + let array = ScriptObject::array(context.gc_context, Some(avm.prototypes.array)); - let length = end - start; - let array = ArrayObject::array(context.gc_context, Some(avm.prototypes.array)); - array.set_length(context.gc_context, length); + if start < end { + let length = end - start; + array.set_length(context.gc_context, length); - for i in 0..length { - let old = this - .get(&(start + i).to_string(), avm, context) - .and_then(|v| v.resolve(avm, context)) - .unwrap_or(Value::Undefined); - array.define_value(context.gc_context, &i.to_string(), old, EnumSet::empty()); + for i in 0..length { + array.set_array_element(i, this.get_array_element(start + i), context.gc_context); + } } Ok(Value::Object(array.into()).into()) @@ -257,7 +229,7 @@ pub fn concat<'gc>( this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error> { - let array = ArrayObject::array(context.gc_context, Some(avm.prototypes.array)); + let array = ScriptObject::array(context.gc_context, Some(avm.prototypes.array)); let mut length = 0; for i in 0..this.get_length() { @@ -327,7 +299,7 @@ pub fn create_proto<'gc>( proto: Object<'gc>, fn_proto: Object<'gc>, ) -> Object<'gc> { - let array = ArrayObject::array(gc_context, Some(proto)); + let array = ScriptObject::array(gc_context, Some(proto)); let mut object = array.as_script_object().unwrap(); object.force_set_function( diff --git a/core/src/avm1/globals/object.rs b/core/src/avm1/globals/object.rs index 1927c46f1..9091e4115 100644 --- a/core/src/avm1/globals/object.rs +++ b/core/src/avm1/globals/object.rs @@ -17,23 +17,26 @@ pub fn constructor<'gc>( /// Implements `Object.prototype.addProperty` pub fn add_property<'gc>( - _avm: &mut Avm1<'gc>, + avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error> { - let name = args.get(0).unwrap_or(&Value::Undefined); + let name = args + .get(0) + .and_then(|v| v.to_owned().coerce_to_string(avm, context).ok()) + .unwrap_or_else(|| "undefined".to_string()); let getter = args.get(1).unwrap_or(&Value::Undefined); let setter = args.get(2).unwrap_or(&Value::Undefined); - match (name, getter) { - (Value::String(name), Value::Object(get)) if !name.is_empty() => { + match getter { + Value::Object(get) if !name.is_empty() => { if let Some(get_func) = get.as_executable() { if let Value::Object(set) = setter { if let Some(set_func) = set.as_executable() { this.add_property( context.gc_context, - name, + &name, get_func, Some(set_func), EnumSet::empty(), @@ -42,7 +45,7 @@ pub fn add_property<'gc>( return Ok(false.into()); } } else if let Value::Null = setter { - this.add_property(context.gc_context, name, get_func, None, ReadOnly.into()); + this.add_property(context.gc_context, &name, get_func, None, ReadOnly.into()); } else { return Ok(false.into()); } diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index 5487a805c..c13399721 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -3,7 +3,7 @@ use crate::avm1::function::Executable; use crate::avm1::property::Attribute; use crate::avm1::return_value::ReturnValue; -use crate::avm1::{ArrayObject, Avm1, Error, ScriptObject, StageObject, UpdateContext, Value}; +use crate::avm1::{Avm1, Error, ScriptObject, StageObject, UpdateContext, Value}; use crate::display_object::DisplayObject; use enumset::EnumSet; use gc_arena::{Collect, MutationContext}; @@ -19,7 +19,6 @@ use std::fmt::Debug; pub enum Object<'gc> { ScriptObject(ScriptObject<'gc>), StageObject(StageObject<'gc>), - ArrayObject(ArrayObject<'gc>), } )] pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy { @@ -160,12 +159,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy /// Enumerate the object. fn get_keys(&self) -> HashSet; - /// Get the length of this object, as if it were an array. - fn get_length(&self) -> i32; - - /// Sets the length of this object, as if it were an array. - fn set_length(&self, gc_context: MutationContext<'gc, '_>, length: i32); - /// Coerce the object into a string. fn as_string(&self) -> String; @@ -197,6 +190,41 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy false } + + /// Get the length of this object, as if it were an array. + fn get_length(&self) -> usize; + + /// Gets a copy of the array storage behind this object. + fn get_array(&self) -> Vec>; + + /// Sets the length of this object, as if it were an array. + /// + /// Increasing this value will fill the gap with Value::Undefined. + /// Decreasing this value will remove affected items from both the array and properties storage. + fn set_length(&self, gc_context: MutationContext<'gc, '_>, length: usize); + + /// Gets a property of this object as if it were an array. + /// + /// Array element lookups do not respect the prototype chain, and will ignore virtual properties. + fn get_array_element(&self, index: usize) -> Value<'gc>; + + /// Sets a property of this object as if it were an array. + /// + /// This will increase the "length" of this object to encompass the index, and return the new length. + /// Any gap created by increasing the length will be filled with Value::Undefined, both in array + /// and property storage. + fn set_array_element( + &self, + index: usize, + value: Value<'gc>, + gc_context: MutationContext<'gc, '_>, + ) -> usize; + + /// Deletes a property of this object as if it were an array. + /// + /// This will not rearrange the array or adjust the length, nor will it affect the properties + /// storage. + fn delete_array_element(&self, index: usize, gc_context: MutationContext<'gc, '_>); } pub enum ObjectPtr {} diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 3e3288254..d9892aec0 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -12,6 +12,13 @@ use std::collections::{HashMap, HashSet}; pub const TYPE_OF_OBJECT: &str = "object"; pub const TYPE_OF_FUNCTION: &str = "function"; +#[derive(Debug, Clone, Collect)] +#[collect(no_drop)] +pub enum ArrayStorage<'gc> { + Vector(Vec>), + Properties { length: usize }, +} + #[derive(Debug, Copy, Clone, Collect)] #[collect(no_drop)] pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>); @@ -21,7 +28,7 @@ pub struct ScriptObjectData<'gc> { values: HashMap>, function: Option>, type_of: &'static str, - length: i32, + array: ArrayStorage<'gc>, } unsafe impl<'gc> Collect for ScriptObjectData<'gc> { @@ -29,6 +36,7 @@ unsafe impl<'gc> Collect for ScriptObjectData<'gc> { self.prototype.trace(cc); self.values.trace(cc); self.function.trace(cc); + self.array.trace(cc); } } @@ -38,7 +46,7 @@ impl fmt::Debug for ScriptObjectData<'_> { .field("prototype", &self.prototype) .field("values", &self.values) .field("function", &self.function.is_some()) - .field("length", &self.length) + .field("array", &self.array) .finish() } } @@ -55,11 +63,29 @@ impl<'gc> ScriptObject<'gc> { type_of: TYPE_OF_OBJECT, values: HashMap::new(), function: None, - length: 0, + array: ArrayStorage::Properties { length: 0 }, }, )) } + pub fn array( + gc_context: MutationContext<'gc, '_>, + proto: Option>, + ) -> ScriptObject<'gc> { + let object = ScriptObject(GcCell::allocate( + gc_context, + ScriptObjectData { + prototype: proto, + type_of: TYPE_OF_OBJECT, + values: HashMap::new(), + function: None, + array: ArrayStorage::Vector(Vec::new()), + }, + )); + object.sync_native_property("length", gc_context, Some(0.into())); + object + } + /// Constructs and allocates an empty but normal object in one go. pub fn object_cell( gc_context: MutationContext<'gc, '_>, @@ -72,7 +98,7 @@ impl<'gc> ScriptObject<'gc> { type_of: TYPE_OF_OBJECT, values: HashMap::new(), function: None, - length: 0, + array: ArrayStorage::Properties { length: 0 }, }, )) .into() @@ -91,7 +117,7 @@ impl<'gc> ScriptObject<'gc> { type_of: TYPE_OF_OBJECT, values: HashMap::new(), function: None, - length: 0, + array: ArrayStorage::Properties { length: 0 }, }, )) } @@ -109,7 +135,7 @@ impl<'gc> ScriptObject<'gc> { type_of: TYPE_OF_FUNCTION, function: Some(function.into()), values: HashMap::new(), - length: 0, + array: ArrayStorage::Properties { length: 0 }, }, )) } @@ -172,6 +198,37 @@ impl<'gc> ScriptObject<'gc> { pub fn set_type_of(&mut self, gc_context: MutationContext<'gc, '_>, type_of: &'static str) { self.0.write(gc_context).type_of = type_of; } + + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn sync_native_property( + &self, + name: &str, + gc_context: MutationContext<'gc, '_>, + native_value: Option>, + ) { + match self.0.write(gc_context).values.entry(name.to_string()) { + Entry::Occupied(mut entry) => { + if let Property::Stored { value, .. } = entry.get_mut() { + match native_value { + None => { + entry.remove_entry(); + } + Some(native_value) => { + *value = native_value; + } + } + } + } + Entry::Vacant(entry) => { + if let Some(native_value) = native_value { + entry.insert(Property::Stored { + value: native_value, + attributes: Attribute::DontEnum.into(), + }); + } + } + } + } } impl<'gc> TObject<'gc> for ScriptObject<'gc> { @@ -215,7 +272,21 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> { ) -> Result<(), Error> { if name == "__proto__" { self.0.write(context.gc_context).prototype = value.as_object().ok(); + } else if let Ok(index) = name.parse::() { + self.set_array_element(index, value.to_owned(), context.gc_context); } else { + if name == "length" { + let length = value + .as_number(avm, context) + .map(|v| v.abs() as i32) + .unwrap_or(0); + if length > 0 { + self.set_length(context.gc_context, length as usize); + } else { + self.set_length(context.gc_context, 0); + } + } + match self .0 .write(context.gc_context) @@ -264,7 +335,14 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> { this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { - Ok(ScriptObject::object(context.gc_context, Some(this)).into()) + match self.0.read().array { + ArrayStorage::Vector(_) => { + Ok(ScriptObject::array(context.gc_context, Some(this)).into()) + } + ArrayStorage::Properties { .. } => { + Ok(ScriptObject::object(context.gc_context, Some(this)).into()) + } + } } /// Delete a named property from the object. @@ -375,16 +453,6 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> { result } - /// Get the length of this object, as if it were an array. - fn get_length(&self) -> i32 { - self.0.read().length - } - - /// Sets the length of this object, as if it were an array. - fn set_length(&self, gc_context: MutationContext<'gc, '_>, length: i32) { - self.0.write(gc_context).length = length; - } - fn as_string(&self) -> String { if self.0.read().function.is_some() { "[type Function]".to_string() @@ -417,6 +485,104 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> { fn as_ptr(&self) -> *const ObjectPtr { self.0.as_ptr() as *const ObjectPtr } + + fn get_length(&self) -> usize { + match &self.0.read().array { + ArrayStorage::Vector(vector) => vector.len(), + ArrayStorage::Properties { length } => *length, + } + } + + fn set_length(&self, gc_context: MutationContext<'gc, '_>, new_length: usize) { + let mut to_remove = None; + + match &mut self.0.write(gc_context).array { + ArrayStorage::Vector(vector) => { + let old_length = vector.len(); + vector.resize(new_length, Value::Undefined); + if new_length < old_length { + to_remove = Some(new_length..old_length); + } + } + ArrayStorage::Properties { length } => { + *length = new_length; + } + } + if let Some(to_remove) = to_remove { + for i in to_remove { + self.sync_native_property(&i.to_string(), gc_context, None); + } + } + self.sync_native_property("length", gc_context, Some(new_length.into())); + } + + fn get_array(&self) -> Vec> { + match &self.0.read().array { + ArrayStorage::Vector(vector) => vector.to_owned(), + ArrayStorage::Properties { length } => { + let mut values = Vec::new(); + for i in 0..*length { + values.push(self.get_array_element(i)); + } + values + } + } + } + + fn get_array_element(&self, index: usize) -> Value<'gc> { + match &self.0.read().array { + ArrayStorage::Vector(vector) => { + if let Some(value) = vector.get(index) { + value.to_owned() + } else { + Value::Undefined + } + } + ArrayStorage::Properties { length } => { + if index < *length { + if let Some(Property::Stored { value, .. }) = + self.0.read().values.get(&index.to_string()) + { + return value.to_owned(); + } + } + Value::Undefined + } + } + } + + fn set_array_element( + &self, + index: usize, + value: Value<'gc>, + gc_context: MutationContext<'gc, '_>, + ) -> usize { + self.sync_native_property(&index.to_string(), gc_context, Some(value.clone())); + let mut adjust_length = false; + let length = match &mut self.0.write(gc_context).array { + ArrayStorage::Vector(vector) => { + if index >= vector.len() { + vector.resize(index + 1, Value::Undefined); + } + vector[index] = value.clone(); + adjust_length = true; + vector.len() + } + ArrayStorage::Properties { length } => *length, + }; + if adjust_length { + self.sync_native_property("length", gc_context, Some(length.into())); + } + length + } + + fn delete_array_element(&self, index: usize, gc_context: MutationContext<'gc, '_>) { + if let ArrayStorage::Vector(vector) = &mut self.0.write(gc_context).array { + if index < vector.len() { + vector[index] = Value::Undefined; + } + } + } } #[cfg(test)] diff --git a/core/src/avm1/stage_object.rs b/core/src/avm1/stage_object.rs index b49eb49d8..fb27d7b6d 100644 --- a/core/src/avm1/stage_object.rs +++ b/core/src/avm1/stage_object.rs @@ -199,12 +199,33 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { keys } - fn get_length(&self) -> i32 { + fn get_length(&self) -> usize { self.base.get_length() } - fn set_length(&self, gc_context: MutationContext<'gc, '_>, length: i32) { - self.base.set_length(gc_context, length) + fn set_length(&self, gc_context: MutationContext<'gc, '_>, new_length: usize) { + self.base.set_length(gc_context, new_length) + } + + fn get_array(&self) -> Vec> { + self.base.get_array() + } + + fn get_array_element(&self, index: usize) -> Value<'gc> { + self.base.get_array_element(index) + } + + fn set_array_element( + &self, + index: usize, + value: Value<'gc>, + gc_context: MutationContext<'gc, '_>, + ) -> usize { + self.base.set_array_element(index, value, gc_context) + } + + fn delete_array_element(&self, index: usize, gc_context: MutationContext<'gc, '_>) { + self.base.delete_array_element(index, gc_context) } fn as_string(&self) -> String { diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 98f788fb4..aca4a8769 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -63,6 +63,8 @@ swf_tests! { (array_concat, "avm1/array_concat", 1), (array_slice, "avm1/array_slice", 1), (array_properties, "avm1/array_properties", 1), + (array_prototyping, "avm1/array_prototyping", 1), + (array_vs_object_length, "avm1/array_vs_object_length", 1), (timeline_function_def, "avm1/timeline_function_def", 3), (root_global_parent, "avm1/root_global_parent", 3), (register_underflow, "avm1/register_underflow", 1), diff --git a/core/tests/swfs/avm1/array_prototyping/output.txt b/core/tests/swfs/avm1/array_prototyping/output.txt new file mode 100644 index 000000000..60fabc969 --- /dev/null +++ b/core/tests/swfs/avm1/array_prototyping/output.txt @@ -0,0 +1,12 @@ +// array +undefined,undefined,undefined +// array[0] +a +// array[1] +b +// array[2] +x +// array[3] +undefined +// array.length +3 diff --git a/core/tests/swfs/avm1/array_prototyping/test.fla b/core/tests/swfs/avm1/array_prototyping/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..3882f1ab4fb52b5dafb0de94f845cdee490effe4 GIT binary patch literal 4709 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 C3}R>i literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/array_vs_object_length/output.txt b/core/tests/swfs/avm1/array_vs_object_length/output.txt new file mode 100644 index 000000000..c94c150d6 --- /dev/null +++ b/core/tests/swfs/avm1/array_vs_object_length/output.txt @@ -0,0 +1,100 @@ +// array.length +1 + +// array +x + +// array[0] +x + + +// array.length = 0 + + +// array.length +0 + +// array + + +// array[0] +undefined + + +// array.length = 1 + + +// array.length +1 + +// array +undefined + +// array[0] +undefined + + +// array.length = 3 + + +// array.length +3 + +// array +undefined,undefined,undefined + +// array[0] +undefined + + + + + +// object.length +undefined + +// object + + +// object[0] +x + + +// object.length = 0 + + +// object.length +0 + +// object + + +// object[0] +x + + +// object.length = 1 + + +// object.length +1 + +// object +x + +// object[0] +x + + +// object.length = 3 + + +// object.length +3 + +// object +x,undefined,undefined + +// object[0] +x + diff --git a/core/tests/swfs/avm1/array_vs_object_length/test.fla b/core/tests/swfs/avm1/array_vs_object_length/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..d9bc2b5c2dbad7eb0237c135f8cb73ab16a23354 GIT binary patch literal 4768 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 S(}feVDpT-YZ2kbVNOIu)vx1!f literal 0 HcmV?d00001