core: Made arrays a storage property of objects, not a unique object type. Added more corner case tests.

This commit is contained in:
Nathan Adams 2019-12-13 15:50:58 +01:00 committed by Mike Welsh
parent 3bdf710af6
commit 31b84c5f19
14 changed files with 434 additions and 331 deletions

View File

@ -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(())
}

View File

@ -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<ReturnValue<'gc>, Error> {
Ok(this.get_length().into())
}
impl<'gc> ArrayObject<'gc> {
pub fn array(
gc_context: MutationContext<'gc, '_>,
proto: Option<Object<'gc>>,
) -> 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<ReturnValue<'gc>, 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::<i32>() {
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<ReturnValue<'gc>, 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<Object<'gc>, 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<Object<'gc>> {
self.0.read().base.proto()
}
fn define_value(
&self,
gc_context: MutationContext<'gc, '_>,
name: &str,
value: Value<'gc>,
attributes: EnumSet<Attribute>,
) {
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<Executable<'gc>>,
attributes: EnumSet<Attribute>,
) {
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<String, RandomState> {
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<ScriptObject<'gc>> {
self.0.read().base.as_script_object()
}
fn as_display_object(&self) -> Option<DisplayObject<'gc>> {
self.0.read().base.as_display_object()
}
fn as_executable(&self) -> Option<Executable<'gc>> {
self.0.read().base.as_executable()
}
fn as_ptr(&self) -> *const ObjectPtr {
self.0.read().base.as_ptr()
}
}

View File

@ -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,10 +20,12 @@ 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);
if length >= 0.0 {
this.set_length(context.gc_context, length as usize);
consumed = true;
}
}
}
if !consumed {
let mut length = 0;
@ -49,80 +51,67 @@ pub fn push<'gc>(
args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<Value<'gc>> = 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::<Vec<String>>()
.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<ReturnValue<'gc>, 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));
if start < end {
let length = end - start;
let array = ArrayObject::array(context.gc_context, Some(avm.prototypes.array));
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());
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<ReturnValue<'gc>, 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(

View File

@ -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<ReturnValue<'gc>, 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());
}

View File

@ -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<Object<'gc>> + Clone + Copy {
@ -160,12 +159,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
/// Enumerate the object.
fn get_keys(&self) -> HashSet<String>;
/// 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<Object<'gc>> + 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<Value<'gc>>;
/// 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 {}

View File

@ -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<Value<'gc>>),
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<String, Property<'gc>>,
function: Option<Executable<'gc>>,
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<Object<'gc>>,
) -> 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<Value<'gc>>,
) {
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::<usize>() {
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,8 +335,15 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> {
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Object<'gc>, Error> {
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<Value<'gc>> {
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)]

View File

@ -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<Value<'gc>> {
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 {

View File

@ -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),

View File

@ -0,0 +1,12 @@
// array
undefined,undefined,undefined
// array[0]
a
// array[1]
b
// array[2]
x
// array[3]
undefined
// array.length
3

Binary file not shown.

Binary file not shown.

View File

@ -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

Binary file not shown.

Binary file not shown.