core: Reimplement `Object.registerClass`.

Maintain an external mapping from symbol names to registered
constructors to properly handle `registerClass` being called on
not-yet-available symbols.

SWFs v6 and v7+ each have a separate global mapping, with
different case sensitivities.

Also returns the correct boolean value to the AVM.

Fixes #2343 and #1864.
This commit is contained in:
Moulins 2021-01-10 16:45:54 +01:00 committed by Mike Welsh
parent 9bb36885bb
commit f394953331
6 changed files with 194 additions and 144 deletions

View File

@ -5,7 +5,6 @@ use crate::avm1::function::{Executable, FunctionObject};
use crate::avm1::property::Attribute::{self, *};
use crate::avm1::{Object, ScriptObject, TObject, Value};
use crate::avm_warn;
use crate::character::Character;
use crate::display_object::TDisplayObject;
use enumset::EnumSet;
use gc_arena::MutationContext;
@ -147,36 +146,35 @@ pub fn register_class<'gc>(
_this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
if let Some(class_name) = args.get(0).cloned() {
let class_name = class_name.coerce_to_string(activation)?;
if let Some(movie) = activation.base_clip().movie() {
if let Some(Character::MovieClip(movie_clip)) = activation
.context
.library
.library_for_movie_mut(movie)
.get_character_by_export_name(&class_name)
{
if let Some(constructor) = args.get(1) {
movie_clip.set_avm1_constructor(
activation.context.gc_context,
Some(constructor.coerce_to_object(activation)),
);
} else {
movie_clip.set_avm1_constructor(activation.context.gc_context, None);
}
} else {
log::warn!(
"Tried to register_class on an unknown export {}",
class_name
);
}
} else {
log::warn!("Tried to register_class on an unknown movie");
let (class_name, constructor) = match args {
[class_name, constructor, ..] => (class_name, constructor),
_ => return Ok(Value::Bool(false)),
};
let constructor = match constructor {
Value::Null | Value::Undefined => None,
Value::Object(Object::FunctionObject(func)) => Some(*func),
_ => return Ok(Value::Bool(false)),
};
let class_name = class_name.coerce_to_string(activation)?;
let registry = activation
.base_clip()
.movie()
.map(|movie| activation.context.library.library_for_movie_mut(movie))
.and_then(|library| library.get_avm1_constructor_registry());
match registry {
Some(registry) => {
registry.set(&class_name, constructor, activation.context.gc_context);
Ok(Value::Bool(true))
}
None => {
log::warn!("Can't register_class without a constructor registry");
Ok(Value::Bool(false))
}
} else {
log::warn!("Tried to register_class with an unknown class");
}
Ok(Value::Undefined)
}
/// Implements `Object.prototype.watch`

View File

@ -893,7 +893,7 @@ mod tests {
audio: &mut NullAudioBackend::new(),
input: &mut NullInputBackend::new(),
background_color: &mut None,
library: &mut Library::default(),
library: &mut Library::empty(gc_context),
navigator: &mut NullNavigatorBackend::new(),
renderer: &mut NullRenderer::new(),
locale: &mut NullLocaleBackend::new(),

View File

@ -54,7 +54,7 @@ where
input: &mut NullInputBackend::new(),
action_queue: &mut ActionQueue::new(),
background_color: &mut None,
library: &mut Library::default(),
library: &mut Library::empty(gc_context),
navigator: &mut NullNavigatorBackend::new(),
renderer: &mut NullRenderer::new(),
locale: &mut NullLocaleBackend::new(),

View File

@ -27,7 +27,7 @@ use crate::vminterface::{AvmObject, AvmType, Instantiator};
use enumset::{EnumSet, EnumSetType};
use gc_arena::{Collect, Gc, GcCell, MutationContext};
use smallvec::SmallVec;
use std::cell::Ref;
use std::cell::{Ref, RefCell};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::sync::Arc;
@ -58,7 +58,7 @@ pub struct MovieClipData<'gc> {
frame_scripts: Vec<Avm2FrameScript<'gc>>,
has_button_clip_event: bool,
flags: EnumSet<MovieClipFlags>,
avm_constructor: Option<AvmObject<'gc>>,
avm2_constructor: Option<Avm2Object<'gc>>,
drawing: Drawing,
is_focusable: bool,
has_focus: bool,
@ -72,7 +72,7 @@ unsafe impl<'gc> Collect for MovieClipData<'gc> {
self.base.trace(cc);
self.static_data.trace(cc);
self.object.trace(cc);
self.avm_constructor.trace(cc);
self.avm2_constructor.trace(cc);
self.frame_scripts.trace(cc);
}
}
@ -94,7 +94,7 @@ impl<'gc> MovieClip<'gc> {
frame_scripts: Vec::new(),
has_button_clip_event: false,
flags: EnumSet::empty(),
avm_constructor: None,
avm2_constructor: None,
drawing: Drawing::new(),
is_focusable: false,
has_focus: false,
@ -115,14 +115,7 @@ impl<'gc> MovieClip<'gc> {
base: Default::default(),
static_data: Gc::allocate(
gc_context,
MovieClipStatic {
id,
swf,
total_frames: num_frames,
audio_stream_info: None,
frame_labels: HashMap::new(),
scene_labels: HashMap::new(),
},
MovieClipStatic::with_data(id, swf, num_frames),
),
tag_stream_pos: 0,
current_frame: 0,
@ -133,7 +126,7 @@ impl<'gc> MovieClip<'gc> {
frame_scripts: Vec::new(),
has_button_clip_event: false,
flags: MovieClipFlags::Playing.into(),
avm_constructor: None,
avm2_constructor: None,
drawing: Drawing::new(),
is_focusable: false,
has_focus: false,
@ -840,42 +833,13 @@ impl<'gc> MovieClip<'gc> {
self.0.read().static_data.total_frames
}
pub fn set_avm1_constructor(
self,
gc_context: MutationContext<'gc, '_>,
prototype: Option<Avm1Object<'gc>>,
) {
let mut write = self.0.write(gc_context);
if write
.avm_constructor
.map(|c| c.is_avm2_object())
.unwrap_or(false)
{
log::error!("Blocked attempt to set AVM1 constructor on AVM2 object");
return;
}
write.avm_constructor = prototype.map(|o| o.into());
}
pub fn set_avm2_constructor(
self,
gc_context: MutationContext<'gc, '_>,
prototype: Option<Avm2Object<'gc>>,
) {
let mut write = self.0.write(gc_context);
if write
.avm_constructor
.map(|c| c.is_avm1_object())
.unwrap_or(false)
{
log::error!("Blocked attempt to set AVM2 constructor on AVM1 object");
return;
}
write.avm_constructor = prototype.map(|o| o.into());
write.avm2_constructor = prototype;
}
pub fn frame_label_to_number(self, frame_label: &str) -> Option<FrameNumber> {
@ -1399,10 +1363,11 @@ impl<'gc> MovieClip<'gc> {
if self.0.read().object.is_none() {
let version = context.swf.version();
let globals = context.avm1.global_object_cell();
let avm1_constructor = self.0.read().get_registered_avm1_constructor(context);
// If we are running within the AVM, this must be an immediate action.
// If we are not, then this must be queued to be ran first-thing
if instantiated_by.is_avm() && self.0.read().avm_constructor.is_some() {
if let Some(constructor) = avm1_constructor.filter(|_| instantiated_by.is_avm()) {
let mut activation = Avm1Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root("[Construct]"),
@ -1411,13 +1376,6 @@ impl<'gc> MovieClip<'gc> {
self.into(),
);
let constructor = self
.0
.read()
.avm_constructor
.unwrap()
.as_avm1_object()
.unwrap();
if let Ok(prototype) = constructor
.get("prototype", &mut activation)
.map(|v| v.coerce_to_object(&mut activation))
@ -1488,7 +1446,7 @@ impl<'gc> MovieClip<'gc> {
context.action_queue.queue_actions(
display_object,
ActionType::Construct {
constructor: mc.avm_constructor.map(|a| a.as_avm1_object().unwrap()),
constructor: avm1_constructor,
events,
},
false,
@ -1515,7 +1473,7 @@ impl<'gc> MovieClip<'gc> {
context: &mut UpdateContext<'_, 'gc, '_>,
display_object: DisplayObject<'gc>,
) {
let constructor = self.0.read().avm_constructor.unwrap_or_else(|| {
let mut constructor = self.0.read().avm2_constructor.unwrap_or_else(|| {
let mut activation = Avm2Activation::from_nothing(context.reborrow());
let mut mc_proto = activation.context.avm2.prototypes().movieclip;
mc_proto
@ -1527,39 +1485,34 @@ impl<'gc> MovieClip<'gc> {
.unwrap()
.coerce_to_object(&mut activation)
.unwrap()
.into()
});
if let AvmObject::Avm2(mut constr) = constructor {
let mut constr_thing = || {
let mut activation = Avm2Activation::from_nothing(context.reborrow());
let proto = constr
.get_property(
constr,
&Avm2QName::new(Avm2Namespace::public_namespace(), "prototype"),
&mut activation,
)?
.coerce_to_object(&mut activation)?;
let object = Avm2StageObject::for_display_object(
activation.context.gc_context,
display_object,
proto,
)
.into();
let mut constr_thing = || {
let mut activation = Avm2Activation::from_nothing(context.reborrow());
let proto = constructor
.get_property(
constructor,
&Avm2QName::new(Avm2Namespace::public_namespace(), "prototype"),
&mut activation,
)?
.coerce_to_object(&mut activation)?;
let object = Avm2StageObject::for_display_object(
activation.context.gc_context,
display_object,
proto,
)
.into();
constr.call(Some(object), &[], &mut activation, Some(proto))?;
constructor.call(Some(object), &[], &mut activation, Some(proto))?;
Ok(object)
};
let result: Result<Avm2Object<'gc>, Avm2Error> = constr_thing();
Ok(object)
};
let result: Result<Avm2Object<'gc>, Avm2Error> = constr_thing();
if let Ok(object) = result {
self.0.write(context.gc_context).object = Some(object.into());
} else if let Err(e) = result {
log::error!("Got {} when constructing AVM2 side of display object", e);
}
} else {
log::error!("Attempted to construct AVM2 movieclip with AVM1 constructor!");
if let Ok(object) = result {
self.0.write(context.gc_context).object = Some(object.into());
} else if let Err(e) = result {
log::error!("Got {} when constructing AVM2 side of display object", e);
}
}
@ -1928,14 +1881,7 @@ impl<'gc> MovieClipData<'gc> {
self.base.reset_for_movie_load();
self.static_data = Gc::allocate(
gc_context,
MovieClipStatic {
id: 0,
swf: movie.into(),
total_frames,
audio_stream_info: None,
frame_labels: HashMap::new(),
scene_labels: HashMap::new(),
},
MovieClipStatic::with_data(0, movie.into(), total_frames),
);
self.tag_stream_pos = 0;
self.flags = MovieClipFlags::Playing.into();
@ -2130,6 +2076,21 @@ impl<'gc> MovieClipData<'gc> {
}
}
/// Fetch the avm1 constructor associated with this MovieClip by `Object.registerClass`.
/// Return `None` if this MovieClip isn't exported, or if no constructor is associated
/// to its symbol name.
fn get_registered_avm1_constructor(
&self,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Option<Avm1Object<'gc>> {
let symbol_name = self.static_data.exported_name.borrow();
let symbol_name = symbol_name.as_ref()?;
let library = context.library.library_for_movie_mut(self.movie());
let registry = library.get_avm1_constructor_registry()?;
let ctor = registry.get(symbol_name)?;
Some(Avm1Object::FunctionObject(ctor))
}
pub fn movie(&self) -> Arc<SwfMovie> {
self.static_data.swf.movie.clone()
}
@ -2711,10 +2672,15 @@ impl<'gc, 'a> MovieClipData<'gc> {
) -> DecodeResult {
let exports = reader.read_export_assets()?;
for export in exports {
context
let character = context
.library
.library_for_movie_mut(self.movie())
.register_export(export.id, &export.name);
// TODO: do other types of Character need to know their exported name?
if let Some(Character::MovieClip(movie_clip)) = character {
*movie_clip.0.read().static_data.exported_name.borrow_mut() = Some(export.name);
}
}
Ok(())
}
@ -2997,7 +2963,8 @@ impl Default for Scene {
/// Static data shared between all instances of a movie clip.
#[allow(dead_code)]
#[derive(Clone)]
#[derive(Clone, Collect)]
#[collect(require_static)]
struct MovieClipStatic {
id: CharacterId,
swf: SwfSlice,
@ -3005,28 +2972,29 @@ struct MovieClipStatic {
scene_labels: HashMap<String, Scene>,
audio_stream_info: Option<swf::SoundStreamHead>,
total_frames: FrameNumber,
/// The last known symbol name under which this movie clip was exported.
/// Used for looking up constructors registered with `Object.registerClass`.
exported_name: RefCell<Option<String>>,
}
impl MovieClipStatic {
fn empty(swf: SwfSlice) -> Self {
Self::with_data(0, swf, 1)
}
fn with_data(id: CharacterId, swf: SwfSlice, total_frames: FrameNumber) -> Self {
Self {
id: 0,
id,
swf,
total_frames: 1,
total_frames,
frame_labels: HashMap::new(),
scene_labels: HashMap::new(),
audio_stream_info: None,
exported_name: RefCell::new(None),
}
}
}
unsafe impl<'gc> Collect for MovieClipStatic {
#[inline]
fn needs_trace() -> bool {
false
}
}
/// Stores the placement settings for display objects during a
/// goto command.
#[derive(Debug)]

View File

@ -1,4 +1,3 @@
use crate::avm2::Domain as Avm2Domain;
use crate::backend::audio::SoundHandle;
use crate::character::Character;
use crate::display_object::{Bitmap, TDisplayObject};
@ -7,7 +6,8 @@ use crate::prelude::*;
use crate::property_map::{Entry, PropertyMap};
use crate::tag_utils::{SwfMovie, SwfSlice};
use crate::vminterface::AvmType;
use gc_arena::{Collect, MutationContext};
use crate::{avm1::function::FunctionObject, avm2::Domain as Avm2Domain};
use gc_arena::{Collect, Gc, GcCell, MutationContext};
use std::collections::HashMap;
use std::sync::{Arc, Weak};
use swf::{CharacterId, TagCode};
@ -16,6 +16,45 @@ use weak_table::PtrWeakKeyHashMap;
/// Boxed error alias.
type Error = Box<dyn std::error::Error>;
/// The mappings between symbol names and constructors registered
/// with `Object.registerClass`.
#[derive(Collect)]
#[collect(no_drop)]
pub struct Avm1ConstructorRegistry<'gc> {
symbol_map: GcCell<'gc, PropertyMap<FunctionObject<'gc>>>,
is_case_sensitive: bool,
}
impl<'gc> Avm1ConstructorRegistry<'gc> {
pub fn new(is_case_sensitive: bool, gc_context: MutationContext<'gc, '_>) -> Self {
Self {
symbol_map: GcCell::allocate(gc_context, PropertyMap::new()),
is_case_sensitive,
}
}
pub fn get(&self, symbol: &str) -> Option<FunctionObject<'gc>> {
self.symbol_map
.read()
.get(symbol, self.is_case_sensitive)
.copied()
}
pub fn set(
&self,
symbol: &str,
constructor: Option<FunctionObject<'gc>>,
gc_context: MutationContext<'gc, '_>,
) {
let mut map = self.symbol_map.write(gc_context);
if let Some(ctor) = constructor {
map.insert(symbol, ctor, self.is_case_sensitive);
} else {
map.remove(symbol, self.is_case_sensitive);
};
}
}
/// Symbol library for a single given SWF.
#[derive(Collect)]
#[collect(no_drop)]
@ -26,6 +65,9 @@ pub struct MovieLibrary<'gc> {
fonts: HashMap<FontDescriptor, Font<'gc>>,
avm_type: AvmType,
avm2_domain: Option<Avm2Domain<'gc>>,
/// Shared reference to the constructor registry used for this movie.
/// Should be `None` if this is an AVM2 movie.
avm1_constructor_registry: Option<Gc<'gc, Avm1ConstructorRegistry<'gc>>>,
}
impl<'gc> MovieLibrary<'gc> {
@ -37,6 +79,7 @@ impl<'gc> MovieLibrary<'gc> {
fonts: HashMap::new(),
avm_type,
avm2_domain: None,
avm1_constructor_registry: None,
}
}
@ -55,11 +98,16 @@ impl<'gc> MovieLibrary<'gc> {
/// Registers an export name for a given character ID.
/// This character will then be instantiable from AVM1.
pub fn register_export(&mut self, id: CharacterId, export_name: &str) {
pub fn register_export(
&mut self,
id: CharacterId,
export_name: &str,
) -> Option<&Character<'gc>> {
if let Some(character) = self.characters.get(&id) {
match self.export_characters.entry(export_name, false) {
Entry::Vacant(e) => {
e.insert(character.clone());
return Some(character);
}
Entry::Occupied(_) => {
log::warn!(
@ -75,6 +123,7 @@ impl<'gc> MovieLibrary<'gc> {
id
)
}
None
}
pub fn contains_character(&self, id: CharacterId) -> bool {
@ -91,6 +140,10 @@ impl<'gc> MovieLibrary<'gc> {
self.export_characters.get(name, false)
}
pub fn get_avm1_constructor_registry(&self) -> Option<Gc<'gc, Avm1ConstructorRegistry<'gc>>> {
self.avm1_constructor_registry
}
/// Instantiates the library item with the given character ID into a display object.
/// The object must then be post-instantiated before being used.
pub fn instantiate_by_id(
@ -242,6 +295,9 @@ pub struct Library<'gc> {
/// The embedded device font.
device_font: Option<Font<'gc>>,
constructor_registry_case_insensitive: Gc<'gc, Avm1ConstructorRegistry<'gc>>,
constructor_registry_case_sensitive: Gc<'gc, Avm1ConstructorRegistry<'gc>>,
}
unsafe impl<'gc> gc_arena::Collect for Library<'gc> {
@ -251,10 +307,27 @@ unsafe impl<'gc> gc_arena::Collect for Library<'gc> {
val.trace(cc);
}
self.device_font.trace(cc);
self.constructor_registry_case_insensitive.trace(cc);
self.constructor_registry_case_sensitive.trace(cc);
}
}
impl<'gc> Library<'gc> {
pub fn empty(gc_context: MutationContext<'gc, '_>) -> Self {
Self {
movie_libraries: PtrWeakKeyHashMap::new(),
device_font: None,
constructor_registry_case_insensitive: Gc::allocate(
gc_context,
Avm1ConstructorRegistry::new(false, gc_context),
),
constructor_registry_case_sensitive: Gc::allocate(
gc_context,
Avm1ConstructorRegistry::new(true, gc_context),
),
}
}
pub fn library_for_movie(&self, movie: Arc<SwfMovie>) -> Option<&MovieLibrary<'gc>> {
self.movie_libraries.get(&movie)
}
@ -263,7 +336,8 @@ impl<'gc> Library<'gc> {
if !self.movie_libraries.contains_key(&movie) {
let slice = SwfSlice::from(movie.clone());
let mut reader = slice.read_from(0);
let vm_type = if movie.header().version > 8 {
let movie_version = movie.header().version;
let vm_type = if movie_version > 8 {
match reader.read_tag_code_and_length() {
Ok((tag_code, _tag_len))
if TagCode::from_u16(tag_code) == Some(TagCode::FileAttributes) =>
@ -284,8 +358,13 @@ impl<'gc> Library<'gc> {
AvmType::Avm1
};
self.movie_libraries
.insert(movie.clone(), MovieLibrary::new(vm_type));
let mut movie_library = MovieLibrary::new(vm_type);
if vm_type == AvmType::Avm1 {
movie_library.avm1_constructor_registry =
Some(self.get_avm1_constructor_registry(movie_version));
}
self.movie_libraries.insert(movie.clone(), movie_library);
};
self.movie_libraries.get_mut(&movie).unwrap()
@ -300,13 +379,18 @@ impl<'gc> Library<'gc> {
pub fn set_device_font(&mut self, font: Option<Font<'gc>>) {
self.device_font = font;
}
}
impl<'gc> Default for Library<'gc> {
fn default() -> Self {
Self {
movie_libraries: PtrWeakKeyHashMap::new(),
device_font: None,
/// Gets the constructor registry to use for the given SWF version.
/// Because SWFs v6 and v7+ use different case-sensitivity rules, Flash
/// keeps two separate registries, one case-sensitive, the other not.
fn get_avm1_constructor_registry(
&mut self,
swf_version: u8,
) -> Gc<'gc, Avm1ConstructorRegistry<'gc>> {
if swf_version < 7 {
self.constructor_registry_case_insensitive
} else {
self.constructor_registry_case_sensitive
}
}
}

View File

@ -260,7 +260,7 @@ impl Player {
GcRoot(GcCell::allocate(
gc_context,
GcRootData {
library: Library::default(),
library: Library::empty(gc_context),
levels: BTreeMap::new(),
mouse_hovered_object: None,
drag_object: None,