core: Made arrays a storage property of objects, not a unique object type. Added more corner case tests.
This commit is contained in:
parent
3bdf710af6
commit
31b84c5f19
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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<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));
|
||||
|
||||
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<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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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,7 +335,14 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> {
|
|||
this: Object<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Object<'gc>, 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<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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
@ -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.
Loading…
Reference in New Issue