2020-02-15 01:30:19 +00:00
|
|
|
//! Property data structures
|
|
|
|
|
|
|
|
use self::Attribute::*;
|
|
|
|
use crate::avm2::function::Executable;
|
2020-02-19 19:17:33 +00:00
|
|
|
use crate::avm2::object::{Object, TObject};
|
2020-02-15 01:30:19 +00:00
|
|
|
use crate::avm2::return_value::ReturnValue;
|
|
|
|
use crate::avm2::value::Value;
|
|
|
|
use crate::avm2::{Avm2, Error};
|
|
|
|
use crate::context::UpdateContext;
|
|
|
|
use enumset::{EnumSet, EnumSetType};
|
|
|
|
use gc_arena::{Collect, CollectionContext};
|
|
|
|
|
|
|
|
/// Attributes of properties in the AVM runtime.
|
|
|
|
///
|
|
|
|
/// TODO: Replace with AVM2 properties for traits
|
|
|
|
#[derive(EnumSetType, Debug)]
|
|
|
|
pub enum Attribute {
|
|
|
|
DontEnum,
|
|
|
|
DontDelete,
|
|
|
|
ReadOnly,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub enum Property<'gc> {
|
|
|
|
Virtual {
|
2020-02-18 00:43:23 +00:00
|
|
|
get: Option<Executable<'gc>>,
|
2020-02-15 01:30:19 +00:00
|
|
|
set: Option<Executable<'gc>>,
|
|
|
|
attributes: EnumSet<Attribute>,
|
|
|
|
},
|
|
|
|
Stored {
|
|
|
|
value: Value<'gc>,
|
|
|
|
attributes: EnumSet<Attribute>,
|
|
|
|
},
|
2020-02-19 19:17:33 +00:00
|
|
|
Slot {
|
|
|
|
slot_id: u32,
|
|
|
|
attributes: EnumSet<Attribute>,
|
|
|
|
},
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
unsafe impl<'gc> Collect for Property<'gc> {
|
|
|
|
fn trace(&self, cc: CollectionContext) {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { get, set, .. } => {
|
|
|
|
get.trace(cc);
|
|
|
|
set.trace(cc);
|
|
|
|
}
|
|
|
|
Property::Stored { value, .. } => value.trace(cc),
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { .. } => {}
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'gc> Property<'gc> {
|
2020-02-21 02:02:49 +00:00
|
|
|
/// Create a new stored property.
|
|
|
|
pub fn new_stored(value: impl Into<Value<'gc>>) -> Self {
|
|
|
|
Property::Stored {
|
|
|
|
value: value.into(),
|
|
|
|
attributes: EnumSet::from(Attribute::DontDelete),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-21 04:20:46 +00:00
|
|
|
/// Create a new stored property.
|
|
|
|
pub fn new_const(value: impl Into<Value<'gc>>) -> Self {
|
|
|
|
Property::Stored {
|
|
|
|
value: value.into(),
|
|
|
|
attributes: Attribute::ReadOnly | Attribute::DontDelete,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-15 01:30:19 +00:00
|
|
|
/// Convert a value into a dynamic property.
|
|
|
|
pub fn new_dynamic_property(value: impl Into<Value<'gc>>) -> Self {
|
|
|
|
Property::Stored {
|
|
|
|
value: value.into(),
|
|
|
|
attributes: EnumSet::empty(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-18 00:43:23 +00:00
|
|
|
/// Convert a function into a method.
|
|
|
|
///
|
|
|
|
/// This applies ReadOnly/DontDelete to the property.
|
|
|
|
pub fn new_method(fn_obj: Object<'gc>) -> Self {
|
|
|
|
Property::Stored {
|
|
|
|
value: fn_obj.into(),
|
|
|
|
attributes: Attribute::ReadOnly | Attribute::DontDelete,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a new, unconfigured virtual property item.
|
|
|
|
pub fn new_virtual() -> Self {
|
|
|
|
Property::Virtual {
|
|
|
|
get: None,
|
|
|
|
set: None,
|
|
|
|
attributes: Attribute::ReadOnly | Attribute::DontDelete,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-21 02:02:49 +00:00
|
|
|
/// Create a new slot property.
|
|
|
|
pub fn new_slot(slot_id: u32) -> Self {
|
|
|
|
Property::Slot {
|
|
|
|
slot_id,
|
|
|
|
attributes: EnumSet::from(Attribute::DontDelete),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-18 00:43:23 +00:00
|
|
|
/// Install a getter into this property.
|
|
|
|
///
|
|
|
|
/// This function errors if attempting to install executables into a
|
|
|
|
/// non-virtual property.
|
|
|
|
pub fn install_virtual_getter(&mut self, getter_impl: Executable<'gc>) -> Result<(), Error> {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { get, .. } => *get = Some(getter_impl),
|
|
|
|
Property::Stored { .. } => return Err("Not a virtual property".into()),
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { .. } => return Err("Not a virtual property".into()),
|
2020-02-18 00:43:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Install a setter into this property.
|
|
|
|
///
|
|
|
|
/// This function errors if attempting to install executables into a
|
|
|
|
/// non-virtual property.
|
|
|
|
pub fn install_virtual_setter(&mut self, setter_impl: Executable<'gc>) -> Result<(), Error> {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { set, .. } => *set = Some(setter_impl),
|
|
|
|
Property::Stored { .. } => return Err("Not a virtual property".into()),
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { .. } => return Err("Not a virtual property".into()),
|
2020-02-18 00:43:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-02-15 01:30:19 +00:00
|
|
|
/// Get the value of a property slot.
|
|
|
|
///
|
|
|
|
/// This function yields `ReturnValue` because some properties may be
|
|
|
|
/// user-defined.
|
|
|
|
pub fn get(
|
|
|
|
&self,
|
|
|
|
avm: &mut Avm2<'gc>,
|
|
|
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
|
|
|
this: Object<'gc>,
|
|
|
|
) -> Result<ReturnValue<'gc>, Error> {
|
|
|
|
match self {
|
Refactor the base_proto system to more accurately record what prototype methods come from.
The previous system primarily relied on `Executable` to automatically start and continue a super chain. This works, but only for class hierarchies without *override gaps* - methods that override another method not defined by the direct superclass of the method. In that case, the override method would be called twice as the `base_class` was moved up one prototype at a time, which is wrong.
The new system relies on the call site to accurately report the prototype from which the current method was retrieved from. Super calls then start the resolution process *from the superclass of this prototype*, to ensure that the already-called method is skipped.
It should be noted that the proper `base_class` for things like `callmethod`, `callstatic`, `call`, `get`/`set` methods, and other call opcodes that don't use property look-up are best-effort guesses that may need to be amended later with better tests.
To facilitate `base_proto` resolution, a new `Object` method has been added. It's similar to `get_property`, but instead returns the closest prototype that can resolve the given `QName`, rather than the actual property's `ReturnValue`. Call operations use this to resolve the `base_proto`, and then resolve the method being called in `base_proto`. The existing `exec_super` method was removed and a `base_proto` method added to `exec` and `call`.
2020-02-28 02:58:59 +00:00
|
|
|
Property::Virtual { get: Some(get), .. } => {
|
|
|
|
get.exec(Some(this), &[], avm, context, this.proto())
|
|
|
|
}
|
2020-02-18 00:43:23 +00:00
|
|
|
Property::Virtual { get: None, .. } => Ok(Value::Undefined.into()),
|
2020-02-15 01:30:19 +00:00
|
|
|
Property::Stored { value, .. } => Ok(value.to_owned().into()),
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { slot_id, .. } => this.get_slot(*slot_id).map(|v| v.into()),
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set a property slot.
|
|
|
|
///
|
|
|
|
/// This function returns `true` if the set has completed, or `false` if
|
|
|
|
/// it has not yet occured. If `false`, and you need to run code after the
|
|
|
|
/// set has occured, you must recursively execute the top-most frame via
|
|
|
|
/// `run_current_frame`.
|
2020-02-23 02:28:02 +00:00
|
|
|
///
|
|
|
|
/// This function cannot set slot properties and will panic if one
|
|
|
|
/// is encountered.
|
2020-02-15 01:30:19 +00:00
|
|
|
pub fn set(
|
|
|
|
&mut self,
|
|
|
|
avm: &mut Avm2<'gc>,
|
|
|
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
|
|
|
this: Object<'gc>,
|
|
|
|
new_value: impl Into<Value<'gc>>,
|
|
|
|
) -> Result<bool, Error> {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { set, .. } => {
|
|
|
|
if let Some(function) = set {
|
Refactor the base_proto system to more accurately record what prototype methods come from.
The previous system primarily relied on `Executable` to automatically start and continue a super chain. This works, but only for class hierarchies without *override gaps* - methods that override another method not defined by the direct superclass of the method. In that case, the override method would be called twice as the `base_class` was moved up one prototype at a time, which is wrong.
The new system relies on the call site to accurately report the prototype from which the current method was retrieved from. Super calls then start the resolution process *from the superclass of this prototype*, to ensure that the already-called method is skipped.
It should be noted that the proper `base_class` for things like `callmethod`, `callstatic`, `call`, `get`/`set` methods, and other call opcodes that don't use property look-up are best-effort guesses that may need to be amended later with better tests.
To facilitate `base_proto` resolution, a new `Object` method has been added. It's similar to `get_property`, but instead returns the closest prototype that can resolve the given `QName`, rather than the actual property's `ReturnValue`. Call operations use this to resolve the `base_proto`, and then resolve the method being called in `base_proto`. The existing `exec_super` method was removed and a `base_proto` method added to `exec` and `call`.
2020-02-28 02:58:59 +00:00
|
|
|
let return_value = function.exec(
|
|
|
|
Some(this),
|
|
|
|
&[new_value.into()],
|
|
|
|
avm,
|
|
|
|
context,
|
|
|
|
this.proto(),
|
|
|
|
)?;
|
2020-02-15 01:30:19 +00:00
|
|
|
Ok(return_value.is_immediate())
|
|
|
|
} else {
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Property::Stored {
|
|
|
|
value, attributes, ..
|
|
|
|
} => {
|
|
|
|
if !attributes.contains(ReadOnly) {
|
2020-02-22 21:21:28 +00:00
|
|
|
*value = new_value.into();
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(true)
|
|
|
|
}
|
2020-02-23 03:13:15 +00:00
|
|
|
Property::Slot { .. } => panic!("Cannot recursively set slots"),
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-22 21:21:28 +00:00
|
|
|
/// Init a property slot.
|
|
|
|
///
|
|
|
|
/// The difference between `set` and `init` is that this function does not
|
|
|
|
/// respect `ReadOnly` and will allow initializing nominally `const`
|
|
|
|
/// properties, at least once. Virtual properties with no setter cannot be
|
|
|
|
/// initialized.
|
|
|
|
///
|
|
|
|
/// This function returns `true` if the set has completed, or `false` if
|
|
|
|
/// it has not yet occured. If `false`, and you need to run code after the
|
|
|
|
/// set has occured, you must recursively execute the top-most frame via
|
|
|
|
/// `run_current_frame`.
|
2020-02-23 02:28:02 +00:00
|
|
|
///
|
|
|
|
/// This function cannot initialize slot properties and will panic if one
|
|
|
|
/// is encountered.
|
2020-02-22 21:21:28 +00:00
|
|
|
pub fn init(
|
|
|
|
&mut self,
|
|
|
|
avm: &mut Avm2<'gc>,
|
|
|
|
context: &mut UpdateContext<'_, 'gc, '_>,
|
|
|
|
this: Object<'gc>,
|
|
|
|
new_value: impl Into<Value<'gc>>,
|
|
|
|
) -> Result<bool, Error> {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { set, .. } => {
|
|
|
|
if let Some(function) = set {
|
Refactor the base_proto system to more accurately record what prototype methods come from.
The previous system primarily relied on `Executable` to automatically start and continue a super chain. This works, but only for class hierarchies without *override gaps* - methods that override another method not defined by the direct superclass of the method. In that case, the override method would be called twice as the `base_class` was moved up one prototype at a time, which is wrong.
The new system relies on the call site to accurately report the prototype from which the current method was retrieved from. Super calls then start the resolution process *from the superclass of this prototype*, to ensure that the already-called method is skipped.
It should be noted that the proper `base_class` for things like `callmethod`, `callstatic`, `call`, `get`/`set` methods, and other call opcodes that don't use property look-up are best-effort guesses that may need to be amended later with better tests.
To facilitate `base_proto` resolution, a new `Object` method has been added. It's similar to `get_property`, but instead returns the closest prototype that can resolve the given `QName`, rather than the actual property's `ReturnValue`. Call operations use this to resolve the `base_proto`, and then resolve the method being called in `base_proto`. The existing `exec_super` method was removed and a `base_proto` method added to `exec` and `call`.
2020-02-28 02:58:59 +00:00
|
|
|
let return_value = function.exec(
|
|
|
|
Some(this),
|
|
|
|
&[new_value.into()],
|
|
|
|
avm,
|
|
|
|
context,
|
|
|
|
this.proto(),
|
|
|
|
)?;
|
2020-02-22 21:21:28 +00:00
|
|
|
Ok(return_value.is_immediate())
|
|
|
|
} else {
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Property::Stored { value, .. } => {
|
|
|
|
*value = new_value.into();
|
|
|
|
|
|
|
|
Ok(true)
|
|
|
|
}
|
2020-02-23 03:13:15 +00:00
|
|
|
Property::Slot { .. } => panic!("Cannot recursively init slots"),
|
2020-02-23 02:28:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Retrieve the slot ID of a property.
|
|
|
|
///
|
|
|
|
/// This function yields `None` if this property is not a slot.
|
|
|
|
pub fn slot_id(&self) -> Option<u32> {
|
|
|
|
match self {
|
|
|
|
Property::Slot { slot_id, .. } => Some(*slot_id),
|
|
|
|
_ => None,
|
2020-02-22 21:21:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-15 01:30:19 +00:00
|
|
|
/// List this property's attributes.
|
|
|
|
pub fn attributes(&self) -> EnumSet<Attribute> {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { attributes, .. } => *attributes,
|
|
|
|
Property::Stored { attributes, .. } => *attributes,
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { attributes, .. } => *attributes,
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Re-define this property's attributes.
|
|
|
|
pub fn set_attributes(&mut self, new_attributes: EnumSet<Attribute>) {
|
|
|
|
match self {
|
|
|
|
Property::Virtual {
|
|
|
|
ref mut attributes, ..
|
|
|
|
} => *attributes = new_attributes,
|
|
|
|
Property::Stored {
|
|
|
|
ref mut attributes, ..
|
|
|
|
} => *attributes = new_attributes,
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot {
|
|
|
|
ref mut attributes, ..
|
|
|
|
} => *attributes = new_attributes,
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn can_delete(&self) -> bool {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { attributes, .. } => !attributes.contains(DontDelete),
|
|
|
|
Property::Stored { attributes, .. } => !attributes.contains(DontDelete),
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { attributes, .. } => !attributes.contains(DontDelete),
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_enumerable(&self) -> bool {
|
|
|
|
match self {
|
|
|
|
Property::Virtual { attributes, .. } => !attributes.contains(DontEnum),
|
|
|
|
Property::Stored { attributes, .. } => !attributes.contains(DontEnum),
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { attributes, .. } => !attributes.contains(DontEnum),
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_overwritable(&self) -> bool {
|
|
|
|
match self {
|
|
|
|
Property::Virtual {
|
|
|
|
attributes, set, ..
|
|
|
|
} => !attributes.contains(ReadOnly) && !set.is_none(),
|
|
|
|
Property::Stored { attributes, .. } => !attributes.contains(ReadOnly),
|
2020-02-19 19:17:33 +00:00
|
|
|
Property::Slot { attributes, .. } => !attributes.contains(ReadOnly),
|
2020-02-15 01:30:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|