avm2: Implement namespace versioning

This allows us to hide things like `MovieClip.isPlaying` from older
SWFs, which may define an `isPlaying` method without marking it
as an override.

This has the largest impact on public namespaces. There is no longer
just one public namespace - we now have a public namespace per API
version. Most callsites should use `Avm2.find_public_namespace()`,
which determines the public namespace based on the root SWF API version.
This ensures that explicit property access in Ruffle (e.g. calling
"toString") are able to see things defined in the user SWF.

This requires several changes other:

* A new `ApiVersion` enum is introduced, storing all of the api versions
  defined in avmplus
* NamespaceData::Namespace now holds an `ApiVersion`. When we read a
  namespace from a user-defined ABC file, we use the `ApiVersion` from
  the root movie clip. This matches Flash Player's behavior - when a
  child SWF is loaded through `Loader`, its API version gets overwritten
  with the parent's API version, which affects definition visibility and
  'override' requirements in the child SWF. Note that 'behavior changes'
  in methods like `gotoAndPlay` are unaffected.
* The `PartialEq` impl for `Namespace` has been removed - each call site must now choose
  to compare namespaces either by an exact version match, or by
  'compatible' versions (a `<=` check) with `Namespace::matches_ns`
* `PropertyMap` now uses `Namespace::matches_ns` to check for a
  compatible version when looking up a definition.
* When compiling our playerglobal, we pass in the `-apiversioning` flag,
  which causes asc.jar to convert `[API]` metadata into a special
  Unicode 'version mark' suffix in namespace URLs. We parse this
  when loading namespaces to determine the API version to use for
  playerglobal definitions (unmarked definitions use the lowest possible
  version).

Unfortunately, this makes ffdec unable to decompile our
playerglobal.swc
I've going to file an upstream issue
This commit is contained in:
Aaron Hill 2023-07-31 17:39:42 -04:00 committed by Nathan Adams
parent 4d99459849
commit fa05c66e2a
74 changed files with 880 additions and 273 deletions

View File

@ -52,6 +52,10 @@ pub fn build_playerglobal(
&asc_path.to_string_lossy(),
"macromedia.asc.embedding.ScriptCompiler",
"-optimize",
"-builtin",
"-apiversioning",
"-version",
"9",
"-outdir",
&out_dir.to_string_lossy(),
"-out",
@ -114,18 +118,54 @@ fn resolve_multiname_name<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a st
}
}
// Strips off the version mark inserted by 'asc.jar',
// giving us a valid Rust module name. The actual versioning logic
// is handling in Ruffle when we load playerglobals
fn strip_version_mark(val: &str) -> &str {
const MIN_API_MARK: usize = 0xE000;
const MAX_API_MARK: usize = 0xF8FF;
if let Some(chr) = val.chars().last() {
if chr as usize >= MIN_API_MARK && chr as usize <= MAX_API_MARK {
// The version mark is 3 bytes in utf-8
return &val[..val.len() - 3];
}
}
val
}
// Like `resolve_multiname_name`, but for namespaces instead.
fn resolve_multiname_ns<'a>(abc: &'a AbcFile, multiname: &Multiname) -> &'a str {
if let Multiname::QName { namespace, .. } = multiname {
let ns = &abc.constant_pool.namespaces[namespace.0 as usize - 1];
if let Namespace::Package(p) | Namespace::PackageInternal(p) = ns {
&abc.constant_pool.strings[p.0 as usize - 1]
} else {
panic!("Unexpected Namespace {ns:?}");
let ns = match multiname {
Multiname::QName { namespace, .. } => {
&abc.constant_pool.namespaces[namespace.0 as usize - 1]
}
Multiname::Multiname { namespace_set, .. } => {
if namespace_set.0 == 0 {
panic!("Multiname namespace set must not be null");
}
let actual_index = namespace_set.0 as usize - 1;
let ns_set = abc
.constant_pool
.namespace_sets
.get(actual_index)
.unwrap_or_else(|| panic!("Unknown namespace set constant {}", actual_index));
if ns_set.len() == 1 {
&abc.constant_pool.namespaces[ns_set[0].0 as usize - 1]
} else {
panic!("Found multiple namespaces in namespace set {ns_set:?}")
}
}
_ => panic!("Unexpected Multiname {multiname:?}"),
};
let namespace = if let Namespace::Package(p) | Namespace::PackageInternal(p) = ns {
&abc.constant_pool.strings[p.0 as usize - 1]
} else {
panic!("Unexpected Multiname {multiname:?}");
}
panic!("Unexpected Namespace {ns:?}");
};
strip_version_mark(namespace)
}
fn flash_to_rust_path(path: &str) -> String {

View File

@ -29,6 +29,7 @@ macro_rules! avm_debug {
pub mod activation;
mod amf;
pub mod api_version;
mod array;
pub mod bytearray;
mod call_stack;
@ -77,8 +78,10 @@ pub use crate::avm2::object::{
pub use crate::avm2::qname::QName;
pub use crate::avm2::value::Value;
use self::api_version::ApiVersion;
use self::object::WeakObject;
use self::scope::Scope;
use num_traits::FromPrimitive;
const BROADCAST_WHITELIST: [&str; 4] = ["enterFrame", "exitFrame", "frameConstructed", "render"];
@ -113,7 +116,13 @@ pub struct Avm2<'gc> {
/// However, it's not strictly defined which items end up there.
toplevel_global_object: Option<Object<'gc>>,
pub public_namespace: Namespace<'gc>,
/// The public namespace, versioned with `ApiVersion::ALL_VERSIONS`.
/// When calling into user code, you should almost always use `find_public_namespace`
/// instead, as it will return the correct version for the current call stack.
public_namespace_base_version: Namespace<'gc>,
// FIXME - make this an enum map once gc-arena supports it
public_namespaces: Vec<Namespace<'gc>>,
public_namespace_vm_internal: Namespace<'gc>,
pub internal_namespace: Namespace<'gc>,
pub as3_namespace: Namespace<'gc>,
pub vector_public_namespace: Namespace<'gc>,
@ -156,6 +165,14 @@ pub struct Avm2<'gc> {
/// strong references around (this matches Flash's behavior).
orphan_objects: Rc<Vec<DisplayObjectWeak<'gc>>>,
/// The api version of our root movie clip. Note - this is used as the
/// api version for swfs loaded via `Loader`, overriding the api version
/// specified in the loaded SWF. This is only used for API versioning (hiding
/// definitions from playerglobals) - version-specific behavior in things like
/// `gotoAndPlay` uses the current movie clip's SWF version.
#[collect(require_static)]
pub root_api_version: ApiVersion,
#[cfg(feature = "avm_debug")]
pub debug_output: bool,
}
@ -167,6 +184,10 @@ impl<'gc> Avm2<'gc> {
let stage_domain =
Domain::uninitialized_domain(context.gc_context, Some(playerglobals_domain));
let public_namespaces = (0..=(ApiVersion::VM_INTERNAL as usize))
.map(|val| Namespace::package("", ApiVersion::from_usize(val).unwrap(), context))
.collect();
Self {
player_version,
stack: Vec::new(),
@ -177,13 +198,24 @@ impl<'gc> Avm2<'gc> {
system_classes: None,
toplevel_global_object: None,
public_namespace: Namespace::package("", context),
public_namespace_base_version: Namespace::package("", ApiVersion::AllVersions, context),
public_namespaces,
public_namespace_vm_internal: Namespace::package("", ApiVersion::VM_INTERNAL, context),
internal_namespace: Namespace::internal("", context),
as3_namespace: Namespace::package("http://adobe.com/AS3/2006/builtin", context),
vector_public_namespace: Namespace::package("__AS3__.vec", context),
as3_namespace: Namespace::package(
"http://adobe.com/AS3/2006/builtin",
ApiVersion::AllVersions,
context,
),
vector_public_namespace: Namespace::package(
"__AS3__.vec",
ApiVersion::AllVersions,
context,
),
vector_internal_namespace: Namespace::internal("__AS3__.vec", context),
proxy_namespace: Namespace::package(
"http://www.adobe.com/2006/actionscript/flash/proxy",
ApiVersion::AllVersions,
context,
),
// these are required to facilitate shared access between Rust and AS
@ -201,6 +233,9 @@ impl<'gc> Avm2<'gc> {
orphan_objects: Default::default(),
// Set the lowest version for now - this be overriden when we set our movie
root_api_version: ApiVersion::AllVersions,
#[cfg(feature = "avm_debug")]
debug_output: false,
}
@ -639,6 +674,13 @@ impl<'gc> Avm2<'gc> {
#[cfg(not(feature = "avm_debug"))]
pub const fn set_show_debug_output(&self, _visible: bool) {}
/// Gets the public namespace, versioned based on the current root SWF.
/// See `AvmCore::findPublicNamespace()`
/// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/AvmCore.cpp#L5809C25-L5809C25
pub fn find_public_namespace(&self) -> Namespace<'gc> {
self.public_namespaces[self.root_api_version as usize]
}
}
/// If the provided `DisplayObjectWeak` should have frames run, returns

View File

@ -761,7 +761,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
) -> Result<Namespace<'gc>, Error<'gc>> {
method
.translation_unit()
.pool_namespace(index, &mut self.borrow_gc())
.pool_namespace(index, &mut self.context)
}
/// Retrieve a multiname from the current constant pool.
@ -772,7 +772,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
) -> Result<Gc<'gc, Multiname<'gc>>, Error<'gc>> {
method
.translation_unit()
.pool_maybe_uninitialized_multiname(index, &mut self.borrow_gc())
.pool_maybe_uninitialized_multiname(index, &mut self.context)
}
/// Retrieve a multiname from the current constant pool.
@ -784,7 +784,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
) -> Result<Gc<'gc, Multiname<'gc>>, Error<'gc>> {
let name = method
.translation_unit()
.pool_maybe_uninitialized_multiname(index, &mut self.borrow_gc())?;
.pool_maybe_uninitialized_multiname(index, &mut self.context)?;
if name.has_lazy_component() {
let name = name.fill_with_runtime_params(self)?;
Ok(Gc::new(self.context.gc_context, name))
@ -804,7 +804,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
) -> Result<Gc<'gc, Multiname<'gc>>, Error<'gc>> {
method
.translation_unit()
.pool_multiname_static(index, &mut self.borrow_gc())
.pool_multiname_static(index, &mut self.context)
}
/// Retrieve a static, or non-runtime, multiname from the current constant
@ -818,7 +818,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
) -> Result<Gc<'gc, Multiname<'gc>>, Error<'gc>> {
method
.translation_unit()
.pool_multiname_static_any(index, &mut self.borrow_gc())
.pool_multiname_static_any(index, &mut self.context)
}
/// Retrieve a method entry from the current ABC file's method table.
@ -1652,7 +1652,7 @@ impl<'a, 'gc> Activation<'a, 'gc> {
}
let name = name_value.coerce_to_string(self)?;
let multiname = Multiname::new(self.avm2().public_namespace, name);
let multiname = Multiname::new(self.avm2().find_public_namespace(), name);
let has_prop = obj.has_property_via_in(self, &multiname)?;
self.push_stack(has_prop);
@ -1672,11 +1672,8 @@ impl<'a, 'gc> Activation<'a, 'gc> {
// for `finally` scopes, FP just creates a bare object.
self.avm2().classes().object.construct(self, &[])?
} else {
let qname = QName::from_abc_multiname(
method.translation_unit(),
vname,
&mut self.borrow_gc(),
)?;
let qname =
QName::from_abc_multiname(method.translation_unit(), vname, &mut self.context)?;
ScriptObject::catch_scope(self.context.gc_context, &qname)
};
self.push_stack(so);

