UNPKG

@awayfl/avm2

Version:

Virtual machine for executing AS3 code

565 lines (484 loc) 16.7 kB
import { defineNonEnumerableProperty, isNumeric, isNumber, isString, release, Debug } from '@awayfl/swf-loader'; import { ASObject } from './ASObject'; import { addPrototypeFunctionAlias } from './addPrototypeFunctionAlias'; import { makeMultiname } from './makeMultiname'; import { axCoerceName } from '../run/axCoerceName'; import { ensureBoxedReceiver } from '../run/ensureBoxedReceiver'; import { rn } from './rn'; import { Namespace } from '../abc/lazy/Namespace'; import { SORT } from '../abc/lazy/SORT'; import { Multiname } from '../abc/lazy/Multiname'; import { coerceArray } from './coerceArray'; import { createArrayValueFromArgs } from './createArrayValueFromArgs'; import { assert } from '@awayjs/graphics'; import { Bytecode } from '../abc/ops'; import { checkValue } from '../run/checkValue'; import { axDefaultCompareFunction } from '../run/axDefaultCompareFunction'; import { axCompare } from '../run/axCompare'; import { axCompareFields } from '../run/axCompareFields'; import { Errors } from '../errors'; import { axBoxPrimitive } from '../run/axBoxPrimitive'; import { ASFunction } from './ASFunction'; export class ASArray extends ASObject { /** * Proxy ASArray for allow use brackets notation [index], used in Box2D and other external modules * @param original */ static wrapProxy(original: ASArray): ASArray { return new Proxy(original, { set(target: ASArray, p: PropertyKey, value: any, receiver: any): boolean { if (typeof p === 'string' && !Number.isNaN(+p)) { target.value[+p] = value; return true; } target[p] = value; return true; }, get(target: ASArray, p: PropertyKey, receiver: any): any { if (typeof p === 'string' && !Number.isNaN(+p)) { return target.value[+p]; } return target[p]; } }); } static axBox (asValue: any[]): ASArray { return ASArray.wrapProxy(axBoxPrimitive.call(this, asValue)); } static classInitializer() { const proto: any = this.dPrototype; const asProto: any = ASArray.prototype; // option flags for sort and sortOn defineNonEnumerableProperty(this, '$BgCASEINSENSITIVE', 1); defineNonEnumerableProperty(this, '$BgDESCENDING', 2); defineNonEnumerableProperty(this, '$BgUNIQUESORT', 4); defineNonEnumerableProperty(this, '$BgRETURNINDEXEDARRAY', 8); defineNonEnumerableProperty(this, '$BgNUMERIC', 16); addPrototypeFunctionAlias(proto, '$Bgpush', asProto.generic_push); addPrototypeFunctionAlias(proto, '$Bgpop', asProto.generic_pop); addPrototypeFunctionAlias(proto, '$Bgshift', asProto.generic_shift); addPrototypeFunctionAlias(proto, '$Bgunshift', asProto.generic_unshift); addPrototypeFunctionAlias(proto, '$Bgreverse', asProto.generic_reverse); addPrototypeFunctionAlias(proto, '$Bgconcat', asProto.generic_concat); addPrototypeFunctionAlias(proto, '$Bgslice', asProto.generic_slice); addPrototypeFunctionAlias(proto, '$Bgsplice', asProto.generic_splice); addPrototypeFunctionAlias(proto, '$Bgjoin', asProto.generic_join); addPrototypeFunctionAlias(proto, '$BgtoString', asProto.generic_toString); addPrototypeFunctionAlias(proto, '$BgindexOf', asProto.generic_indexOf); addPrototypeFunctionAlias(proto, '$BglastIndexOf', asProto.generic_lastIndexOf); addPrototypeFunctionAlias(proto, '$Bgevery', asProto.generic_every); addPrototypeFunctionAlias(proto, '$Bgsome', asProto.generic_some); addPrototypeFunctionAlias(proto, '$BgforEach', asProto.generic_forEach); addPrototypeFunctionAlias(proto, '$Bgmap', asProto.generic_map); addPrototypeFunctionAlias(proto, '$Bgfilter', asProto.generic_filter); addPrototypeFunctionAlias(proto, '$Bgsort', asProto.generic_sort); addPrototypeFunctionAlias(proto, '$BgsortOn', asProto.generic_sortOn); addPrototypeFunctionAlias(proto, '$BghasOwnProperty', asProto.native_hasOwnProperty); addPrototypeFunctionAlias(proto, '$BgpropertyIsEnumerable', asProto.native_propertyIsEnumerable); addPrototypeFunctionAlias(proto, '$BgtoLocaleString', asProto.generic_toString); } constructor() { super(); this.value = createArrayValueFromArgs(this.sec, <any>arguments); return ASArray.wrapProxy(this); } native_hasOwnProperty(nm: string): boolean { return this.axHasOwnProperty(makeMultiname(nm)); } native_propertyIsEnumerable(nm: string): boolean { if (typeof nm === 'number' || isNumeric(nm = axCoerceName(nm))) { const descriptor = Object.getOwnPropertyDescriptor(this.value, nm); return !!descriptor && descriptor.enumerable; } super.native_propertyIsEnumerable(nm); } $Bglength: number; value: any []; public static axApply(self: ASArray, args: any[]): ASArray { return this.sec.createArrayUnsafe(createArrayValueFromArgs(this.sec, args)); } public static axConstruct(args: any[]): ASArray { return this.sec.createArrayUnsafe(createArrayValueFromArgs(this.sec, args)); } push() { // Amazingly, AS3 doesn't throw an error if `push` would make the argument too large. // Instead, it just replaces the last element. if (this.value.length + arguments.length > 0xffffffff) { const limit = 0xffffffff - this.value.length; for (let i = 0; i < limit; i++) { this.value.push(arguments[i]); } return 0xffffffff; } return this.value.push.apply(this.value, arguments); } generic_push() { if (this && this.value instanceof Array) { return this.push.apply(this, arguments); } let n = this.axGetPublicProperty('length') >>> 0; for (let i = 0; i < arguments.length; i++) { this.axSetNumericProperty(n++, arguments[i]); } this.axSetPublicProperty('length', n); return n; } pop() { return this.value.pop(); } generic_pop() { if (this && this.value instanceof Array) { return this.value.pop(); } const len = this.axGetPublicProperty('length') >>> 0; if (!len) { this.axSetPublicProperty('length', 0); return; } const retVal = this.axGetNumericProperty(len - 1); rn.name = len - 1; rn.namespaces = [Namespace.PUBLIC]; this.axDeleteProperty(rn); this.axSetPublicProperty('length', len - 1); return retVal; } shift() { return this.value.shift(); } generic_shift() { return coerceArray(this).shift(); } unshift() { return this.value.unshift.apply(this.value, arguments); } generic_unshift() { const self = coerceArray(this); return self.value.unshift.apply(self.value, arguments); } reverse() { this.value.reverse(); return this; } generic_reverse() { return coerceArray(this).reverse(); } concat() { const value = this.value.slice(); for (let i = 0; i < arguments.length; i++) { const a = arguments[i]; // Treat all objects with a `sec` property and a value that's an Array as // concat-spreadable. // TODO: verify that this is correct. if (typeof a === 'object' && a && a.sec && Array.isArray(a.value)) { value.push.apply(value, a.value); } else { value.push(a); } } return this.sec.createArrayUnsafe(value); } generic_concat() { return coerceArray(this).concat.apply(this, arguments); } slice(startIndex: number, endIndex: number) { return this.sec.createArray(this.value.slice(startIndex, endIndex)); } generic_slice(startIndex: number, endIndex: number) { return coerceArray(this).slice(startIndex, endIndex); } splice(): any[] { const o = this.value; if (arguments.length === 0) { return undefined; } return this.sec.createArray(o.splice.apply(o, arguments)); } generic_splice(): any[] { return coerceArray(this).splice.apply(this, arguments); } join(sep: string) { return this.value.join(sep); } generic_join(sep: string) { return coerceArray(this).join(sep); } toString() { return this.value.join(','); } valueOf() { // same return this.value.join(','); } generic_toString() { return coerceArray(this).join(','); } indexOf(value: any, fromIndex: number) { return this.value.indexOf(value, fromIndex|0); } generic_indexOf(value: any, fromIndex: number) { return coerceArray(this).indexOf(value, fromIndex|0); } lastIndexOf(value: any, fromIndex: number) { return this.value.lastIndexOf(value, arguments.length > 1 ? fromIndex : 0x7fffffff); } generic_lastIndexOf(value: any, fromIndex: number) { return coerceArray(this).lastIndexOf(value, arguments.length > 1 ? fromIndex : 0x7fffffff); } every(callbackfn: {value: Function}, thisArg?) { if (!callbackfn || !callbackfn.value || typeof callbackfn.value !== 'function') { return true; } thisArg = ensureBoxedReceiver(this.sec, thisArg, callbackfn); const o = this.value; for (let i = 0; i < o.length; i++) { if (callbackfn.value.call(thisArg, o[i], i, this) !== true) { return false; } } return true; } generic_every(callbackfn: {value: Function}, thisArg?) { return coerceArray(this).every(callbackfn, thisArg); } some(callbackfn: {value}, thisArg?) { if (!callbackfn || !callbackfn.value || typeof callbackfn.value !== 'function') { return false; } thisArg = ensureBoxedReceiver(this.sec, thisArg, callbackfn); const self = this; return this.value.some(function (currentValue, index, array) { return callbackfn.value.call(thisArg, currentValue, index, self); }); } generic_some(callbackfn: {value}, thisArg?) { return coerceArray(this).some(callbackfn, thisArg); } forEach(callbackfn: {value}, thisArg?) { if (!callbackfn || !callbackfn.value || typeof callbackfn.value !== 'function') { return; } thisArg = ensureBoxedReceiver(this.sec, thisArg, callbackfn); const self = this; this.value.forEach(function (currentValue, index) { callbackfn.value.call(thisArg, currentValue, index, self); }); } generic_forEach(callbackfn: {value}, thisArg?) { return coerceArray(this).forEach(callbackfn, thisArg); } map(callbackfn: {value}, thisArg?) { if (!callbackfn || !callbackfn.value || typeof callbackfn.value !== 'function') { return this.sec.createArrayUnsafe([]); } thisArg = ensureBoxedReceiver(this.sec, thisArg, callbackfn); const self = this; return this.sec.createArrayUnsafe(this.value.map(function (currentValue, index) { return callbackfn.value.call(thisArg, currentValue, index, self); })); } generic_map(callbackfn: {value}, thisArg?) { return coerceArray(this).map(callbackfn, thisArg); } filter(callbackfn: {value: Function}, thisArg?) { if (!callbackfn || !callbackfn.value || typeof callbackfn.value !== 'function') { return this.sec.createArrayUnsafe([]); } thisArg = ensureBoxedReceiver(this.sec, thisArg, callbackfn); const result = []; const o = this.value; for (let i = 0; i < o.length; i++) { if (callbackfn.value.call(thisArg, o[i], i, this) === true) { result.push(o[i]); } } return this.sec.createArrayUnsafe(result); } generic_filter(callbackfn: {value: Function}, thisArg?) { return coerceArray(this).filter(callbackfn, thisArg); } toLocaleString(): string { const value = this.sec.AXArray.axCoerce(this).value; let out: string = ''; for (let i = 0, n = value.length; i < n; i++) { const val = value[i]; if (val !== null && val !== undefined) { out += val.toLocaleString(); } if (i + 1 < n) { out += ','; } } return out; } public sort(func: ASFunction | number, options?: number): this { const value = this.value; if (func == void 0) { value.sort(); return this; } let compareFunction; let context; if (this.sec.AXFunction.axIsInstanceOf(func)) { compareFunction = (<ASFunction>func).value; context = (<ASFunction>func).receiver; } else if (isNumber(func)) { options = func as number; } if (options != void 0 && !isNumber(options)) { options = void 0; } if (!options) { // Just passing compareFunction is ok because `undefined` is treated as not passed in JS. if (context) { // we must pass context for sort function because it can be bounded onto closure value.sort(compareFunction.bind(context)); return this; } value.sort(compareFunction); return this; } if (!compareFunction) { compareFunction = axDefaultCompareFunction; } const sortOrder = options & SORT.DESCENDING ? -1 : 1; value.sort(function (a, b) { return axCompare(a, b, options, sortOrder, compareFunction); }); return this; } generic_sort() { return coerceArray(this).sort.apply(this, arguments); } sortOn(names: any, options: any): any { if (arguments.length === 0) { this.sec.throwError( 'ArgumentError', Errors.WrongArgumentCountError, 'Array/http://adobe.com/AS3/2006/builtin::sortOn()', '1', '0'); } // The following oddities in how the arguments are used are gleaned from Tamarin, so hush. const o = this.value; // The options we'll end up using. let optionsList: number[] = []; if (isString(names)) { names = [Multiname.getPublicMangledName(names)]; // If the name is a string, coerce `options` to int. optionsList = [options | 0]; } else if (names && Array.isArray(names.value)) { names = names.value; for (let i = 0; i < names.length; i++) { names[i] = Multiname.getPublicMangledName(names[i]); } if (options && Array.isArray(options.value)) { options = options.value; // Use the options Array only if it's the same length as names. if (options.length === names.length) { for (let i = 0; i < options.length; i++) { optionsList[i] = options[i] | 0; } // Otherwise, use 0 for all options. } else { for (let i = 0; i < names.length; i++) { optionsList[i] = 0; } } } else { const optionsVal = options | 0; for (let i = 0; i < names.length; i++) { optionsList[i] = optionsVal; } } } else { // Not supplying either a String or an Array means nothing is sorted on. return this; } release || assert(optionsList.length === names.length); // For use with uniqueSort and returnIndexedArray once we support them. const optionsVal: number = optionsList[0]; release || Debug.assertNotImplemented(!(optionsVal & SORT.UNIQUESORT), 'UNIQUESORT'); let ret = o; if (optionsVal & SORT.RETURNINDEXEDARRAY) { const cp = o.concat(); // Make a copy of the array cp.sort((a, b) => axCompareFields(a, b, names, optionsList)); ret = cp.map((e) => o.indexOf(e)); // TODO: What happens if there are duplicate elements in the array? } else { o.sort((a, b) => axCompareFields(a, b, names, optionsList)); } return ret; } generic_sortOn() { return coerceArray(this).sortOn.apply(this, arguments); } get length(): number { return this.value.length; } set length(newLength: number) { this.value.length = newLength >>> 0; } axGetEnumerableKeys(): any [] { // Get the numeric Array keys first ... const keys = Object.keys(this.value); // ... then the keys that live on the array object. return keys.concat(super.axGetEnumerableKeys()); } axHasPropertyInternal(mn: Multiname): boolean { let name = mn.name; if (typeof name === 'number' || isNumeric(name = axCoerceName(name))) { release || assert(mn.isRuntimeName()); return name in this.value; } if (this.traits.getTrait(mn.namespaces, name)) { return true; } return '$Bg' + name in this; } axHasOwnProperty(mn: Multiname): boolean { let name = mn.name; if (typeof name === 'number' || isNumeric(name = axCoerceName(name))) { release || assert(mn.isRuntimeName()); return this.value.hasOwnProperty(name); } return !!this.traits.getTrait(mn.namespaces, name) || this.hasOwnProperty('$Bg' + name); } axGetProperty(mn: Multiname): any { let name = mn.name; if (typeof name === 'number' || isNumeric(name = axCoerceName(name))) { return this.value[name] ?? this[name]; // quick fix } return super.axGetProperty(mn); } axSetProperty(mn: Multiname, value: any, bc: Bytecode) { release || checkValue(value); let name = mn.name; if (typeof name === 'number' || isNumeric(name = axCoerceName(name))) { this.value[name] = value; return; } super.axSetProperty(mn, value, bc); } axDeleteProperty(mn: Multiname): any { let name = mn.name; if (typeof name === 'number' || isNumeric(name = axCoerceName(name))) { return delete this.value[name]; } // Cannot delete array traits. if (this.traits.getTrait(mn.namespaces, name)) { return false; } return delete this['$Bg' + name]; } axGetPublicProperty(nm: any): any { if (typeof nm === 'number' || isNumeric(nm = axCoerceName(nm))) { return this.value[nm]; } return this['$Bg' + nm]; } axSetPublicProperty(nm: string, value: any) { release || checkValue(value); if (typeof nm === 'number' || isNumeric(nm = axCoerceName(nm))) { this.value[nm] = value; return; } this['$Bg' + nm] = value; } }