From f7cda1186774c97cc075f7f606471cb16f1dac62 Mon Sep 17 00:00:00 2001 From: Lord-McSweeney Date: Sun, 15 Dec 2024 11:40:24 -0800 Subject: [PATCH] avm2: Port Boolean class to Actionscript This requires some minor changes to support it --- core/src/avm2.rs | 13 ++- core/src/avm2/e4x.rs | 36 +++++--- core/src/avm2/globals.rs | 23 +++-- core/src/avm2/globals/Boolean.as | 52 +++++++++++- core/src/avm2/globals/Toplevel.as | 2 + core/src/avm2/globals/XML.as | 19 +++-- core/src/avm2/globals/boolean.rs | 137 +----------------------------- core/src/avm2/globals/stubs.as | 1 - core/src/avm2/globals/xml.rs | 100 ++++++++++++++++++++++ 9 files changed, 209 insertions(+), 174 deletions(-) diff --git a/core/src/avm2.rs b/core/src/avm2.rs index be3c6352181d..1504fd5df5ed 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -3,9 +3,11 @@ use std::rc::Rc; use crate::avm2::class::{AllocatorFn, CustomConstructorFn}; +use crate::avm2::e4x::XmlSettings; use crate::avm2::error::{make_error_1014, make_error_1107, type_error, Error1014Type}; use crate::avm2::globals::{ - init_builtin_system_classes, init_native_system_classes, SystemClassDefs, SystemClasses, + init_builtin_system_class_defs, init_builtin_system_classes, init_native_system_classes, + SystemClassDefs, SystemClasses, }; use crate::avm2::method::{Method, NativeMethodImpl}; use crate::avm2::scope::ScopeChain; @@ -175,6 +177,9 @@ pub struct Avm2<'gc> { alias_to_class_map: FnvHashMap, ClassObject<'gc>>, class_to_alias_map: FnvHashMap, AvmString<'gc>>, + #[collect(require_static)] + pub xml_settings: XmlSettings, + /// 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 @@ -230,6 +235,8 @@ impl<'gc> Avm2<'gc> { alias_to_class_map: Default::default(), class_to_alias_map: Default::default(), + xml_settings: XmlSettings::new_default(), + // Set the lowest version for now - this will be overridden when we set our movie root_api_version: ApiVersion::AllVersions, @@ -707,6 +714,10 @@ impl<'gc> Avm2<'gc> { .load_classes(&mut activation) .expect("Classes should load"); + // These Classes are absolutely critical to the runtime, so make sure + // we've registered them before anything else. + init_builtin_system_class_defs(&mut activation); + // The second script (script #1) is Toplevel.as, and includes important // builtin classes such as Namespace, QName, and XML. tunit diff --git a/core/src/avm2/e4x.rs b/core/src/avm2/e4x.rs index 573f69dcbc39..e3dec1dc0cec 100644 --- a/core/src/avm2/e4x.rs +++ b/core/src/avm2/e4x.rs @@ -1,5 +1,4 @@ use crate::avm2::error::{make_error_1010, make_error_1085, make_error_1118, type_error}; -use crate::avm2::globals::slots::xml as xml_class_slots; use crate::avm2::object::{E4XOrXml, FunctionObject, NamespaceObject}; use crate::avm2::{Activation, Error, Multiname, TObject, Value}; use crate::string::{AvmString, WStr, WString}; @@ -1659,21 +1658,10 @@ pub fn to_xml_string<'gc>( xml: E4XOrXml<'gc>, activation: &mut Activation<'_, 'gc>, ) -> AvmString<'gc> { - let pretty_printing = activation - .avm2() - .classes() - .xml - .get_slot(xml_class_slots::PRETTY_PRINTING) - .coerce_to_boolean(); + let pretty_printing = activation.avm2().xml_settings.pretty_printing; let pretty = if pretty_printing { - let pretty_indent = activation - .avm2() - .classes() - .xml - .get_slot(xml_class_slots::PRETTY_INDENT) - .coerce_to_i32(activation) - .expect("shouldn't error"); + let pretty_indent = activation.avm2().xml_settings.pretty_indent; // NOTE: Negative values are invalid and are ignored. if pretty_indent < 0 { @@ -1785,3 +1773,23 @@ pub fn maybe_escape_child<'gc>( Ok(child) } + +pub struct XmlSettings { + pub ignore_comments: bool, + pub ignore_processing_instructions: bool, + pub ignore_whitespace: bool, + pub pretty_printing: bool, + pub pretty_indent: i32, +} + +impl XmlSettings { + pub fn new_default() -> Self { + XmlSettings { + ignore_comments: true, + ignore_processing_instructions: true, + ignore_whitespace: true, + pretty_printing: true, + pretty_indent: 2, + } + } +} diff --git a/core/src/avm2/globals.rs b/core/src/avm2/globals.rs index a9017762cab1..609be4cc1105 100644 --- a/core/src/avm2/globals.rs +++ b/core/src/avm2/globals.rs @@ -8,10 +8,7 @@ use crate::avm2::script::Script; use crate::avm2::traits::Trait; use crate::avm2::value::Value; use crate::avm2::vtable::VTable; -use crate::avm2::Avm2; -use crate::avm2::Error; -use crate::avm2::Namespace; -use crate::avm2::QName; +use crate::avm2::{Avm2, Error, Multiname, Namespace, QName}; use crate::tag_utils::{self, ControlFlow, SwfMovie, SwfSlice, SwfStream}; use gc_arena::Collect; use std::sync::Arc; @@ -572,7 +569,6 @@ pub fn load_player_globals<'gc>( )); let string_class = string::create_class(activation); - let boolean_class = boolean::create_class(activation); let number_class = number::create_class(activation); let int_class = int::create_class(activation); let uint_class = uint::create_class(activation); @@ -597,7 +593,6 @@ pub fn load_player_globals<'gc>( (public_ns, "Class", class_i_class), (public_ns, "Function", fn_classdef), (public_ns, "String", string_class), - (public_ns, "Boolean", boolean_class), (public_ns, "Number", number_class), (public_ns, "int", int_class), (public_ns, "uint", uint_class), @@ -740,7 +735,6 @@ pub fn load_player_globals<'gc>( // Make sure to initialize superclasses *before* their subclasses! avm2_system_class!(string, activation, string_class, script); - avm2_system_class!(boolean, activation, boolean_class, script); avm2_system_class!(number, activation, number_class, script); avm2_system_class!(int, activation, int_class, script); avm2_system_class!(uint, activation, uint_class, script); @@ -833,11 +827,12 @@ macro_rules! avm2_system_class_defs_playerglobal { ($activation:expr, [$(($package:expr, $class_name:expr, $field:ident)),* $(,)?]) => { let activation = $activation; $( + let domain = activation.domain(); + // Lookup with the highest version, so we we see all defined classes here let ns = Namespace::package($package, ApiVersion::VM_INTERNAL, activation.strings()); - let name = QName::new(ns, $class_name); - let class_object = activation.domain().get_defined_value(activation, name).unwrap_or_else(|e| panic!("Failed to lookup {name:?}: {e:?}")); - let class_def = class_object.as_object().unwrap().as_class_object().unwrap().inner_class_definition(); + let name = Multiname::new(ns, $class_name); + let class_def = domain.get_class(activation.context, &name).unwrap_or_else(|| panic!("Failed to lookup {name:?}")); let sc = activation.avm2().system_class_defs.as_mut().unwrap(); sc.$field = class_def; )* @@ -848,11 +843,12 @@ pub fn init_builtin_system_classes(activation: &mut Activation<'_, '_>) { avm2_system_classes_playerglobal!( &mut *activation, [ - ("", "Error", error), ("", "ArgumentError", argumenterror), - ("", "QName", qname), + ("", "Boolean", boolean), + ("", "Error", error), ("", "EvalError", evalerror), ("", "Namespace", namespace), + ("", "QName", qname), ("", "RangeError", rangeerror), ("", "ReferenceError", referenceerror), ("", "SecurityError", securityerror), @@ -864,10 +860,13 @@ pub fn init_builtin_system_classes(activation: &mut Activation<'_, '_>) { ("", "XMLList", xml_list), ] ); +} +pub fn init_builtin_system_class_defs(activation: &mut Activation<'_, '_>) { avm2_system_class_defs_playerglobal!( &mut *activation, [ + ("", "Boolean", boolean), ("", "Namespace", namespace), ("", "XML", xml), ("", "XMLList", xml_list), diff --git a/core/src/avm2/globals/Boolean.as b/core/src/avm2/globals/Boolean.as index 360ffe7c1bcd..0d45b42ec67c 100644 --- a/core/src/avm2/globals/Boolean.as +++ b/core/src/avm2/globals/Boolean.as @@ -1,5 +1,51 @@ -// This is a stub - the actual class is defined in `boolean.rs` package { - public class Boolean { - } + [Ruffle(CustomConstructor)] + [Ruffle(CallHandler)] + public final class Boolean { + public function Boolean(value:* = void 0) { + // The Boolean constructor is implemented natively: + // this AS-defined method does nothing + } + + prototype.toString = function():String { + if (this === Boolean.prototype) { + return "false"; + } + + if (!(this is Boolean)) { + throw new TypeError("Error #1004: Method Boolean.prototype.toString was invoked on an incompatible object.", 1004); + } + + return this.AS3::toString(); + }; + + prototype.valueOf = function():* { + if (this === Boolean.prototype) { + return false; + } + + if (!(this is Boolean)) { + throw new TypeError("Error #1004: Method Boolean.prototype.valueOf was invoked on an incompatible object.", 1004); + } + + return this; + }; + + prototype.setPropertyIsEnumerable("toString", false); + prototype.setPropertyIsEnumerable("valueOf", false); + + AS3 function toString():String { + if (this) { + return "true"; + } else { + return "false"; + } + } + + AS3 function valueOf():Boolean { + return this; + } + + public static const length:int = 1; + } } diff --git a/core/src/avm2/globals/Toplevel.as b/core/src/avm2/globals/Toplevel.as index 9787710f062e..db4be5c71e31 100644 --- a/core/src/avm2/globals/Toplevel.as +++ b/core/src/avm2/globals/Toplevel.as @@ -32,6 +32,8 @@ package { include "Error.as" +include "Boolean.as" + include "ArgumentError.as" include "DefinitionError.as" include "EvalError.as" diff --git a/core/src/avm2/globals/XML.as b/core/src/avm2/globals/XML.as index 4c73cba25db0..06876fe03a1f 100644 --- a/core/src/avm2/globals/XML.as +++ b/core/src/avm2/globals/XML.as @@ -103,15 +103,20 @@ package { AS3 native function notification():Function; AS3 native function setNotification(f:Function):*; - public static var ignoreComments:Boolean = true; - public static var ignoreProcessingInstructions:Boolean = true; - public static var ignoreWhitespace:Boolean = true; + public static native function get ignoreComments():Boolean; + public static native function set ignoreComments(value:Boolean):void; - [Ruffle(InternalSlot)] - public static var prettyPrinting:Boolean = true; + public static native function get ignoreProcessingInstructions():Boolean; + public static native function set ignoreProcessingInstructions(value:Boolean):void; - [Ruffle(InternalSlot)] - public static var prettyIndent:int = 2; + public static native function get ignoreWhitespace():Boolean; + public static native function set ignoreWhitespace(value:Boolean):void; + + public static native function get prettyPrinting():Boolean; + public static native function set prettyPrinting(value:Boolean):void; + + public static native function get prettyIndent():int; + public static native function set prettyIndent(value:int):void; prototype.hasComplexContent = function():Boolean { var self:XML = this; diff --git a/core/src/avm2/globals/boolean.rs b/core/src/avm2/globals/boolean.rs index 4c0ceef521b0..bc1f78bd598e 100644 --- a/core/src/avm2/globals/boolean.rs +++ b/core/src/avm2/globals/boolean.rs @@ -1,26 +1,10 @@ //! `Boolean` impl use crate::avm2::activation::Activation; -use crate::avm2::class::{Class, ClassAttributes}; -use crate::avm2::error::make_error_1004; -use crate::avm2::method::{Method, NativeMethodImpl}; -use crate::avm2::object::{FunctionObject, Object, TObject}; use crate::avm2::value::Value; use crate::avm2::Error; -use crate::avm2::QName; -/// Implements `Boolean`'s instance initializer. -/// -/// Because of the presence of a custom constructor, this method is unreachable. -fn instance_init<'gc>( - _activation: &mut Activation<'_, 'gc>, - _this: Value<'gc>, - _args: &[Value<'gc>], -) -> Result, Error<'gc>> { - unreachable!() -} - -fn boolean_constructor<'gc>( +pub fn boolean_constructor<'gc>( _activation: &mut Activation<'_, 'gc>, args: &[Value<'gc>], ) -> Result, Error<'gc>> { @@ -33,51 +17,6 @@ fn boolean_constructor<'gc>( Ok(bool_value.into()) } -/// Implements `Boolean`'s class initializer. -fn class_init<'gc>( - activation: &mut Activation<'_, 'gc>, - this: Value<'gc>, - _args: &[Value<'gc>], -) -> Result, Error<'gc>> { - let this = this.as_object().unwrap(); - - let scope = activation.create_scopechain(); - let gc_context = activation.context.gc_context; - let this_class = this.as_class_object().unwrap(); - let boolean_proto = this_class.prototype(); - - boolean_proto.set_string_property_local( - "toString", - FunctionObject::from_method( - activation, - Method::from_builtin(to_string, "toString", gc_context), - scope, - None, - None, - None, - ) - .into(), - activation, - )?; - boolean_proto.set_string_property_local( - "valueOf", - FunctionObject::from_method( - activation, - Method::from_builtin(value_of, "valueOf", gc_context), - scope, - None, - None, - None, - ) - .into(), - activation, - )?; - boolean_proto.set_local_property_is_enumerable(gc_context, "toString".into(), false); - boolean_proto.set_local_property_is_enumerable(gc_context, "valueOf".into(), false); - - Ok(Value::Undefined) -} - pub fn call_handler<'gc>( _activation: &mut Activation<'_, 'gc>, _this: Value<'gc>, @@ -90,77 +29,3 @@ pub fn call_handler<'gc>( .coerce_to_boolean() .into()) } - -/// Implements `Boolean.prototype.toString` -fn to_string<'gc>( - activation: &mut Activation<'_, 'gc>, - this: Value<'gc>, - _args: &[Value<'gc>], -) -> Result, Error<'gc>> { - match this { - Value::Bool(true) => return Ok("true".into()), - Value::Bool(false) => return Ok("false".into()), - _ => {} - } - - if let Some(this) = this.as_object() { - let boolean_proto = activation.avm2().classes().boolean.prototype(); - if Object::ptr_eq(boolean_proto, this) { - return Ok("false".into()); - } - } - - Err(make_error_1004(activation, "Boolean.prototype.toString")) -} - -/// Implements `Boolean.valueOf` -fn value_of<'gc>( - _activation: &mut Activation<'_, 'gc>, - this: Value<'gc>, - _args: &[Value<'gc>], -) -> Result, Error<'gc>> { - Ok(this) -} - -/// Construct `Boolean`'s class. -pub fn create_class<'gc>(activation: &mut Activation<'_, 'gc>) -> Class<'gc> { - let mc = activation.gc(); - let namespaces = activation.avm2().namespaces; - - let class = Class::new( - QName::new(namespaces.public_all(), "Boolean"), - Some(activation.avm2().class_defs().object), - Method::from_builtin(instance_init, "", mc), - Method::from_builtin(class_init, "", mc), - activation.avm2().class_defs().class, - mc, - ); - - class.set_attributes(mc, ClassAttributes::FINAL | ClassAttributes::SEALED); - class.set_custom_constructor(mc, boolean_constructor); - class.set_call_handler( - mc, - Method::from_builtin(call_handler, "", mc), - ); - - const AS3_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = - &[("toString", to_string), ("valueOf", value_of)]; - class.define_builtin_instance_methods(mc, namespaces.as3, AS3_INSTANCE_METHODS); - - const CONSTANTS_INT: &[(&str, i32)] = &[("length", 1)]; - class.define_constant_int_class_traits(namespaces.public_all(), CONSTANTS_INT, activation); - - class.mark_traits_loaded(activation.context.gc_context); - class - .init_vtable(activation.context) - .expect("Native class's vtable should initialize"); - - let c_class = class.c_class().expect("Class::new returns an i_class"); - - c_class.mark_traits_loaded(activation.context.gc_context); - c_class - .init_vtable(activation.context) - .expect("Native class's vtable should initialize"); - - class -} diff --git a/core/src/avm2/globals/stubs.as b/core/src/avm2/globals/stubs.as index ba8aec6991f4..b9444b7b3ffc 100644 --- a/core/src/avm2/globals/stubs.as +++ b/core/src/avm2/globals/stubs.as @@ -6,7 +6,6 @@ include "Object.as" // List is ordered alphabetically, except where superclasses // are listed before subclasses include "Array.as" -include "Boolean.as" include "Class.as" include "Function.as" diff --git a/core/src/avm2/globals/xml.rs b/core/src/avm2/globals/xml.rs index cd9c9d69e84f..fdd012ad48e2 100644 --- a/core/src/avm2/globals/xml.rs +++ b/core/src/avm2/globals/xml.rs @@ -102,6 +102,106 @@ pub fn init<'gc>( Ok(Value::Undefined) } +pub fn get_ignore_comments<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + Ok(Value::Bool(activation.avm2().xml_settings.ignore_comments)) +} + +pub fn set_ignore_comments<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + activation.avm2().xml_settings.ignore_comments = args.get_bool(0); + + Ok(Value::Undefined) +} + +pub fn get_ignore_processing_instructions<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + Ok(Value::Bool( + activation + .avm2() + .xml_settings + .ignore_processing_instructions, + )) +} + +pub fn set_ignore_processing_instructions<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + activation + .avm2() + .xml_settings + .ignore_processing_instructions = args.get_bool(0); + + Ok(Value::Undefined) +} + +pub fn get_ignore_whitespace<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + Ok(Value::Bool( + activation.avm2().xml_settings.ignore_whitespace, + )) +} + +pub fn set_ignore_whitespace<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + activation.avm2().xml_settings.ignore_whitespace = args.get_bool(0); + + Ok(Value::Undefined) +} + +pub fn get_pretty_printing<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + Ok(Value::Bool(activation.avm2().xml_settings.pretty_printing)) +} + +pub fn set_pretty_printing<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + activation.avm2().xml_settings.pretty_printing = args.get_bool(0); + + Ok(Value::Undefined) +} + +pub fn get_pretty_indent<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + _args: &[Value<'gc>], +) -> Result, Error<'gc>> { + Ok(Value::Integer(activation.avm2().xml_settings.pretty_indent)) +} + +pub fn set_pretty_indent<'gc>( + activation: &mut Activation<'_, 'gc>, + _this: Value<'gc>, + args: &[Value<'gc>], +) -> Result, Error<'gc>> { + activation.avm2().xml_settings.pretty_indent = args.get_i32(activation, 0)?; + + Ok(Value::Undefined) +} + pub fn normalize<'gc>( activation: &mut Activation<'_, 'gc>, this: Value<'gc>,