View File

@ -0,0 +1,96 @@
// Based on https://github.com/adobe/avmplus/blob/master/core/api-versions.h
// FIXME - if we ever add in AIR emulation, then implement the 'version series' logic.
// For now, it's fine to just compare Flash player version against air versions directly.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, FromPrimitive)]
#[allow(non_camel_case_types)]
pub enum ApiVersion {
AllVersions = 0,
AIR_1_0 = 1,
FP_10_0 = 2,
AIR_1_5 = 3,
AIR_1_5_1 = 4,
FP_10_0_32 = 5,
AIR_1_5_2 = 6,
FP_10_1 = 7,
AIR_2_0 = 8,
AIR_2_5 = 9,
FP_10_2 = 10,
AIR_2_6 = 11,
SWF_12 = 12,
AIR_2_7 = 13,
SWF_13 = 14,
AIR_3_0 = 15,
SWF_14 = 16,
AIR_3_1 = 17,
SWF_15 = 18,
AIR_3_2 = 19,
SWF_16 = 20,
AIR_3_3 = 21,
SWF_17 = 22,
AIR_3_4 = 23,
SWF_18 = 24,
AIR_3_5 = 25,
SWF_19 = 26,
AIR_3_6 = 27,
SWF_20 = 28,
AIR_3_7 = 29,
SWF_21 = 30,
AIR_3_8 = 31,
SWF_22 = 32,
AIR_3_9 = 33,
SWF_23 = 34,
AIR_4_0 = 35,
SWF_24 = 36,
AIR_13_0 = 37,
SWF_25 = 38,
AIR_14_0 = 39,
SWF_26 = 40,
AIR_15_0 = 41,
SWF_27 = 42,
AIR_16_0 = 43,
SWF_28 = 44,
AIR_17_0 = 45,
SWF_29 = 46,
AIR_18_0 = 47,
SWF_30 = 48,
AIR_19_0 = 49,
SWF_31 = 50,
AIR_20_0 = 51,
VM_INTERNAL = 52,
}
impl ApiVersion {
pub fn from_swf_version(val: u8) -> Option<ApiVersion> {
match val {
// There's no specific entry for SWF 9 in avmplus,
// so map it to the lowest entry.
9 => Some(ApiVersion::AllVersions),
10 => Some(ApiVersion::FP_10_1),
11 => Some(ApiVersion::FP_10_2),
12 => Some(ApiVersion::SWF_12),
13 => Some(ApiVersion::SWF_13),
14 => Some(ApiVersion::SWF_14),
15 => Some(ApiVersion::SWF_15),
16 => Some(ApiVersion::SWF_16),
17 => Some(ApiVersion::SWF_17),
18 => Some(ApiVersion::SWF_18),
19 => Some(ApiVersion::SWF_19),
20 => Some(ApiVersion::SWF_20),
21 => Some(ApiVersion::SWF_21),
22 => Some(ApiVersion::SWF_22),
23 => Some(ApiVersion::SWF_23),
24 => Some(ApiVersion::SWF_24),
25 => Some(ApiVersion::SWF_25),
26 => Some(ApiVersion::SWF_26),
27 => Some(ApiVersion::SWF_27),
28 => Some(ApiVersion::SWF_28),
29 => Some(ApiVersion::SWF_29),
30 => Some(ApiVersion::SWF_30),
31 => Some(ApiVersion::SWF_31),
// We haven't yet created entries from higher versions - just map them
// to the highest non-VM_INTERNAL version.
_ if val > 31 => Some(ApiVersion::SWF_31),
_ => None,
}
}
}

View File

