2019-10-25 03:21:35 +00:00
|
|
|
use crate::avm1::function::{Executable, NativeFunction};
|
|
|
|
use crate::avm1::property::{Attribute, Property};
|
|
|
|
use crate::avm1::return_value::ReturnValue;
|
2019-12-06 18:24:36 +00:00
|
|
|
use crate::avm1::{Avm1, Error, Object, ObjectPtr, TObject, UpdateContext, Value};
|
2019-12-07 02:29:36 +00:00
|
|
|
use crate::display_object::DisplayObject;
|
2019-10-25 03:21:35 +00:00
|
|
|
use core::fmt;
|
|
|
|
use enumset::EnumSet;
|
2019-12-06 18:24:36 +00:00
|
|
|
use gc_arena::{Collect, GcCell, MutationContext};
|
2019-10-25 03:21:35 +00:00
|
|
|
use std::collections::hash_map::Entry;
|
2019-11-27 21:30:01 +00:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2019-10-25 03:21:35 +00:00
|
|
|
|
|
|
|
pub const TYPE_OF_OBJECT: &str = "object";
|
|
|
|
pub const TYPE_OF_FUNCTION: &str = "function";
|
|
|
|
pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip";
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
#[derive(Debug, Copy, Clone, Collect)]
|
|
|
|
#[collect(no_drop)]
|
|
|
|
pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>);
|
|
|
|
|
|
|
|
pub struct ScriptObjectData<'gc> {
|
|
|
|
prototype: Option<Object<'gc>>,
|
2019-12-07 02:29:36 +00:00
|
|
|
display_node: Option<DisplayObject<'gc>>,
|
2019-10-25 03:21:35 +00:00
|
|
|
values: HashMap<String, Property<'gc>>,
|
|
|
|
function: Option<Executable<'gc>>,
|
|
|
|
type_of: &'static str,
|
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
unsafe impl<'gc> Collect for ScriptObjectData<'gc> {
|
2019-10-25 03:21:35 +00:00
|
|
|
fn trace(&self, cc: gc_arena::CollectionContext) {
|
|
|
|
self.prototype.trace(cc);
|
|
|
|
self.display_node.trace(cc);
|
|
|
|
self.values.trace(cc);
|
2019-11-26 20:07:03 +00:00
|
|
|
self.function.trace(cc);
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
impl fmt::Debug for ScriptObjectData<'_> {
|
2019-10-25 03:21:35 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
f.debug_struct("Object")
|
|
|
|
.field("prototype", &self.prototype)
|
|
|
|
.field("display_node", &self.display_node)
|
|
|
|
.field("values", &self.values)
|
|
|
|
.field("function", &self.function.is_some())
|
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'gc> ScriptObject<'gc> {
|
|
|
|
pub fn object(
|
2019-12-06 18:24:36 +00:00
|
|
|
gc_context: MutationContext<'gc, '_>,
|
|
|
|
proto: Option<Object<'gc>>,
|
2019-10-25 03:21:35 +00:00
|
|
|
) -> ScriptObject<'gc> {
|
2019-12-06 18:24:36 +00:00
|
|
|
ScriptObject(GcCell::allocate(
|
|
|
|
gc_context,
|
|
|
|
ScriptObjectData {
|
|
|
|
prototype: proto,
|
|
|
|
type_of: TYPE_OF_OBJECT,
|
|
|
|
display_node: None,
|
|
|
|
values: HashMap::new(),
|
|
|
|
function: None,
|
|
|
|
},
|
|
|
|
))
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Constructs and allocates an empty but normal object in one go.
|
|
|
|
pub fn object_cell(
|
|
|
|
gc_context: MutationContext<'gc, '_>,
|
2019-12-06 18:24:36 +00:00
|
|
|
proto: Option<Object<'gc>>,
|
|
|
|
) -> Object<'gc> {
|
|
|
|
ScriptObject(GcCell::allocate(
|
2019-10-25 03:21:35 +00:00
|
|
|
gc_context,
|
2019-12-06 18:24:36 +00:00
|
|
|
ScriptObjectData {
|
2019-10-25 03:21:35 +00:00
|
|
|
prototype: proto,
|
|
|
|
type_of: TYPE_OF_OBJECT,
|
|
|
|
display_node: None,
|
|
|
|
values: HashMap::new(),
|
|
|
|
function: None,
|
2019-12-06 18:24:36 +00:00
|
|
|
},
|
|
|
|
))
|
|
|
|
.into()
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Constructs an object with no values, not even builtins.
|
|
|
|
///
|
|
|
|
/// Intended for constructing scope chains, since they exclusively use the
|
|
|
|
/// object values, but can't just have a hashmap because of `with` and
|
|
|
|
/// friends.
|
2019-12-06 18:24:36 +00:00
|
|
|
pub fn bare_object(gc_context: MutationContext<'gc, '_>) -> Self {
|
|
|
|
ScriptObject(GcCell::allocate(
|
|
|
|
gc_context,
|
|
|
|
ScriptObjectData {
|
|
|
|
prototype: None,
|
|
|
|
type_of: TYPE_OF_OBJECT,
|
|
|
|
display_node: None,
|
|
|
|
values: HashMap::new(),
|
|
|
|
function: None,
|
|
|
|
},
|
|
|
|
))
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Construct a function sans prototype.
|
|
|
|
pub fn bare_function(
|
2019-12-06 18:24:36 +00:00
|
|
|
gc_context: MutationContext<'gc, '_>,
|
2019-10-25 03:21:35 +00:00
|
|
|
function: impl Into<Executable<'gc>>,
|
2019-12-06 18:24:36 +00:00
|
|
|
fn_proto: Option<Object<'gc>>,
|
2019-10-25 03:21:35 +00:00
|
|
|
) -> Self {
|
2019-12-06 18:24:36 +00:00
|
|
|
ScriptObject(GcCell::allocate(
|
|
|
|
gc_context,
|
|
|
|
ScriptObjectData {
|
|
|
|
prototype: fn_proto,
|
|
|
|
type_of: TYPE_OF_FUNCTION,
|
|
|
|
function: Some(function.into()),
|
|
|
|
display_node: None,
|
|
|
|
values: HashMap::new(),
|
|
|
|
},
|
|
|
|
))
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Construct a function from an executable and associated protos.
|
|
|
|
///
|
|
|
|
/// Since prototypes need to link back to themselves, this function builds
|
|
|
|
/// both objects itself and returns the function to you, fully allocated.
|
|
|
|
///
|
|
|
|
/// `fn_proto` refers to the implicit proto of the function object, and the
|
|
|
|
/// `prototype` refers to the explicit prototype of the function. If
|
|
|
|
/// provided, the function and it's prototype will be linked to each other.
|
|
|
|
pub fn function(
|
|
|
|
gc_context: MutationContext<'gc, '_>,
|
|
|
|
function: impl Into<Executable<'gc>>,
|
2019-12-06 18:24:36 +00:00
|
|
|
fn_proto: Option<Object<'gc>>,
|
|
|
|
prototype: Option<Object<'gc>>,
|
|
|
|
) -> Object<'gc> {
|
|
|
|
let function = Self::bare_function(gc_context, function, fn_proto).into();
|
2019-10-25 03:21:35 +00:00
|
|
|
|
|
|
|
//TODO: Can we make these proper sets or no?
|
|
|
|
if let Some(p) = prototype {
|
2019-12-06 18:24:36 +00:00
|
|
|
p.define_value(
|
|
|
|
gc_context,
|
2019-10-25 23:30:05 +00:00
|
|
|
"constructor",
|
|
|
|
Value::Object(function),
|
2019-11-27 21:30:01 +00:00
|
|
|
Attribute::DontEnum.into(),
|
2019-10-25 23:30:05 +00:00
|
|
|
);
|
2019-12-06 18:24:36 +00:00
|
|
|
function.define_value(gc_context, "prototype", p.into(), EnumSet::empty());
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function
|
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
pub fn set_display_node(
|
|
|
|
self,
|
|
|
|
gc_context: MutationContext<'gc, '_>,
|
2019-12-07 02:29:36 +00:00
|
|
|
display_node: DisplayObject<'gc>,
|
2019-12-06 18:24:36 +00:00
|
|
|
) {
|
|
|
|
self.0.write(gc_context).display_node = Some(display_node);
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
#[allow(dead_code)]
|
2019-12-07 02:29:36 +00:00
|
|
|
pub fn display_node(&self) -> Option<DisplayObject<'gc>> {
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0.read().display_node
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Declare a native function on the current object.
|
|
|
|
///
|
|
|
|
/// This is intended for use with defining host object prototypes. Notably,
|
|
|
|
/// this creates a function object without an explicit `prototype`, which
|
|
|
|
/// is only possible when defining host functions. User-defined functions
|
|
|
|
/// always get a fresh explicit prototype, so you should never force set a
|
|
|
|
/// user-defined function.
|
|
|
|
pub fn force_set_function<A>(
|
|
|
|
&mut self,
|
|
|
|
name: &str,
|
|
|
|
function: NativeFunction<'gc>,
|
|
|
|
gc_context: MutationContext<'gc, '_>,
|
|
|
|
attributes: A,
|
2019-12-06 18:24:36 +00:00
|
|
|
fn_proto: Option<Object<'gc>>,
|
2019-10-25 03:21:35 +00:00
|
|
|
) where
|
|
|
|
A: Into<EnumSet<Attribute>>,
|
|
|
|
{
|
2019-10-25 23:30:05 +00:00
|
|
|
self.define_value(
|
2019-12-06 18:24:36 +00:00
|
|
|
gc_context,
|
2019-10-25 03:21:35 +00:00
|
|
|
name,
|
|
|
|
Value::Object(ScriptObject::function(gc_context, function, fn_proto, None)),
|
2019-10-25 23:30:05 +00:00
|
|
|
attributes.into(),
|
2019-10-25 03:21:35 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
pub fn set_prototype(&mut self, gc_context: MutationContext<'gc, '_>, prototype: Object<'gc>) {
|
|
|
|
self.0.write(gc_context).prototype = Some(prototype);
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
pub fn set_type_of(&mut self, gc_context: MutationContext<'gc, '_>, type_of: &'static str) {
|
|
|
|
self.0.write(gc_context).type_of = type_of;
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
impl<'gc> TObject<'gc> for ScriptObject<'gc> {
|
2019-10-25 03:21:35 +00:00
|
|
|
/// Get the value of a particular property on this object.
|
|
|
|
///
|
|
|
|
/// The `avm`, `context`, and `this` parameters exist so that this object
|
|
|
|
/// can call virtual properties. Furthermore, since some virtual properties
|
|
|
|
/// may resolve on the AVM stack, this function may return `None` instead
|
|
|
|
/// of a `Value`. *This is not equivalent to `undefined`.* Instead, it is a
|
|
|
|
/// signal that your value will be returned on the ActionScript stack, and
|
|
|
|
/// that you should register a stack continuation in order to get it.
|
2019-11-27 13:59:16 +00:00
|
|
|
fn get_local(
|
2019-10-25 03:21:35 +00:00
|
|
|
&self,
|
|
|
|
name: &str,
|
|
|
|
avm: &mut Avm1<'gc>,
|
|
|
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
2019-12-06 18:24:36 +00:00
|
|
|
this: Object<'gc>,
|
2019-10-25 03:21:35 +00:00
|
|
|
) -> Result<ReturnValue<'gc>, Error> {
|
|
|
|
if name == "__proto__" {
|
2019-12-06 18:24:36 +00:00
|
|
|
return Ok(self.proto().map_or(Value::Undefined, Value::Object).into());
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
if let Some(value) = self.0.read().values.get(name) {
|
2019-10-25 03:21:35 +00:00
|
|
|
return value.get(avm, context, this);
|
|
|
|
}
|
|
|
|
|
2019-11-27 13:59:16 +00:00
|
|
|
Ok(Value::Undefined.into())
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Set a named property on the object.
|
|
|
|
///
|
|
|
|
/// This function takes a redundant `this` parameter which should be
|
|
|
|
/// the object's own `GcCell`, so that it can pass it to user-defined
|
|
|
|
/// overrides that may need to interact with the underlying object.
|
|
|
|
fn set(
|
2019-12-06 18:24:36 +00:00
|
|
|
&self,
|
2019-10-25 03:21:35 +00:00
|
|
|
name: &str,
|
|
|
|
value: Value<'gc>,
|
|
|
|
avm: &mut Avm1<'gc>,
|
|
|
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
2019-12-06 18:24:36 +00:00
|
|
|
this: Object<'gc>,
|
2019-10-25 03:21:35 +00:00
|
|
|
) -> Result<(), Error> {
|
|
|
|
if name == "__proto__" {
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0.write(context.gc_context).prototype = value.as_object().ok();
|
2019-10-25 03:21:35 +00:00
|
|
|
} else {
|
2019-12-06 18:24:36 +00:00
|
|
|
match self
|
|
|
|
.0
|
|
|
|
.write(context.gc_context)
|
|
|
|
.values
|
|
|
|
.entry(name.to_owned())
|
|
|
|
{
|
2019-10-25 03:21:35 +00:00
|
|
|
Entry::Occupied(mut entry) => {
|
|
|
|
entry.get_mut().set(avm, context, this, value)?;
|
|
|
|
}
|
|
|
|
Entry::Vacant(entry) => {
|
|
|
|
entry.insert(Property::Stored {
|
|
|
|
value,
|
|
|
|
attributes: Default::default(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Call the underlying object.
|
|
|
|
///
|
|
|
|
/// This function takes a redundant `this` parameter which should be
|
|
|
|
/// the object's own `GcCell`, so that it can pass it to user-defined
|
|
|
|
/// overrides that may need to interact with the underlying object.
|
|
|
|
fn call(
|
|
|
|
&self,
|
|
|
|
avm: &mut Avm1<'gc>,
|
|
|
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
2019-12-06 18:24:36 +00:00
|
|
|
this: Object<'gc>,
|
2019-10-25 03:21:35 +00:00
|
|
|
args: &[Value<'gc>],
|
|
|
|
) -> Result<ReturnValue<'gc>, Error> {
|
2019-12-06 18:24:36 +00:00
|
|
|
if let Some(function) = &self.0.read().function {
|
2019-10-25 03:21:35 +00:00
|
|
|
function.exec(avm, context, this, args)
|
|
|
|
} else {
|
|
|
|
Ok(Value::Undefined.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-25 18:35:07 +00:00
|
|
|
#[allow(clippy::new_ret_no_self)]
|
|
|
|
fn new(
|
|
|
|
&self,
|
2019-10-28 03:54:35 +00:00
|
|
|
_avm: &mut Avm1<'gc>,
|
2019-10-25 18:35:07 +00:00
|
|
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
2019-12-06 18:24:36 +00:00
|
|
|
this: Object<'gc>,
|
2019-10-25 18:35:07 +00:00
|
|
|
_args: &[Value<'gc>],
|
2019-12-06 18:24:36 +00:00
|
|
|
) -> Result<Object<'gc>, Error> {
|
|
|
|
Ok(ScriptObject::object(context.gc_context, Some(this)).into())
|
2019-10-25 18:35:07 +00:00
|
|
|
}
|
|
|
|
|
2019-10-25 03:21:35 +00:00
|
|
|
/// Delete a named property from the object.
|
|
|
|
///
|
|
|
|
/// Returns false if the property cannot be deleted.
|
2019-12-06 18:24:36 +00:00
|
|
|
fn delete(&self, gc_context: MutationContext<'gc, '_>, name: &str) -> bool {
|
|
|
|
let mut object = self.0.write(gc_context);
|
|
|
|
if let Some(prop) = object.values.get(name) {
|
2019-10-25 03:21:35 +00:00
|
|
|
if prop.can_delete() {
|
2019-12-06 18:24:36 +00:00
|
|
|
object.values.remove(name);
|
2019-10-25 03:21:35 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
2019-10-25 23:30:05 +00:00
|
|
|
fn add_property(
|
2019-12-06 18:24:36 +00:00
|
|
|
&self,
|
|
|
|
gc_context: MutationContext<'gc, '_>,
|
2019-10-25 23:30:05 +00:00
|
|
|
name: &str,
|
|
|
|
get: Executable<'gc>,
|
|
|
|
set: Option<Executable<'gc>>,
|
|
|
|
attributes: EnumSet<Attribute>,
|
|
|
|
) {
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0.write(gc_context).values.insert(
|
2019-10-25 23:30:05 +00:00
|
|
|
name.to_owned(),
|
|
|
|
Property::Virtual {
|
|
|
|
get,
|
|
|
|
set,
|
|
|
|
attributes,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
fn define_value(
|
|
|
|
&self,
|
|
|
|
gc_context: MutationContext<'gc, '_>,
|
|
|
|
name: &str,
|
|
|
|
value: Value<'gc>,
|
|
|
|
attributes: EnumSet<Attribute>,
|
|
|
|
) {
|
|
|
|
self.0
|
|
|
|
.write(gc_context)
|
|
|
|
.values
|
2019-10-25 23:30:05 +00:00
|
|
|
.insert(name.to_string(), Property::Stored { value, attributes });
|
|
|
|
}
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
fn proto(&self) -> Option<Object<'gc>> {
|
|
|
|
self.0.read().prototype
|
2019-10-25 22:02:47 +00:00
|
|
|
}
|
|
|
|
|
2019-10-25 03:21:35 +00:00
|
|
|
/// Checks if the object has a given named property.
|
|
|
|
fn has_property(&self, name: &str) -> bool {
|
|
|
|
self.has_own_property(name)
|
|
|
|
|| self
|
2019-12-06 18:24:36 +00:00
|
|
|
.proto()
|
2019-10-25 03:21:35 +00:00
|
|
|
.as_ref()
|
2019-12-06 18:24:36 +00:00
|
|
|
.map_or(false, |p| p.has_property(name))
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Checks if the object has a given named property on itself (and not,
|
|
|
|
/// say, the object's prototype or superclass)
|
|
|
|
fn has_own_property(&self, name: &str) -> bool {
|
|
|
|
if name == "__proto__" {
|
|
|
|
return true;
|
|
|
|
}
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0.read().values.contains_key(name)
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn is_property_overwritable(&self, name: &str) -> bool {
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0
|
|
|
|
.read()
|
|
|
|
.values
|
2019-10-25 03:21:35 +00:00
|
|
|
.get(name)
|
|
|
|
.map(|p| p.is_overwritable())
|
|
|
|
.unwrap_or(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Checks if a named property appears when enumerating the object.
|
|
|
|
fn is_property_enumerable(&self, name: &str) -> bool {
|
2019-12-06 18:24:36 +00:00
|
|
|
if let Some(prop) = self.0.read().values.get(name) {
|
2019-10-25 03:21:35 +00:00
|
|
|
prop.is_enumerable()
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Enumerate the object.
|
2019-11-27 21:30:01 +00:00
|
|
|
fn get_keys(&self) -> HashSet<String> {
|
2019-12-06 18:24:36 +00:00
|
|
|
let mut result = self.proto().map_or_else(HashSet::new, |p| p.get_keys());
|
2019-11-27 21:30:01 +00:00
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0
|
|
|
|
.read()
|
|
|
|
.values
|
2019-10-25 03:21:35 +00:00
|
|
|
.iter()
|
|
|
|
.filter_map(|(k, p)| {
|
|
|
|
if p.is_enumerable() {
|
|
|
|
Some(k.to_string())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
2019-11-27 21:30:01 +00:00
|
|
|
.for_each(|k| {
|
|
|
|
result.insert(k);
|
|
|
|
});
|
|
|
|
|
|
|
|
result
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn as_string(&self) -> String {
|
2019-12-06 18:24:36 +00:00
|
|
|
if self.0.read().function.is_some() {
|
2019-10-25 03:21:35 +00:00
|
|
|
"[type Function]".to_string()
|
|
|
|
} else {
|
|
|
|
"[object Object]".to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn type_of(&self) -> &'static str {
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0.read().type_of
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn as_script_object(&self) -> Option<&ScriptObject<'gc>> {
|
|
|
|
Some(self)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn as_script_object_mut(&mut self) -> Option<&mut ScriptObject<'gc>> {
|
|
|
|
Some(self)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the underlying display node for this object, if it exists.
|
2019-12-07 02:29:36 +00:00
|
|
|
fn as_display_node(&self) -> Option<DisplayObject<'gc>> {
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0.read().display_node
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a copy of a given function.
|
|
|
|
///
|
|
|
|
/// TODO: We have to clone here because of how executables are stored on
|
|
|
|
/// objects directly. This might not be a good idea for performance.
|
|
|
|
fn as_executable(&self) -> Option<Executable<'gc>> {
|
2019-12-06 18:24:36 +00:00
|
|
|
self.0.read().function.clone()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn as_ptr(&self) -> *const ObjectPtr {
|
|
|
|
self.0.as_ptr() as *const ObjectPtr
|
2019-10-25 03:21:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
use crate::avm1::activation::Activation;
|
|
|
|
use crate::avm1::property::Attribute::*;
|
|
|
|
use crate::backend::audio::NullAudioBackend;
|
|
|
|
use crate::backend::navigator::NullNavigatorBackend;
|
|
|
|
use crate::backend::render::NullRenderer;
|
2019-12-06 18:24:36 +00:00
|
|
|
use crate::display_object::MovieClip;
|
2019-10-25 03:21:35 +00:00
|
|
|
use crate::library::Library;
|
|
|
|
use crate::prelude::*;
|
|
|
|
use gc_arena::rootless_arena;
|
|
|
|
use rand::{rngs::SmallRng, SeedableRng};
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
fn with_object<F, R>(swf_version: u8, test: F) -> R
|
|
|
|
where
|
2019-12-06 18:24:36 +00:00
|
|
|
F: for<'a, 'gc> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, Object<'gc>) -> R,
|
2019-10-25 03:21:35 +00:00
|
|
|
{
|
|
|
|
rootless_arena(|gc_context| {
|
|
|
|
let mut avm = Avm1::new(gc_context, swf_version);
|
2019-12-07 02:29:36 +00:00
|
|
|
let root = MovieClip::new(swf_version, gc_context).into();
|
2019-10-25 03:21:35 +00:00
|
|
|
let mut context = UpdateContext {
|
|
|
|
gc_context,
|
|
|
|
global_time: 0,
|
|
|
|
player_version: 32,
|
|
|
|
swf_version,
|
|
|
|
root,
|
|
|
|
start_clip: root,
|
|
|
|
active_clip: root,
|
|
|
|
target_clip: Some(root),
|
|
|
|
target_path: Value::Undefined,
|
|
|
|
rng: &mut SmallRng::from_seed([0u8; 16]),
|
|
|
|
action_queue: &mut crate::context::ActionQueue::new(),
|
|
|
|
audio: &mut NullAudioBackend::new(),
|
|
|
|
background_color: &mut Color {
|
|
|
|
r: 0,
|
|
|
|
g: 0,
|
|
|
|
b: 0,
|
|
|
|
a: 0,
|
|
|
|
},
|
|
|
|
library: &mut Library::new(),
|
|
|
|
navigator: &mut NullNavigatorBackend::new(),
|
|
|
|
renderer: &mut NullRenderer::new(),
|
|
|
|
swf_data: &mut Arc::new(vec![]),
|
|
|
|
system_prototypes: avm.prototypes().clone(),
|
|
|
|
};
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
let object = ScriptObject::object(gc_context, Some(avm.prototypes().object)).into();
|
2019-10-25 03:21:35 +00:00
|
|
|
|
|
|
|
let globals = avm.global_object_cell();
|
|
|
|
avm.insert_stack_frame(GcCell::allocate(
|
|
|
|
gc_context,
|
|
|
|
Activation::from_nothing(swf_version, globals, gc_context),
|
|
|
|
));
|
|
|
|
|
|
|
|
test(&mut avm, &mut context, object)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_get_undefined() {
|
|
|
|
with_object(0, |avm, context, object| {
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("not_defined", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate(Value::Undefined)
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_set_get() {
|
2019-12-06 18:24:36 +00:00
|
|
|
with_object(0, |avm, context, mut object| {
|
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"forced",
|
|
|
|
"forced".into(),
|
|
|
|
EnumSet::empty(),
|
|
|
|
);
|
2019-10-25 03:21:35 +00:00
|
|
|
object
|
|
|
|
.set("natural", "natural".into(), avm, context, object)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("forced", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("forced".into())
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("natural", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("natural".into())
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_set_readonly() {
|
2019-12-06 18:24:36 +00:00
|
|
|
with_object(0, |avm, context, mut object| {
|
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"normal",
|
|
|
|
"initial".into(),
|
|
|
|
EnumSet::empty(),
|
|
|
|
);
|
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"readonly",
|
|
|
|
"initial".into(),
|
|
|
|
ReadOnly.into(),
|
|
|
|
);
|
2019-10-25 03:21:35 +00:00
|
|
|
|
|
|
|
object
|
|
|
|
.set("normal", "replaced".into(), avm, context, object)
|
|
|
|
.unwrap();
|
|
|
|
object
|
|
|
|
.set("readonly", "replaced".into(), avm, context, object)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("normal", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("replaced".into())
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("readonly", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("initial".into())
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_deletable_not_readonly() {
|
2019-12-06 18:24:36 +00:00
|
|
|
with_object(0, |avm, context, mut object| {
|
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"test",
|
|
|
|
"initial".into(),
|
|
|
|
DontDelete.into(),
|
|
|
|
);
|
2019-10-25 03:21:35 +00:00
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
assert_eq!(object.delete(context.gc_context, "test"), false);
|
2019-10-25 03:21:35 +00:00
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("test", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("initial".into())
|
|
|
|
);
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
let this = object;
|
2019-10-25 03:21:35 +00:00
|
|
|
object
|
|
|
|
.as_script_object_mut()
|
|
|
|
.unwrap()
|
2019-12-06 18:24:36 +00:00
|
|
|
.set("test", "replaced".into(), avm, context, this)
|
2019-10-25 03:21:35 +00:00
|
|
|
.unwrap();
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
assert_eq!(object.delete(context.gc_context, "test"), false);
|
2019-10-25 03:21:35 +00:00
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("test", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("replaced".into())
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_virtual_get() {
|
2019-12-06 18:24:36 +00:00
|
|
|
with_object(0, |avm, context, mut object| {
|
2019-10-25 03:21:35 +00:00
|
|
|
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
|
|
|
Ok(ReturnValue::Immediate("Virtual!".into()))
|
|
|
|
});
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
object.as_script_object_mut().unwrap().add_property(
|
|
|
|
context.gc_context,
|
|
|
|
"test",
|
|
|
|
getter,
|
|
|
|
None,
|
|
|
|
EnumSet::empty(),
|
|
|
|
);
|
2019-10-25 03:21:35 +00:00
|
|
|
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("test", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("Virtual!".into())
|
|
|
|
);
|
|
|
|
|
|
|
|
// This set should do nothing
|
|
|
|
object
|
|
|
|
.set("test", "Ignored!".into(), avm, context, object)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("test", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("Virtual!".into())
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_delete() {
|
2019-12-06 18:24:36 +00:00
|
|
|
with_object(0, |avm, context, mut object| {
|
2019-10-25 03:21:35 +00:00
|
|
|
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
|
|
|
Ok(ReturnValue::Immediate("Virtual!".into()))
|
|
|
|
});
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
object.as_script_object_mut().unwrap().add_property(
|
|
|
|
context.gc_context,
|
|
|
|
"virtual",
|
|
|
|
getter.clone(),
|
|
|
|
None,
|
|
|
|
EnumSet::empty(),
|
2019-10-25 03:21:35 +00:00
|
|
|
);
|
2019-12-06 18:24:36 +00:00
|
|
|
object.as_script_object_mut().unwrap().add_property(
|
|
|
|
context.gc_context,
|
|
|
|
"virtual_un",
|
|
|
|
getter,
|
|
|
|
None,
|
|
|
|
DontDelete.into(),
|
|
|
|
);
|
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"stored",
|
|
|
|
"Stored!".into(),
|
|
|
|
EnumSet::empty(),
|
|
|
|
);
|
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"stored_un",
|
|
|
|
"Stored!".into(),
|
|
|
|
DontDelete.into(),
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(object.delete(context.gc_context, "virtual"), true);
|
|
|
|
assert_eq!(object.delete(context.gc_context, "virtual_un"), false);
|
|
|
|
assert_eq!(object.delete(context.gc_context, "stored"), true);
|
|
|
|
assert_eq!(object.delete(context.gc_context, "stored_un"), false);
|
|
|
|
assert_eq!(object.delete(context.gc_context, "non_existent"), false);
|
2019-10-25 03:21:35 +00:00
|
|
|
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("virtual", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate(Value::Undefined)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("virtual_un", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("Virtual!".into())
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("stored", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate(Value::Undefined)
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2019-12-06 18:24:36 +00:00
|
|
|
object.get("stored_un", avm, context, object).unwrap(),
|
2019-10-25 03:21:35 +00:00
|
|
|
ReturnValue::Immediate("Stored!".into())
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_iter_values() {
|
2019-12-06 18:24:36 +00:00
|
|
|
with_object(0, |_avm, context, mut object| {
|
2019-10-25 03:21:35 +00:00
|
|
|
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
|
|
|
Ok(ReturnValue::Immediate(Value::Null))
|
|
|
|
});
|
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"stored",
|
|
|
|
Value::Null,
|
|
|
|
EnumSet::empty(),
|
|
|
|
);
|
|
|
|
object.as_script_object_mut().unwrap().define_value(
|
|
|
|
context.gc_context,
|
|
|
|
"stored_hidden",
|
|
|
|
Value::Null,
|
|
|
|
DontEnum.into(),
|
|
|
|
);
|
|
|
|
object.as_script_object_mut().unwrap().add_property(
|
|
|
|
context.gc_context,
|
|
|
|
"virtual",
|
|
|
|
getter.clone(),
|
|
|
|
None,
|
|
|
|
EnumSet::empty(),
|
|
|
|
);
|
|
|
|
object.as_script_object_mut().unwrap().add_property(
|
|
|
|
context.gc_context,
|
|
|
|
"virtual_hidden",
|
|
|
|
getter,
|
|
|
|
None,
|
|
|
|
DontEnum.into(),
|
|
|
|
);
|
2019-10-25 03:21:35 +00:00
|
|
|
|
2019-12-06 18:24:36 +00:00
|
|
|
let keys = object.get_keys();
|
2019-10-25 03:21:35 +00:00
|
|
|
assert_eq!(keys.len(), 2);
|
|
|
|
assert_eq!(keys.contains(&"stored".to_string()), true);
|
|
|
|
assert_eq!(keys.contains(&"stored_hidden".to_string()), false);
|
|
|
|
assert_eq!(keys.contains(&"virtual".to_string()), true);
|
|
|
|
assert_eq!(keys.contains(&"virtual_hidden".to_string()), false);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|