@ -303,20 +303,19 @@ impl<'gc> Class<'gc> {
.ok_or_else(|| "LoadError: Instance index not valid".into());
let abc_instance = abc_instance?;
let mut context = activation.borrow_gc();
let name = QName::from_abc_multiname(unit, abc_instance.name, &mut context)?;
let name = QName::from_abc_multiname(unit, abc_instance.name, &mut activation.context)?;
let super_class = if abc_instance.super_name.0 == 0 {
None
} else {
Some(
unit.pool_multiname_static(abc_instance.super_name, &mut context)?
unit.pool_multiname_static(abc_instance.super_name, &mut activation.context)?
.deref()
.clone(),
)
};
let protected_namespace = if let Some(ns) = &abc_instance.protected_namespace {
Some(unit.pool_namespace(*ns, &mut context)?)
Some(unit.pool_namespace(*ns, &mut activation.context)?)
} else {
None
};
@ -324,7 +323,7 @@ impl<'gc> Class<'gc> {
let mut interfaces = Vec::with_capacity(abc_instance.interfaces.len());
for interface_name in &abc_instance.interfaces {
interfaces.push(
unit.pool_multiname_static(*interface_name, &mut context)?
unit.pool_multiname_static(*interface_name, &mut activation.context)?
.deref()
.clone(),
);
@ -344,7 +343,7 @@ impl<'gc> Class<'gc> {
// When loading a class from our playerglobal, grab the corresponding native
// allocator function from the table (which may be `None`)
if unit.domain().is_playerglobals_domain(activation) {
if unit.domain().is_playerglobals_domain(activation.avm2()) {
instance_allocator = activation.avm2().native_instance_allocator_table
[class_index as usize]
.map(|(_name, ptr)| Allocator(ptr));
@ -461,8 +460,9 @@ impl<'gc> Class<'gc> {
if let Some(superclass) = superclass {
for instance_trait in self.instance_traits.iter() {
let is_protected =
self.protected_namespace() == Some(instance_trait.name().namespace());
let is_protected = self.protected_namespace().map_or(false, |prot| {
prot.exact_version_match(instance_trait.name().namespace())
});
let mut current_superclass = Some(superclass);
let mut did_override = false;
@ -476,9 +476,11 @@ impl<'gc> Class<'gc> {
let my_name = instance_trait.name();
let names_match = super_name.local_name() == my_name.local_name()
&& (super_name.namespace() == my_name.namespace()
&& (super_name.namespace().matches_ns(my_name.namespace())
|| (is_protected
&& read.protected_namespace() == Some(super_name.namespace())));
&& read.protected_namespace().map_or(false, |prot| {
prot.exact_version_match(super_name.namespace())
})));
if names_match {
match (supertrait.kind(), instance_trait.kind()) {
//Getter/setter pairs do NOT override one another
@ -538,7 +540,7 @@ impl<'gc> Class<'gc> {
Ok(GcCell::new(
activation.context.gc_context,
Self {
name: QName::new(activation.avm2().public_namespace, name),
name: QName::new(activation.avm2().public_namespace_base_version, name),
param: None,
super_class: None,
attributes: ClassAttributes::empty(),
@ -601,7 +603,7 @@ impl<'gc> Class<'gc> {
for &(name, value) in items {
self.define_class_trait(Trait::from_const(
QName::new(namespace, name),
Multiname::new(activation.avm2().public_namespace, "Number"),
Multiname::new(activation.avm2().public_namespace_base_version, "Number"),
Some(value.into()),
));
}
@ -616,7 +618,7 @@ impl<'gc> Class<'gc> {
for &(name, value) in items {
self.define_class_trait(Trait::from_const(
QName::new(namespace, name),
Multiname::new(activation.avm2().public_namespace, "uint"),
Multiname::new(activation.avm2().public_namespace_base_version, "uint"),
Some(value.into()),
));
}
@ -631,7 +633,7 @@ impl<'gc> Class<'gc> {
for &(name, value) in items {
self.define_class_trait(Trait::from_const(
QName::new(namespace, name),
Multiname::new(activation.avm2().public_namespace, "int"),
Multiname::new(activation.avm2().public_namespace_base_version, "int"),
Some(value.into()),
));
}
@ -747,7 +749,7 @@ impl<'gc> Class<'gc> {
for &(name, value) in items {
self.define_instance_trait(Trait::from_slot(
QName::new(namespace, name),
Multiname::new(activation.avm2().public_namespace, "Number"),
Multiname::new(activation.avm2().public_namespace_base_version, "Number"),
value.map(|v| v.into()),
));
}

View File

@ -14,6 +14,7 @@ use ruffle_wstr::WStr;
use super::class::Class;
use super::error::error;
use super::string::AvmString;
use super::Avm2;
/// Represents a set of scripts and movies that share traits across different
/// script-global scopes.
@ -69,8 +70,8 @@ impl<'gc> Domain<'gc> {
))
}
pub fn is_playerglobals_domain(&self, activation: &mut Activation<'_, 'gc>) -> bool {
activation.avm2().playerglobals_domain.0.as_ptr() == self.0.as_ptr()
pub fn is_playerglobals_domain(&self, avm2: &Avm2<'gc>) -> bool {
avm2.playerglobals_domain.0.as_ptr() == self.0.as_ptr()
}
/// Create a new domain with a given parent.
@ -252,12 +253,14 @@ impl<'gc> Domain<'gc> {
));
name = "__AS3__.vec::Vector".into();
}
let name = QName::from_qualified_name(name, activation);
// FIXME - is this the correct api version?
let api_version = activation.avm2().root_api_version;
let name = QName::from_qualified_name(name, api_version, activation);
let res = self.get_defined_value(activation, name);
if let Some(type_name) = type_name {
let type_qname = QName::from_qualified_name(type_name, activation);
let type_qname = QName::from_qualified_name(type_name, api_version, activation);
let type_class = self.get_defined_value(activation, type_qname)?;
if let Ok(res) = res {
let class = res.as_object().ok_or_else(|| {
@ -292,11 +295,16 @@ impl<'gc> Domain<'gc> {
/// Export a class into the current application domain.
///
/// This does nothing if the definition already exists in this domain or a parent.
pub fn export_class(&self, class: GcCell<'gc, Class<'gc>>, mc: &Mutation<'gc>) {
if self.has_class(class.read().name()) {
pub fn export_class(
&self,
export_name: QName<'gc>,
class: GcCell<'gc, Class<'gc>>,
mc: &Mutation<'gc>,
) {
if self.has_class(export_name) {
return;
}
self.0.write(mc).classes.insert(class.read().name(), class);
self.0.write(mc).classes.insert(export_name, class);
}
pub fn is_default_domain_memory(&self) -> bool {

View File

@ -1248,11 +1248,11 @@ pub fn string_to_multiname<'gc>(
) -> Multiname<'gc> {
if let Some(name) = name.strip_prefix(b'@') {
let name = AvmString::new(activation.context.gc_context, name);
Multiname::attribute(activation.avm2().public_namespace, name)
Multiname::attribute(activation.avm2().public_namespace_base_version, name)
} else if &*name == b"*" {
Multiname::any(activation.context.gc_context)
} else {
Multiname::new(activation.avm2().public_namespace, name)
Multiname::new(activation.avm2().public_namespace_base_version, name)
}
}

View File

@ -1,4 +1,5 @@
use crate::avm2::activation::Activation;
use crate::avm2::api_version::ApiVersion;
use crate::avm2::class::Class;
use crate::avm2::domain::Domain;
use crate::avm2::object::{ClassObject, Object, ScriptObject, TObject};
@ -310,7 +311,11 @@ fn define_fn_on_global<'gc>(
) {
let (_, global, domain) = script.init();
let qname = QName::new(
Namespace::package(package, &mut activation.borrow_gc()),
Namespace::package(
package,
ApiVersion::AllVersions,
&mut activation.borrow_gc(),
),
name,
);
let func = domain
@ -391,8 +396,7 @@ fn class<'gc>(
activation.avm2().classes().class,
);
domain.export_definition(class_name, script, mc);
domain.export_class(class_def, mc);
domain.export_class(class_name, class_def, mc);
Ok(class_object)
}
@ -478,24 +482,24 @@ pub fn load_player_globals<'gc>(
let object_classdef = object::create_class(activation);
let object_class = ClassObject::from_class_partial(activation, object_classdef, None)?;
let object_proto = ScriptObject::custom_object(mc, Some(object_class), None);
domain.export_class(object_classdef, mc);
domain.export_class(object_classdef.read().name(), object_classdef, mc);
let fn_classdef = function::create_class(activation);
let fn_class = ClassObject::from_class_partial(activation, fn_classdef, Some(object_class))?;
let fn_proto = ScriptObject::custom_object(mc, Some(fn_class), Some(object_proto));
domain.export_class(fn_classdef, mc);
domain.export_class(fn_classdef.read().name(), fn_classdef, mc);
let class_classdef = class::create_class(activation);
let class_class =
ClassObject::from_class_partial(activation, class_classdef, Some(object_class))?;
let class_proto = ScriptObject::custom_object(mc, Some(object_class), Some(object_proto));
domain.export_class(class_classdef, mc);
domain.export_class(class_classdef.read().name(), class_classdef, mc);
let global_classdef = global_scope::create_class(activation);
let global_class =
ClassObject::from_class_partial(activation, global_classdef, Some(object_class))?;
let global_proto = ScriptObject::custom_object(mc, Some(object_class), Some(object_proto));
domain.export_class(global_classdef, mc);
domain.export_class(global_classdef.read().name(), global_classdef, mc);
// Now to weave the Gordian knot...
object_class.link_prototype(activation, object_proto)?;
@ -691,7 +695,8 @@ fn load_playerglobal<'gc>(
($activation:expr, $script:expr, [$(($package:expr, $class_name:expr, $field:ident)),* $(,)?]) => {
let activation = $activation;
$(
let ns = Namespace::package($package, &mut activation.borrow_gc());
// Lookup with the highest version, so we we see all defined classes here
let ns = Namespace::package($package, ApiVersion::VM_INTERNAL, &mut activation.borrow_gc());
let name = QName::new(ns, $class_name);
let class_object = activation.domain().get_defined_value(activation, name)?;
let class_object = class_object.as_object().unwrap().as_class_object().unwrap();

View File

@ -63,6 +63,28 @@ class is loaded.
See `flash/events/Event.as` for an example
## API Versioning
Ruffle supports Flash's Api versioning, which hides newer playerglobal definitions
(including methods/properties) from SWFs compiled with older API versions.
For example, see `Event.WORKER_STATE`
To add versioning to an API:
1. Determine the first version where it was added. This can be seen in the Flash Documentation (e.g. "Runtime Versions: Flash Player 11.4, AIR 3.4")
2. Convert the Flash Player version to an SWF version number using [this chart](https://github.com/ruffle-rs/ruffle/wiki/SWF-version-chart)
2. Determine the corresponding asc.jar version code for the SWF version. This can be found in avmplus in https://github.com/adobe/avmplus/blob/master/core/api-versions.as
3. Add an `[API("VersionCode")]` metadata to the defintion. In the `Event.WORKER_STATE` example,
this looks like:
```actionscript
[API("682")]
public static const WORKER_STATE:String = "workerState";
```
WORKER_STATE was added in Flash Player 11.4, which corresponds to SWF version 17. Looking at the avmplus file, this corresponds
to a version code of "682".
## Compiling
Java must be installed for the build process to complete.

View File

@ -1252,8 +1252,11 @@ pub fn remove_at<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "Array"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "Array"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Array instance initializer>", mc),
Method::from_builtin(class_init, "<Array class initializer>", mc),
mc,
@ -1271,7 +1274,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
)] = &[("length", Some(length), Some(set_length))];
write.define_builtin_instance_properties(
mc,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_INSTANCE_PROPERTIES,
);
@ -1295,14 +1298,14 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
("UNIQUESORT", SortOptions::UNIQUE_SORT.bits() as u32),
];
write.define_constant_uint_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CONSTANTS_UINT,
activation,
);
const CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CONSTANTS_INT,
activation,
);

View File

@ -4,7 +4,7 @@ use crate::avm2::method::Method;
use crate::avm2::object::{ArrayObject, TObject};
use crate::avm2::parameters::ParametersExt;
use crate::avm2::property::Property;
use crate::avm2::ClassObject;
use crate::avm2::{ClassObject, Namespace};
use crate::avm2::{Activation, Error, Object, Value};
use crate::avm2_stub_method;
@ -193,33 +193,22 @@ fn describe_internal_body<'gc>(
// Implement the weird 'HIDE_NSURI_METHODS' behavior from avmplus:
// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/TypeDescriber.cpp#L237
let mut skip_ns = Vec::new();
let mut skip_ns: Vec<Namespace<'_>> = Vec::new();
if let Some(super_vtable) = super_vtable {
for (_, ns, prop) in super_vtable.resolved_traits().iter() {
if !ns.as_uri().is_empty() {
if let Property::Method { disp_id } = prop {
let method = super_vtable
.get_full_method(*disp_id)
.unwrap_or_else(|| panic!("Missing method for id {disp_id:?}"));
let is_playerglobals = method
.class
.class_scope()
.domain()
.is_playerglobals_domain(activation);
if !skip_ns.contains(&(ns, is_playerglobals)) {
skip_ns.push((ns, is_playerglobals));
if let Property::Method { .. } = prop {
if !skip_ns
.iter()
.any(|other_ns| other_ns.exact_version_match(ns))
{
skip_ns.push(ns);
}
}
}
}
}
let class_is_playerglobals = class_obj
.class_scope()
.domain()
.is_playerglobals_domain(activation);
// FIXME - avmplus iterates over their own hashtable, so the order in the final XML
// is different
for (prop_name, ns, prop) in vtable.resolved_traits().iter() {
@ -227,35 +216,10 @@ fn describe_internal_body<'gc>(
continue;
}
// Hack around our lack of namespace versioning.
// This is hack to work around the fact that we don't have namespace versioning
// Once we do, methods from playerglobals should end up distinct public and AS3
// namespaces, due to the special `kApiVersion_VM_ALLVERSIONS` used:
// https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/AbcParser.cpp#L1497
//
// The main way this is
// observable is by having a class like this:
//
// ``
// class SubClass extends SuperClass {
// AS3 function subclassMethod {}
// }
// class SuperClass {}
// ```
//
// Here, `subclassMethod` will not get hidden - even though `Object`
// has AS3 methods, they are in the playerglobal AS3 namespace
// (with version kApiVersion_VM_ALLVERSIONS), which is distinct
// from the AS3 namespace used by SubClass. However, if we have any
// user-defined classes in the inheritance chain, then the namespace
// *should* match (if the swf version numbers match).
//
// For now, we approximate this by checking if the declaring class
// and our starting class are both in the playerglobals domain
// or both not in the playerglobals domain. If not, then we ignore
// `skip_ns`, since we should really have two different namespaces here.
if flags.contains(DescribeTypeFlags::HIDE_NSURI_METHODS)
&& skip_ns.contains(&(ns, class_is_playerglobals))
&& skip_ns
.iter()
.any(|other_ns| ns.exact_version_match(*other_ns))
{
continue;
}

View File

@ -129,8 +129,11 @@ fn value_of<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "Boolean"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "Boolean"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Boolean instance initializer>", mc),
Method::from_builtin(class_init, "<Boolean class initializer>", mc),
mc,
@ -160,7 +163,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
const CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CONSTANTS_INT,
activation,
);

View File

@ -47,8 +47,11 @@ fn prototype<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let gc_context = activation.context.gc_context;
let class_class = Class::new(
QName::new(activation.avm2().public_namespace, "Class"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "Class"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Class instance initializer>", gc_context),
Method::from_builtin(class_init, "<Class class initializer>", gc_context),
gc_context,
@ -60,7 +63,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
// We need to define it, since it shows up in 'describeType'
const CLASS_CONSTANTS: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CLASS_CONSTANTS,
activation,
);
@ -72,7 +75,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
)] = &[("prototype", Some(prototype), None)];
write.define_builtin_instance_properties(
gc_context,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_INSTANCE_PROPERTIES,
);

View File

@ -1326,8 +1326,11 @@ pub fn parse<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "Date"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "Date"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Date instance initializer>", mc),
Method::from_builtin(class_init, "<Date class initializer>", mc),
mc,
@ -1371,7 +1374,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
];
write.define_builtin_instance_properties(
mc,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_INSTANCE_PROPERTIES,
);
write.define_builtin_instance_methods(
@ -1384,14 +1387,14 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
write.define_builtin_class_methods(
mc,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_CLASS_METHODS,
);
const CLASS_CONSTANTS_INT: &[(&str, i32)] = &[("length", 7)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CLASS_CONSTANTS_INT,
activation,
);

View File

@ -53,7 +53,9 @@ package flash.display {
public native function get y():Number;
public native function set y(value:Number):void;
[API("662")]
public native function get z():Number;
[API("662")]
public native function set z(value:Number):void;
public native function get rotation():Number;

View File

@ -10,6 +10,7 @@ package flash.display {
public native function get enabled():Boolean;
public native function set enabled(value:Boolean):void;
public native function get framesLoaded():int;
[API("674")]
public native function get isPlaying():Boolean;
public native function get scenes():Array;
public native function get totalFrames():int;

View File

@ -85,6 +85,7 @@ package flash.events {
public static const CHANNEL_STATE:String = "channelState";
[API("682")]
public static const WORKER_STATE:String = "workerState";
public function Event(type:String, bubbles:Boolean = false, cancelable:Boolean = false) {

View File

@ -152,7 +152,7 @@ pub fn to_string<'gc>(
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let object_proto = activation.avm2().classes().object.prototype();
let name = Multiname::new(activation.avm2().public_namespace, "toString");
let name = Multiname::new(activation.avm2().public_namespace_base_version, "toString");
object_proto
.get_property(&name, activation)?

View File

@ -1,5 +1,6 @@
//! `flash.net.SharedObject` builtin/prototype
use crate::avm2::api_version::ApiVersion;
use crate::avm2::error::error;
use crate::avm2::object::TObject;
use crate::avm2::Error::AvmError;
@ -147,7 +148,11 @@ pub fn get_local<'gc>(
// Set the internal name
let ruffle_name = Multiname::new(
Namespace::package("__ruffle__", &mut activation.borrow_gc()),
Namespace::package(
"__ruffle__",
ApiVersion::AllVersions,
&mut activation.borrow_gc(),
),
"_ruffleName",
);
this.set_property(
@ -194,7 +199,11 @@ pub fn flush<'gc>(
.coerce_to_object(activation)?;
let ruffle_name = Multiname::new(
Namespace::package("__ruffle__", &mut activation.borrow_gc()),
Namespace::package(
"__ruffle__",
ApiVersion::AllVersions,
&mut activation.borrow_gc(),
),
"_ruffleName",
);
let name = this
@ -231,7 +240,11 @@ pub fn get_size<'gc>(
.coerce_to_object(activation)?;
let ruffle_name = Multiname::new(
Namespace::package("__ruffle__", &mut activation.borrow_gc()),
Namespace::package(
"__ruffle__",
ApiVersion::AllVersions,
&mut activation.borrow_gc(),
),
"_ruffleName",
);
let name = this
@ -274,7 +287,11 @@ pub fn clear<'gc>(
// Delete data from storage backend.
let ruffle_name = Multiname::new(
Namespace::package("__ruffle__", &mut activation.borrow_gc()),
Namespace::package(
"__ruffle__",
ApiVersion::AllVersions,
&mut activation.borrow_gc(),
),
"_ruffleName",
);
let name = this

View File

@ -50,7 +50,7 @@ pub fn get_parent_domain<'gc>(
) -> Result<Value<'gc>, Error<'gc>> {
if let Some(appdomain) = this.as_application_domain() {
if let Some(parent_domain) = appdomain.parent_domain() {
if parent_domain.is_playerglobals_domain(activation) {
if parent_domain.is_playerglobals_domain(activation.avm2()) {
return Ok(Value::Null);
}
return Ok(DomainObject::from_domain(activation, parent_domain)?.into());

View File

@ -1,7 +1,6 @@
//! `flash.utils` namespace
use crate::avm2::object::TObject;
use crate::avm2::{Activation, Error, Object, Value};
use crate::string::AvmString;
use crate::string::WString;

View File

@ -182,8 +182,11 @@ fn set_prototype<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let gc_context = activation.context.gc_context;
let function_class = Class::new(
QName::new(activation.avm2().public_namespace, "Function"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "Function"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Function instance initializer>", gc_context),
Method::from_builtin(class_init, "<Function class initializer>", gc_context),
gc_context,
@ -209,13 +212,13 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
];
write.define_builtin_instance_properties(
gc_context,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_INSTANCE_PROPERTIES,
);
const CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CONSTANTS_INT,
activation,
);

View File

@ -36,8 +36,11 @@ pub fn class_init<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
Class::new(
QName::new(activation.avm2().public_namespace, "global"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "global"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<global instance initializer>", mc),
Method::from_builtin(class_init, "<global class initializer>", mc),
mc,

View File

@ -215,8 +215,11 @@ fn value_of<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "int"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "int"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin_and_params(
instance_init,
"<int instance initializer>",
@ -251,7 +254,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
("length", 1),
];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CLASS_CONSTANTS,
activation,
);

View File

@ -28,16 +28,19 @@ pub fn instance_init<'gc>(
_ => None,
};
let api_version = activation.avm2().root_api_version;
let namespace = match uri_value {
Some(Value::Object(Object::QNameObject(qname))) => qname
.uri()
.map(|uri| Namespace::package(uri, &mut activation.borrow_gc()))
.map(|uri| Namespace::package(uri, api_version, &mut activation.borrow_gc()))
.unwrap_or_else(|| Namespace::any(activation.context.gc_context)),
Some(val) => Namespace::package(
val.coerce_to_string(activation)?,
api_version,
&mut activation.borrow_gc(),
),
None => activation.avm2().public_namespace,
None => activation.avm2().public_namespace_base_version,
};
this.init_namespace(activation.context.gc_context, namespace);
@ -105,8 +108,11 @@ pub fn uri<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "Namespace"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "Namespace"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Namespace instance initializer>", mc),
Method::from_builtin(class_init, "<Namespace class initializer>", mc),
mc,
@ -132,13 +138,13 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
)] = &[("prefix", Some(prefix), None), ("uri", Some(uri), None)];
write.define_builtin_instance_properties(
mc,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_INSTANCE_PROPERTIES,
);
const CONSTANTS_INT: &[(&str, i32)] = &[("length", 2)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CONSTANTS_INT,
activation,
);

View File

@ -371,8 +371,11 @@ fn value_of<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "Number"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "Number"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Number instance initializer>", mc),
Method::from_builtin(class_init, "<Number class initializer>", mc),
mc,
@ -408,14 +411,14 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
("LOG10E", std::f64::consts::LOG10_E),
];
write.define_constant_number_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CLASS_CONSTANTS_NUMBER,
activation,
);
const CLASS_CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CLASS_CONSTANTS_INT,
activation,
);

View File

@ -254,7 +254,7 @@ pub fn init<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let gc_context = activation.context.gc_context;
let object_class = Class::new(
QName::new(activation.avm2().public_namespace, "Object"),
QName::new(activation.avm2().public_namespace_base_version, "Object"),
None,
Method::from_builtin(instance_init, "<Object instance initializer>", gc_context),
Method::from_builtin(class_init, "<Object class initializer>", gc_context),
@ -268,8 +268,8 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
));
write.define_class_trait(Trait::from_const(
QName::new(activation.avm2().public_namespace, "length"),
Multiname::new(activation.avm2().public_namespace, "int"),
QName::new(activation.avm2().public_namespace_base_version, "length"),
Multiname::new(activation.avm2().public_namespace_base_version, "int"),
Some(1.into()),
));
@ -284,7 +284,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
Multiname::any(activation.context.gc_context),
Value::Undefined,
)],
Multiname::new(activation.avm2().public_namespace, "Boolean"),
Multiname::new(activation.avm2().public_namespace_base_version, "Boolean"),
),
(
"isPrototypeOf",
@ -294,7 +294,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
Multiname::any(activation.context.gc_context),
Value::Undefined,
)],
Multiname::new(activation.avm2().public_namespace, "Boolean"),
Multiname::new(activation.avm2().public_namespace_base_version, "Boolean"),
),
(
"propertyIsEnumerable",
@ -304,7 +304,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
Multiname::any(activation.context.gc_context),
Value::Undefined,
)],
Multiname::new(activation.avm2().public_namespace, "Boolean"),
Multiname::new(activation.avm2().public_namespace_base_version, "Boolean"),
),
];
write.define_builtin_instance_methods_with_sig(

View File

@ -1,6 +1,7 @@
//! `QName` impl
use crate::avm2::activation::Activation;
use crate::avm2::api_version::ApiVersion;
use crate::avm2::object::{Object, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
@ -46,16 +47,19 @@ pub fn init<'gc>(
let ns_arg = args.get(0).cloned().unwrap();
let local_arg = args.get(1).cloned().unwrap_or(Value::Undefined);
let api_version = activation.avm2().root_api_version;
let namespace = match ns_arg {
Value::Object(o) if o.as_namespace().is_some() => o.as_namespace().as_deref().copied(),
Value::Object(o) if o.as_qname_object().is_some() => o
.as_qname_object()
.unwrap()
.uri()
.map(|uri| Namespace::package(uri, &mut activation.borrow_gc())),
Value::Object(o) if o.as_qname_object().is_some() => {
o.as_qname_object().unwrap().uri().map(|uri| {
Namespace::package(uri, ApiVersion::AllVersions, &mut activation.borrow_gc())
})
}
Value::Undefined | Value::Null => None,
v => Some(Namespace::package(
v.coerce_to_string(activation)?,
api_version,
&mut activation.borrow_gc(),
)),
};
@ -81,7 +85,7 @@ pub fn init<'gc>(
};
if &*local != b"*" {
this.set_local_name(activation.context.gc_context, local);
Some(activation.avm2().public_namespace)
Some(activation.avm2().find_public_namespace())
} else {
None
}

View File

@ -651,8 +651,11 @@ fn to_upper_case<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "String"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "String"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<String instance initializer>", mc),
Method::from_builtin(class_init, "<String class initializer>", mc),
mc,
@ -674,7 +677,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
)] = &[("length", Some(length), None)];
write.define_builtin_instance_properties(
mc,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_INSTANCE_PROPERTIES,
);
write.define_builtin_instance_methods(
@ -685,7 +688,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
const CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CONSTANTS_INT,
activation,
);
@ -695,7 +698,7 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
write.define_builtin_class_methods(mc, activation.avm2().as3_namespace, AS3_CLASS_METHODS);
write.define_builtin_class_methods(
mc,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_CLASS_METHODS,
);

View File

@ -216,8 +216,11 @@ fn value_of<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "uint"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
QName::new(activation.avm2().public_namespace_base_version, "uint"),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin_and_params(
instance_init,
"<uint instance initializer>",
@ -251,14 +254,14 @@ pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Cl
const CLASS_CONSTANTS_UINT: &[(&str, u32)] =
&[("MAX_VALUE", u32::MAX), ("MIN_VALUE", u32::MIN)];
write.define_constant_uint_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CLASS_CONSTANTS_UINT,
activation,
);
const CLASS_CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)];
write.define_constant_int_class_traits(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
CLASS_CONSTANTS_INT,
activation,
);

View File

@ -917,7 +917,10 @@ pub fn create_generic_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().vector_public_namespace, "Vector"),
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(generic_init, "<Vector instance initializer>", mc),
Method::from_builtin(generic_init, "<Vector class initializer>", mc),
mc,
@ -950,7 +953,10 @@ pub fn create_builtin_class<'gc>(
let class = Class::new(
name,
Some(Multiname::new(activation.avm2().public_namespace, "Object")),
Some(Multiname::new(
activation.avm2().public_namespace_base_version,
"Object",
)),
Method::from_builtin(instance_init, "<Vector.<T> instance initializer>", mc),
Method::from_builtin(class_init, "<Vector.<T> class initializer>", mc),
mc,
@ -981,7 +987,7 @@ pub fn create_builtin_class<'gc>(
];
write.define_builtin_instance_properties(
mc,
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
PUBLIC_INSTANCE_PROPERTIES,
);

View File

@ -23,7 +23,7 @@ fn void_init<'gc>(
pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> GcCell<'gc, Class<'gc>> {
let mc = activation.context.gc_context;
let class = Class::new(
QName::new(activation.avm2().public_namespace, "void"),
QName::new(activation.avm2().public_namespace_base_version, "void"),
None,
Method::from_builtin(void_init, "", mc),
Method::from_builtin(void_init, "", mc),

View File

@ -1,5 +1,6 @@
//! XML builtin and prototype
use crate::avm2::api_version::ApiVersion;
use crate::avm2::e4x::{name_to_multiname, E4XNode, E4XNodeKind};
use crate::avm2::error::type_error;
pub use crate::avm2::object::xml_allocator;
@ -172,7 +173,11 @@ pub fn namespace_internal_impl<'gc>(
// FIXME: Nodes currently either have zero or one namespace, which has the prefix "" (empty string)
Ok(match node.namespace() {
Some(ns) if prefix.is_empty() => {
let namespace = Namespace::package(ns, &mut activation.context.borrow_gc());
let namespace = Namespace::package(
ns,
ApiVersion::AllVersions,
&mut activation.context.borrow_gc(),
);
NamespaceObject::from_namespace(activation, namespace)?.into()
}
_ => Value::Undefined,
@ -422,7 +427,7 @@ pub fn append_child<'gc>(
.expect("Should have an XMLList");
let length = xml_list.length();
let name = Multiname::new(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
AvmString::new_utf8(activation.context.gc_context, length.to_string()),
);
xml_list.set_property_local(&name, child, activation)?;

View File

@ -63,7 +63,7 @@ impl<'gc> ParamConfig<'gc> {
AvmString::from("<Unnamed Parameter>")
};
let param_type_name = txunit
.pool_multiname_static_any(config.kind, &mut activation.borrow_gc())?
.pool_multiname_static_any(config.kind, &mut activation.context)?
.deref()
.clone();
@ -153,7 +153,7 @@ impl<'gc> BytecodeMethod<'gc> {
}
let return_type = txunit
.pool_multiname_static_any(method.return_type, &mut activation.borrow_gc())?
.pool_multiname_static_any(method.return_type, &mut activation.context)?
.deref()
.clone();

View File

@ -4,7 +4,7 @@ use crate::avm2::Error;
use crate::avm2::Namespace;
use crate::avm2::QName;
use crate::avm2::{Object, Value};
use crate::context::GcContext;
use crate::context::UpdateContext;
use crate::string::{AvmString, WStr, WString};
use bitflags::bitflags;
use gc_arena::Gc;
@ -17,7 +17,7 @@ use swf::avm2::types::{
#[derive(Clone, Copy, Debug, Collect)]
#[collect(no_drop)]
enum NamespaceSet<'gc> {
pub enum NamespaceSet<'gc> {
Multiple(Gc<'gc, Vec<Namespace<'gc>>>),
Single(Namespace<'gc>),
}
@ -115,10 +115,10 @@ impl<'gc> Multiname<'gc> {
/// Read a namespace set from the ABC constant pool, and return a list of
/// copied namespaces.
fn abc_namespace_set(
pub fn abc_namespace_set(
translation_unit: TranslationUnit<'gc>,
namespace_set_index: Index<AbcNamespaceSet>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<NamespaceSet<'gc>, Error<'gc>> {
if namespace_set_index.0 == 0 {
return Err(Error::RustError(
@ -153,7 +153,7 @@ impl<'gc> Multiname<'gc> {
pub fn from_abc_index(
translation_unit: TranslationUnit<'gc>,
multiname_index: Index<AbcMultiname>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<Self, Error<'gc>> {
let mc = context.gc_context;
let abc = translation_unit.abc();
@ -164,7 +164,7 @@ impl<'gc> Multiname<'gc> {
Self {
ns: NamespaceSet::single(translation_unit.pool_namespace(*namespace, context)?),
name: translation_unit
.pool_string_option(name.0, context)?
.pool_string_option(name.0, &mut context.borrow_gc())?
.map(|v| v.into()),
param: None,
flags: Default::default(),
@ -173,7 +173,7 @@ impl<'gc> Multiname<'gc> {
AbcMultiname::RTQName { name } | AbcMultiname::RTQNameA { name } => Self {
ns: NamespaceSet::multiple(vec![], mc),
name: translation_unit
.pool_string_option(name.0, context)?
.pool_string_option(name.0, &mut context.borrow_gc())?
.map(|v| v.into()),
param: None,
flags: MultinameFlags::HAS_LAZY_NS,
@ -194,7 +194,7 @@ impl<'gc> Multiname<'gc> {
} => Self {
ns: Self::abc_namespace_set(translation_unit, *namespace_set, context)?,
name: translation_unit
.pool_string_option(name.0, context)?
.pool_string_option(name.0, &mut context.borrow_gc())?
.map(|v| v.into()),
param: None,
flags: Default::default(),
@ -389,7 +389,7 @@ impl<'gc> Multiname<'gc> {
let ns_match = self
.namespace_set()
.iter()
.any(|ns| ns.is_any() || *ns == name.namespace());
.any(|ns| ns.is_any() || ns.matches_ns(name.namespace()));
let name_match = self.name.map(|n| n == name.local_name()).unwrap_or(true);
ns_match && name_match

View File

@ -1,27 +1,19 @@
use crate::avm2::Error;
use crate::context::UpdateContext;
use crate::string::{AvmAtom, AvmString};
use crate::{avm2::script::TranslationUnit, context::GcContext};
use gc_arena::{Collect, Gc, Mutation};
use num_traits::FromPrimitive;
use ruffle_wstr::WStr;
use std::fmt::Debug;
use swf::avm2::types::{Index, Namespace as AbcNamespace};
use super::api_version::ApiVersion;
#[derive(Clone, Copy, Collect, Debug)]
#[collect(no_drop)]
pub struct Namespace<'gc>(Gc<'gc, NamespaceData<'gc>>);
impl<'gc> PartialEq for Namespace<'gc> {
fn eq(&self, other: &Self) -> bool {
if Gc::as_ptr(self.0) == Gc::as_ptr(other.0) {
true
} else if self.is_private() || other.is_private() {
false
} else {
*self.0 == *other.0
}
}
}
impl<'gc> Eq for Namespace<'gc> {}
/// Represents the name of a namespace.
#[allow(clippy::enum_variant_names)]
#[derive(Copy, Clone, Collect, Debug, PartialEq, Eq)]
@ -29,7 +21,7 @@ impl<'gc> Eq for Namespace<'gc> {}
enum NamespaceData<'gc> {
// note: this is the default "public namespace", corresponding to both
// ABC Namespace and PackageNamespace
Namespace(AvmAtom<'gc>),
Namespace(AvmAtom<'gc>, #[collect(require_static)] ApiVersion),
PackageInternal(AvmAtom<'gc>),
Protected(AvmAtom<'gc>),
Explicit(AvmAtom<'gc>),
@ -40,15 +32,47 @@ enum NamespaceData<'gc> {
Any,
}
fn strip_version_mark(url: &WStr) -> Option<(&WStr, ApiVersion)> {
// See https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/AvmCore.h#L485
const MIN_API_MARK: usize = 0xE000;
const MAX_API_MARK: usize = 0xF8FF;
const WEIRD_START_MARK: usize = 0xE294;
if let Some(Ok(chr)) = url.chars().last() {
let chr = chr as usize;
if chr >= MIN_API_MARK && chr <= MAX_API_MARK {
// Note - sometimes asc.jar emits a version mark of 0xE000.
// We treat this as `AllVersions`
let version = if chr >= WEIRD_START_MARK {
// Note that is an index into the ApiVersion enum, *not* an SWF version
ApiVersion::from_usize(chr - WEIRD_START_MARK)
.unwrap_or_else(|| panic!("Bad version mark {chr}"))
} else {
ApiVersion::AllVersions
};
// Since we had a char in the range 0xE000-0xF8FF, we must
// have a wide WStr, so we can remove this char by just
// removing the last byte
assert!(url.is_wide());
let stripped = &url[..url.len() - 1];
return Some((stripped, version));
}
}
None
}
impl<'gc> Namespace<'gc> {
/// Read a namespace declaration from the ABC constant pool and copy it to
/// a namespace value.
/// NOTE: you should use the TranslationUnit.pool_namespace instead of calling this.
/// otherwise you run a risk of creating a duplicate of private ns singleton.
/// Based on https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/AbcParser.cpp#L1459
pub fn from_abc_namespace(
translation_unit: TranslationUnit<'gc>,
namespace_index: Index<AbcNamespace>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<Self, Error<'gc>> {
if namespace_index.0 == 0 {
return Ok(Self::any(context.gc_context));
@ -61,29 +85,79 @@ impl<'gc> Namespace<'gc> {
.namespaces
.get(actual_index)
.ok_or_else(|| format!("Unknown namespace constant {}", namespace_index.0).into());
let abc_namespace = abc_namespace?;
let ns = match abc_namespace? {
AbcNamespace::Namespace(idx) => {
NamespaceData::Namespace(translation_unit.pool_string(idx.0, context)?)
let index = match abc_namespace {
AbcNamespace::Namespace(idx)
| AbcNamespace::Package(idx)
| AbcNamespace::PackageInternal(idx)
| AbcNamespace::Protected(idx)
| AbcNamespace::Explicit(idx)
| AbcNamespace::StaticProtected(idx)
| AbcNamespace::Private(idx) => idx,
};
let mut namespace_name = translation_unit.pool_string(index.0, &mut context.borrow_gc())?;
// Private namespaces don't get any of the namespace version checks
if let AbcNamespace::Private(_) = abc_namespace {
return Ok(Self(Gc::new(
context.gc_context,
NamespaceData::Private(namespace_name),
)));
}
// FIXME - what other versioned urls are there?
let is_versioned_url = |url: AvmAtom<'gc>| url.as_wstr().is_empty();
let is_public = matches!(
abc_namespace,
AbcNamespace::Namespace(_) | AbcNamespace::Package(_)
);
let api_version = if index.0 != 0 {
let is_playerglobals = translation_unit
.domain()
.is_playerglobals_domain(context.avm2);
let mut api_version = ApiVersion::AllVersions;
let stripped = strip_version_mark(namespace_name.as_wstr());
let has_version_mark = stripped.is_some();
if let Some((stripped, version)) = stripped {
assert!(
is_playerglobals,
"Found versioned url {namespace_name:?} in non-playerglobals domain"
);
let stripped_string = AvmString::new(context.gc_context, stripped);
namespace_name = context.interner.intern(context.gc_context, stripped_string);
api_version = version;
}
AbcNamespace::Package(idx) => {
NamespaceData::Namespace(translation_unit.pool_string(idx.0, context)?)
}
AbcNamespace::PackageInternal(idx) => {
NamespaceData::PackageInternal(translation_unit.pool_string(idx.0, context)?)
}
AbcNamespace::Protected(idx) => {
NamespaceData::Protected(translation_unit.pool_string(idx.0, context)?)
}
AbcNamespace::Explicit(idx) => {
NamespaceData::Explicit(translation_unit.pool_string(idx.0, context)?)
}
AbcNamespace::StaticProtected(idx) => {
NamespaceData::StaticProtected(translation_unit.pool_string(idx.0, context)?)
}
AbcNamespace::Private(idx) => {
NamespaceData::Private(translation_unit.pool_string(idx.0, context)?)
if is_playerglobals {
if !has_version_mark && is_public && is_versioned_url(namespace_name) {
api_version = ApiVersion::VM_INTERNAL;
}
} else if is_public {
api_version = translation_unit.api_version(context.avm2);
};
api_version
} else {
// Note - avmplus walks the (user) call stack to determine the API version.
// However, Flash Player appears to always use the root SWF api version
// for all swfs (e.g. those loaded through `Loader`). We can simply our code
// by skipping walking the stack, and just using the API version of our root SWF.
context.avm2.root_api_version
};
let ns = match abc_namespace {
AbcNamespace::Namespace(_) | AbcNamespace::Package(_) => {
NamespaceData::Namespace(namespace_name, api_version)
}
AbcNamespace::PackageInternal(_) => NamespaceData::PackageInternal(namespace_name),
AbcNamespace::Protected(_) => NamespaceData::Protected(namespace_name),
AbcNamespace::Explicit(_) => NamespaceData::Explicit(namespace_name),
AbcNamespace::StaticProtected(_) => NamespaceData::StaticProtected(namespace_name),
AbcNamespace::Private(_) => unreachable!(),
};
Ok(Self(Gc::new(context.gc_context, ns)))
}
@ -95,12 +169,16 @@ impl<'gc> Namespace<'gc> {
// TODO(moulins): allow passing an AvmAtom or a non-static `&WStr` directly
pub fn package(
package_name: impl Into<AvmString<'gc>>,
api_version: ApiVersion,
context: &mut GcContext<'_, 'gc>,
) -> Self {
let atom = context
.interner
.intern(context.gc_context, package_name.into());
Self(Gc::new(context.gc_context, NamespaceData::Namespace(atom)))
Self(Gc::new(
context.gc_context,
NamespaceData::Namespace(atom, api_version),
))
}
// TODO(moulins): allow passing an AvmAtom or a non-static `&WStr` directly
@ -118,11 +196,11 @@ impl<'gc> Namespace<'gc> {
}
pub fn is_public(&self) -> bool {
matches!(*self.0, NamespaceData::Namespace(name) if name.as_wstr().is_empty())
matches!(*self.0, NamespaceData::Namespace(name, _) if name.as_wstr().is_empty())
}
pub fn is_public_ignoring_ns(&self) -> bool {
matches!(*self.0, NamespaceData::Namespace(_))
matches!(*self.0, NamespaceData::Namespace(_, _))
}
pub fn is_any(&self) -> bool {
@ -134,12 +212,12 @@ impl<'gc> Namespace<'gc> {
}
pub fn is_namespace(&self) -> bool {
matches!(*self.0, NamespaceData::Namespace(_))
matches!(*self.0, NamespaceData::Namespace(_, _))
}
pub fn as_uri_opt(&self) -> Option<AvmString<'gc>> {
match *self.0 {
NamespaceData::Namespace(a) => Some(a.into()),
NamespaceData::Namespace(a, _) => Some(a.into()),
NamespaceData::PackageInternal(a) => Some(a.into()),
NamespaceData::Protected(a) => Some(a.into()),
NamespaceData::Explicit(a) => Some(a.into()),
@ -155,4 +233,49 @@ impl<'gc> Namespace<'gc> {
pub fn as_uri(&self) -> AvmString<'gc> {
self.as_uri_opt().unwrap_or_else(|| "".into())
}
/// Compares two namespaces, requiring that their versions match exactly.
/// Normally, you should use `matches_ns,` which takes version compatibility
/// into account.
///
/// Namespace does not implement `PartialEq`, so that each caller is required
/// to explicitly choose either `exact_version_match` or `matches_ns`.
pub fn exact_version_match(&self, other: Self) -> bool {
if Gc::as_ptr(self.0) == Gc::as_ptr(other.0) {
true
} else if self.is_private() || other.is_private() {
false
} else {
*self.0 == *other.0
}
}
/// Compares this namespace to another, considering them equal if this namespace's version
/// is less than or equal to the other (definitions in this namespace version can be
/// seen by the other). This is used to implement `ProperyMap`, where we want to
/// a definition with `ApiVersion::SWF_16` to be visible when queried from
/// a SWF with `ApiVersion::SWF_16` or any higher version.
pub fn matches_ns(&self, other: Self) -> bool {
if self.exact_version_match(other) {
return true;
}
match (&*self.0, &*other.0) {
(
NamespaceData::Namespace(name1, version1),
NamespaceData::Namespace(name2, version2),
) => {
let name_matches = name1 == name2;
let version_matches = version1 <= version2;
if name_matches && !version_matches {
tracing::info!(
"Rejecting namespace match due to versions: {:?} {:?}",
self.0,
other.0
);
}
name_matches && version_matches
}
_ => false,
}
}
}

View File

@ -271,7 +271,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
self.get_property(
&Multiname::new(activation.avm2().public_namespace, name),
&Multiname::new(activation.avm2().find_public_namespace(), name),
activation,
)
}
@ -301,7 +301,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
let name = Multiname::new(activation.avm2().public_namespace, name);
let name = Multiname::new(activation.avm2().public_namespace_base_version, name);
self.set_property_local(&name, value, activation)
}
@ -368,7 +368,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
self.set_property(
&Multiname::new(activation.avm2().public_namespace, name),
&Multiname::new(activation.avm2().public_namespace_base_version, name),
value,
activation,
)
@ -552,7 +552,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
self.call_property(
&Multiname::new(activation.avm2().public_namespace, name),
&Multiname::new(activation.avm2().find_public_namespace(), name),
arguments,
activation,
)
@ -643,7 +643,10 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
name: impl Into<AvmString<'gc>>,
activation: &mut Activation<'_, 'gc>,
) -> bool {
self.has_property(&Multiname::new(activation.avm2().public_namespace, name))
self.has_property(&Multiname::new(
activation.avm2().find_public_namespace(),
name,
))
}
/// Indicates whether or not a property or trait exists on an object and is
@ -658,7 +661,10 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
name: impl Into<AvmString<'gc>>,
activation: &mut Activation<'_, 'gc>,
) -> Result<bool, Error<'gc>> {
Ok(self.has_own_property(&Multiname::new(activation.avm2().public_namespace, name)))
Ok(self.has_own_property(&Multiname::new(
activation.avm2().find_public_namespace(),
name,
)))
}
/// Returns true if an object has one or more traits of a given name.
@ -729,7 +735,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
activation: &mut Activation<'_, 'gc>,
name: impl Into<AvmString<'gc>>,
) -> Result<bool, Error<'gc>> {
let name = Multiname::new(activation.avm2().public_namespace, name);
let name = Multiname::new(activation.avm2().public_namespace_base_version, name);
self.delete_property(activation, &name)
}

View File

@ -377,7 +377,7 @@ impl<'gc> ClassObject<'gc> {
for interface_trait in iface_read.instance_traits() {
if !interface_trait.name().namespace().is_public() {
let public_name = QName::new(
activation.context.avm2.public_namespace,
activation.context.avm2.public_namespace_vm_internal,
interface_trait.name().local_name(),
);
self.instance_vtable().copy_property_for_interface(

View File

@ -21,7 +21,7 @@ pub fn namespace_allocator<'gc>(
activation.context.gc_context,
NamespaceObjectData {
base,
namespace: activation.context.avm2.public_namespace,
namespace: activation.context.avm2.public_namespace_base_version,
},
))
.into())

View File

@ -1,4 +1,5 @@
use crate::avm2::activation::Activation;
use crate::avm2::api_version::ApiVersion;
use crate::avm2::e4x::{E4XNode, E4XNodeKind};
use crate::avm2::error::make_error_1089;
use crate::avm2::object::script_object::ScriptObjectData;
@ -173,8 +174,12 @@ impl<'gc> XmlListObject<'gc> {
if !matches!(*last_node.kind(), E4XNodeKind::ProcessingInstruction(_)) {
if let Some(name) = last_node.local_name() {
let ns = match last_node.namespace() {
Some(ns) => Namespace::package(ns, &mut activation.context.borrow_gc()),
None => activation.avm2().public_namespace,
Some(ns) => Namespace::package(
ns,
ApiVersion::AllVersions,
&mut activation.context.borrow_gc(),
),
None => activation.avm2().public_namespace_base_version,
};
write.target_property = Some(Multiname::new(ns, name));
@ -812,7 +817,7 @@ impl<'gc> TObject<'gc> for XmlListObject<'gc> {
// FIXME: We probably need to take the namespace too.
// 2.e.i. Let z = ToAttributeName(x[i].[[Name]])
let z = Multiname::attribute(
activation.avm2().public_namespace,
activation.avm2().public_namespace_base_version,
child.local_name().expect("Attribute should have a name"),
);
// 2.e.ii. Call the [[Put]] method of x[i].[[Parent]] with arguments z and V

View File

@ -1,6 +1,7 @@
//! Object representation for XML objects
use crate::avm2::activation::Activation;
use crate::avm2::api_version::ApiVersion;
use crate::avm2::e4x::{string_to_multiname, E4XNode, E4XNodeKind};
use crate::avm2::error::make_error_1087;
use crate::avm2::object::script_object::ScriptObjectData;
@ -127,8 +128,12 @@ impl<'gc> XmlObject<'gc> {
pub fn namespace(&self, activation: &mut Activation<'_, 'gc>) -> Namespace<'gc> {
match self.0.read().node.namespace() {
Some(ns) => Namespace::package(ns, &mut activation.context.borrow_gc()),
None => activation.avm2().public_namespace,
Some(ns) => Namespace::package(
ns,
ApiVersion::AllVersions,
&mut activation.context.borrow_gc(),
),
None => activation.avm2().public_namespace_base_version,
}
}

View File

@ -59,7 +59,7 @@ impl<'gc, V> PropertyMap<'gc, V> {
pub fn get(&self, name: QName<'gc>) -> Option<&V> {
self.0.get(&name.local_name()).iter().find_map(|v| {
v.iter()
.filter(|(n, _)| *n == name.namespace())
.filter(|(n, _)| n.matches_ns(name.namespace()))
.map(|(_, v)| v)
.next()
})
@ -72,7 +72,7 @@ impl<'gc, V> PropertyMap<'gc, V> {
if let Some(local_name) = name.local_name() {
self.0.get(&local_name).iter().find_map(|v| {
v.iter()
.filter(|(n, _)| name.namespace_set().iter().any(|ns| *ns == *n))
.filter(|(n, _)| name.namespace_set().iter().any(|ns| n.matches_ns(*ns)))
.map(|(_, v)| v)
.next()
})
@ -88,7 +88,7 @@ impl<'gc, V> PropertyMap<'gc, V> {
if let Some(local_name) = name.local_name() {
self.0.get(&local_name).iter().find_map(|v| {
v.iter()
.filter(|(n, _)| name.namespace_set().iter().any(|ns| *ns == *n))
.filter(|(n, _)| name.namespace_set().iter().any(|ns| n.matches_ns(*ns)))
.map(|(ns, v)| (*ns, v))
.next()
})
@ -99,7 +99,10 @@ impl<'gc, V> PropertyMap<'gc, V> {
pub fn get_mut(&mut self, name: QName<'gc>) -> Option<&mut V> {
if let Some(bucket) = self.0.get_mut(&name.local_name()) {
if let Some((_, old_value)) = bucket.iter_mut().find(|(n, _)| *n == name.namespace()) {
if let Some((_, old_value)) = bucket
.iter_mut()
.find(|(n, _)| n.matches_ns(name.namespace()))
{
return Some(old_value);
}
}
@ -111,7 +114,7 @@ impl<'gc, V> PropertyMap<'gc, V> {
self.0
.get(&name.local_name())
.iter()
.any(|v| v.iter().any(|(n, _)| *n == name.namespace()))
.any(|v| v.iter().any(|(n, _)| n.matches_ns(name.namespace())))
}
pub fn iter(&self) -> impl Iterator<Item = (AvmString<'gc>, Namespace<'gc>, &V)> {
@ -123,7 +126,10 @@ impl<'gc, V> PropertyMap<'gc, V> {
pub fn insert(&mut self, name: QName<'gc>, mut value: V) -> Option<V> {
let bucket = self.0.entry(name.local_name()).or_default();
if let Some((_, old_value)) = bucket.iter_mut().find(|(n, _)| *n == name.namespace()) {
if let Some((_, old_value)) = bucket
.iter_mut()
.find(|(n, _)| n.matches_ns(name.namespace()))
{
swap(old_value, &mut value);
Some(value)
@ -142,7 +148,7 @@ impl<'gc, V> PropertyMap<'gc, V> {
) -> Option<V> {
let bucket = self.0.entry(name).or_default();
if let Some((_, old_value)) = bucket.iter_mut().find(|(n, _)| *n == ns) {
if let Some((_, old_value)) = bucket.iter_mut().find(|(n, _)| n.matches_ns(ns)) {
swap(old_value, &mut value);
Some(value)
@ -161,7 +167,7 @@ impl<'gc, V> PropertyMap<'gc, V> {
let position = bucket
.iter_mut()
.enumerate()
.find(|(_, (n, _))| *n == name.namespace());
.find(|(_, (n, _))| n.matches_ns(name.namespace()));
if let Some((position, _)) = position {
return Some(bucket.remove(position).1);
}

View File

@ -1,12 +1,15 @@
use crate::avm2::script::TranslationUnit;
use crate::avm2::{Activation, Error, Namespace};
use crate::context::GcContext;
use crate::context::UpdateContext;
use crate::either::Either;
use crate::string::{AvmString, WStr, WString};
use gc_arena::{Collect, Mutation};
use std::fmt::Debug;
use swf::avm2::types::{Index, Multiname as AbcMultiname};
use super::api_version::ApiVersion;
use super::Multiname;
/// A `QName`, likely "qualified name", consists of a namespace and name string.
///
/// This is technically interchangeable with `xml::XMLName`, as they both
@ -26,7 +29,7 @@ pub struct QName<'gc> {
impl<'gc> PartialEq for QName<'gc> {
fn eq(&self, other: &Self) -> bool {
// Implemented by hand to enforce order of comparisons for perf
self.name == other.name && self.ns == other.ns
self.name == other.name && self.ns.exact_version_match(other.ns)
}
}
@ -47,7 +50,7 @@ impl<'gc> QName<'gc> {
pub fn from_abc_multiname(
translation_unit: TranslationUnit<'gc>,
multiname_index: Index<AbcMultiname>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<Self, Error<'gc>> {
if multiname_index.0 == 0 {
return Err("Attempted to load a trait name of index zero".into());
@ -64,8 +67,29 @@ impl<'gc> QName<'gc> {
Ok(match abc_multiname? {
AbcMultiname::QName { namespace, name } => Self {
ns: translation_unit.pool_namespace(*namespace, context)?,
name: translation_unit.pool_string(name.0, context)?.into(),
name: translation_unit
.pool_string(name.0, &mut context.borrow_gc())?
.into(),
},
AbcMultiname::Multiname {
namespace_set,
name,
} => {
let ns_set =
Multiname::abc_namespace_set(translation_unit, *namespace_set, context)?;
if ns_set.len() == 1 {
Self {
ns: ns_set.get(0).unwrap(),
name: translation_unit
.pool_string(name.0, &mut context.borrow_gc())?
.into(),
}
} else {
return Err(
"Attempted to pull QName from multiname with multiple namespaces".into(),
);
}
}
_ => return Err("Attempted to pull QName from non-QName multiname".into()),
})
}
@ -78,24 +102,28 @@ impl<'gc> QName<'gc> {
/// LOCAL_NAME (Use the public namespace)
///
/// This does *not* handle `Vector.<SomeTypeParam>` - use `get_defined_value_handling_vector` for that
pub fn from_qualified_name(name: AvmString<'gc>, activation: &mut Activation<'_, 'gc>) -> Self {
pub fn from_qualified_name(
name: AvmString<'gc>,
api_version: ApiVersion,
activation: &mut Activation<'_, 'gc>,
) -> Self {
let parts = name
.rsplit_once(WStr::from_units(b"::"))
.or_else(|| name.rsplit_once(WStr::from_units(b".")));
let mut context = activation.borrow_gc();
if let Some((package_name, local_name)) = parts {
let mut context = activation.borrow_gc();
let package_name = context
.interner
.intern_wstr(context.gc_context, package_name);
Self {
ns: Namespace::package(package_name, &mut context),
ns: Namespace::package(package_name, api_version, &mut context),
name: AvmString::new(context.gc_context, local_name),
}
} else {
Self {
ns: activation.avm2().public_namespace,
ns: Namespace::package("", api_version, &mut context),
name,
}
}

View File

@ -1,5 +1,6 @@
//! Whole script representation
use super::api_version::ApiVersion;
use super::traits::TraitKind;
use crate::avm2::activation::Activation;
use crate::avm2::class::Class;
@ -131,6 +132,15 @@ impl<'gc> TranslationUnit<'gc> {
self.0.read().movie.clone()
}
pub fn api_version(self, avm2: &Avm2<'gc>) -> ApiVersion {
if self.domain().is_playerglobals_domain(avm2) {
// FIXME: get this from the player version we're emulating
ApiVersion::SWF_31
} else {
avm2.root_api_version
}
}
/// Load a method from the ABC file and return its method definition.
pub fn load_method(
self,
@ -143,7 +153,7 @@ impl<'gc> TranslationUnit<'gc> {
return Ok(*method);
}
let is_global = read.domain.is_playerglobals_domain(activation);
let is_global = read.domain.is_playerglobals_domain(activation.avm2());
drop(read);
let bc_method =
@ -297,7 +307,7 @@ impl<'gc> TranslationUnit<'gc> {
pub fn pool_namespace(
self,
ns_index: Index<AbcNamespace>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<Namespace<'gc>, Error<'gc>> {
let read = self.0.read();
if let Some(Some(namespace)) = read.namespaces.get(ns_index.0 as usize) {
@ -317,7 +327,7 @@ impl<'gc> TranslationUnit<'gc> {
pub fn pool_maybe_uninitialized_multiname(
self,
multiname_index: Index<AbcMultiname>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<Gc<'gc, Multiname<'gc>>, Error<'gc>> {
let mc = context.gc_context;
let read = self.0.read();
@ -341,7 +351,7 @@ impl<'gc> TranslationUnit<'gc> {
pub fn pool_multiname_static(
self,
multiname_index: Index<AbcMultiname>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<Gc<'gc, Multiname<'gc>>, Error<'gc>> {
let multiname = self.pool_maybe_uninitialized_multiname(multiname_index, context)?;
if multiname.has_lazy_component() {
@ -358,7 +368,7 @@ impl<'gc> TranslationUnit<'gc> {
pub fn pool_multiname_static_any(
self,
multiname_index: Index<AbcMultiname>,
context: &mut GcContext<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc>,
) -> Result<Gc<'gc, Multiname<'gc>>, Error<'gc>> {
if multiname_index.0 == 0 {
let mc = context.gc_context;
@ -503,7 +513,7 @@ impl<'gc> Script<'gc> {
if let TraitKind::Class { class, .. } = newtrait.kind() {
write
.domain
.export_class(*class, activation.context.gc_context);
.export_class(newtrait.name(), *class, activation.context.gc_context);
}
write.traits.push(newtrait);

View File

@ -202,7 +202,7 @@ impl<'gc> Trait<'gc> {
abc_trait: &AbcTrait,
activation: &mut Activation<'_, 'gc>,
) -> Result<Self, Error<'gc>> {
let name = QName::from_abc_multiname(unit, abc_trait.name, &mut activation.borrow_gc())?;
let name = QName::from_abc_multiname(unit, abc_trait.name, &mut activation.context)?;
Ok(match &abc_trait.kind {
AbcTraitKind::Slot {
@ -211,7 +211,7 @@ impl<'gc> Trait<'gc> {
value,
} => {
let type_name = unit
.pool_multiname_static_any(*type_name, &mut activation.borrow_gc())?
.pool_multiname_static_any(*type_name, &mut activation.context)?
.deref()
.clone();
let default_value = slot_default_value(unit, value, &type_name, activation)?;
@ -278,7 +278,7 @@ impl<'gc> Trait<'gc> {
value,
} => {
let type_name = unit
.pool_multiname_static_any(*type_name, &mut activation.borrow_gc())?
.pool_multiname_static_any(*type_name, &mut activation.context)?
.deref()
.clone();
let default_value = slot_default_value(unit, value, &type_name, activation)?;

View File

@ -535,7 +535,7 @@ pub fn abc_default_value<'gc>(
| AbcDefaultValue::Explicit(ns)
| AbcDefaultValue::StaticProtected(ns)
| AbcDefaultValue::Private(ns) => {
let ns = translation_unit.pool_namespace(*ns, &mut activation.borrow_gc())?;
let ns = translation_unit.pool_namespace(*ns, &mut activation.context)?;
NamespaceObject::from_namespace(activation, ns).map(Into::into)
}
}

View File

@ -296,7 +296,7 @@ impl<'gc> VTable<'gc> {
// but with this class's protected namespace
for (local_name, ns, prop) in superclass_vtable.0.read().resolved_traits.iter()
{
if ns == super_protected_namespace {
if ns.exact_version_match(super_protected_namespace) {
let new_name = QName::new(protected_namespace, local_name);
write.resolved_traits.insert(new_name, *prop);
}

View File

@ -1885,8 +1885,10 @@ pub trait TDisplayObject<'gc>:
.avm2_domain();
let mut activation =
Avm2Activation::from_domain(context.reborrow(), domain);
let name =
Avm2Multiname::new(activation.avm2().public_namespace, self.name());
let name = Avm2Multiname::new(
activation.avm2().find_public_namespace(),
self.name(),
);
if let Err(e) = p.init_property(&name, c.into(), &mut activation) {
tracing::error!(
"Got error when setting AVM2 child named \"{}\": {}",

View File

@ -2,8 +2,8 @@
use crate::avm1::{Activation, ActivationIdentifier, TObject};
use crate::avm2::{
Activation as Avm2Activation, Avm2, EventObject as Avm2EventObject, TObject as _,
Value as Avm2Value,
Activation as Avm2Activation, Avm2, EventObject as Avm2EventObject, Multiname as Avm2Multiname,
TObject as _, Value as Avm2Value,
};
use crate::context::{RenderContext, UpdateContext};
use crate::display_object::avm1_button::Avm1Button;
@ -653,13 +653,16 @@ impl<'gc> ChildContainer<'gc> {
if child.has_explicit_name() {
if let Avm2Value::Object(parent_obj) = parent.object2() {
let mut activation = Avm2Activation::from_nothing(context.reborrow());
let current_val =
parent_obj.get_public_property(child.name(), &mut activation);
let name = Avm2Multiname::new(
activation.avm2().find_public_namespace(),
child.name(),
);
let current_val = parent_obj.get_property(&name, &mut activation);
match current_val {
Ok(Avm2Value::Null) | Ok(Avm2Value::Undefined) => {}
Ok(_other) => {
let res = parent_obj.set_public_property(
child.name(),
let res = parent_obj.set_property(
&name,
Avm2Value::Null,
&mut activation,
);

View File

@ -856,7 +856,11 @@ impl<'gc> MovieClip<'gc> {
reader.read_str()?.decode(reader.encoding()),
);
let name = Avm2QName::from_qualified_name(class_name, &mut activation);
let name = Avm2QName::from_qualified_name(
class_name,
activation.avm2().root_api_version,
&mut activation,
);
let library = activation
.context
.library

View File

@ -6,6 +6,7 @@ use crate::avm1::SystemProperties;
use crate::avm1::VariableDumper;
use crate::avm1::{Activation, ActivationIdentifier};
use crate::avm1::{ScriptObject, TObject, Value};
use crate::avm2::api_version::ApiVersion;
use crate::avm2::{
object::LoaderInfoObject, object::TObject as _, Activation as Avm2Activation, Avm2, CallStack,
Object as Avm2Object,
@ -383,6 +384,11 @@ impl Player {
self.instance_counter = 0;
self.mutate_with_update_context(|context| {
if context.swf.is_action_script_3() {
context.avm2.root_api_version = ApiVersion::from_swf_version(context.swf.version())
.unwrap_or_else(|| panic!("Unknown SWF version {}", context.swf.version()));
}
context.stage.set_movie_size(
context.gc_context,
context.swf.width().to_pixels() as u32,

View File

@ -0,0 +1,31 @@
package com.ruffle2 {
import flash.display.MovieClip;
import flash.display.Loader;
import flash.net.URLRequest;
import flash.events.Event;
public class Test extends MovieClip {
public function Test() {
var loader = new Loader();
var self = this;
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e) {
trace("Calling childPublicMethod on " + loader.content);
loader.content.childPublicMethod(self);
trace("Called childPublicMethod");
trace("Calling childAS3Method on " + loader.content);
loader.content.childAS3Method(self);
trace("Called childAS3Method");
})
loader.load(new URLRequest("newer/newer.swf"));
}
public function parentPublicMethod() {
trace("Called Test.parentPublicMethod");
}
AS3 function parentAS3Method() {
trace("Called Test.parentAS3Method()");
}
}
}

View File

@ -0,0 +1,28 @@
package com.ruffle {
import flash.display.MovieClip;
import flash.events.Event;
public class Newer extends MovieClip {
public function Newer() {
trace("Initialized Newer with parent: " + this.parent + " Event = " + Event["WORKER_STATE"]);
try {
this.gotoAndPlay("badFrame");
} catch (e) {
// FIXME - print the entire error when Ruffle matches Flash Player
trace("Caught error (truncated): " + e.toString().slice(0, 27));
}
}
public function childPublicMethod(target: Object) {
trace("Newer.childPublicMethod: Calling parentPublicMethod() on " + target);
target.parentPublicMethod();
trace("Newer.childPublicMethod: Called parentPublicMethod() on " + target);
}
AS3 function childAS3Method(target: Object) {
trace("Newer.childAS3Method: Calling parentAS3Method() on " + target);
target.parentAS3Method();
trace("Newer.childAS3Method: Called parentAS3Method() on " + target);
}
}
}

View File

@ -0,0 +1,12 @@
Initialized Newer with parent: null Event = undefined
Caught error (truncated): ArgumentError: Error #2109:
Calling childPublicMethod on [object Newer]
Newer.childPublicMethod: Calling parentPublicMethod() on [object Test]
Called Test.parentPublicMethod
Newer.childPublicMethod: Called parentPublicMethod() on [object Test]
Called childPublicMethod
Calling childAS3Method on [object Newer]
Newer.childAS3Method: Calling parentAS3Method() on [object Test]
Called Test.parentAS3Method()
Newer.childAS3Method: Called parentAS3Method() on [object Test]
Called childAS3Method

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 2

View File

@ -0,0 +1,31 @@
package com.ruffle2 {
import flash.display.MovieClip;
import flash.display.Loader;
import flash.net.URLRequest;
import flash.events.Event;
public class Test extends MovieClip {
public function Test() {
var loader = new Loader();
var self = this;
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e) {
trace("Calling childPublicMethod on " + loader.content);
loader.content.childPublicMethod(self);
trace("Called childPublicMethod");
trace("Calling childAS3Method on " + loader.content);
loader.content.childAS3Method(self);
trace("Called childAS3Method");
})
loader.load(new URLRequest("older/older.swf"));
}
public function parentPublicMethod() {
trace("Called Test.parentPublicMethod");
}
AS3 function parentAS3Method() {
trace("Called Test.parentAS3Method()");
}
}
}

View File

@ -0,0 +1,28 @@
package com.ruffle {
import flash.display.MovieClip;
import flash.events.Event;
public class Older extends MovieClip {
public function Older() {
trace("Initialized Older with parent: " + this.parent + " Event = " + Event["WORKER_STATE"]);
try {
this.gotoAndPlay("badFrame");
} catch (e) {
// FIXME - print the entire error when Ruffle matches Flash Player
trace("Caught error (truncated): " + e.toString().slice(0, 27));
}
}
public function childPublicMethod(target: Object) {
trace("Older.childPublicMethod: Calling parentPublicMethod() on " + target);
target.parentPublicMethod();
trace("Older.olderPublicMethod: Called parentPublicMethod() on " + target);
}
AS3 function childAS3Method(target: Object) {
trace("Older.childAS3Method: Calling parentAS3Method() on " + target);
target.parentAS3Method();
trace("Older.childAS3Method: Called parentAS3Method() on " + target);
}
}
}

View File

@ -0,0 +1,12 @@
Initialized Older with parent: null Event = workerState
Caught error (truncated): ArgumentError: Error #2109:
Calling childPublicMethod on [object Older]
Older.childPublicMethod: Calling parentPublicMethod() on [object Test]
Called Test.parentPublicMethod
Older.olderPublicMethod: Called parentPublicMethod() on [object Test]
Called childPublicMethod
Calling childAS3Method on [object Older]
Older.childAS3Method: Calling parentAS3Method() on [object Test]
Called Test.parentAS3Method()
Older.childAS3Method: Called parentAS3Method() on [object Test]
Called childAS3Method

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 2

View File

@ -0,0 +1,15 @@
package {
import flash.display.MovieClip;
public class Test extends MovieClip {
public function Test() {
this.isPlaying;
this["isPlaying"];
}
public function get isPlaying():Boolean {
trace("Custom non-override isPlaying");
return false;
}
}
}

View File

@ -0,0 +1,2 @@
Custom non-override isPlaying
Custom non-override isPlaying

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 1