@awayfl/avm1
Version:
Virtual machine for executing AS1 and AS2 code
1,703 lines (1,504 loc) • 103 kB
text/typescript
import {
avm1CompilerEnabled, avm1ErrorsEnabled, avm1TimeoutDisabled, avm1TraceEnabled,
avm1WarningsEnabled,
} from './settings';
import { AVM1ActionsData, AVM1Context, IAVM1RuntimeUtils } from './context';
import { ActionCodeBlockItem, ActionsDataAnalyzer, AnalyzerResults } from './analyze';
import {
ActionCode,
ActionsDataParser, ArgumentAssignment, ArgumentAssignmentType, ParsedAction,
ParsedPushConstantAction, ParsedPushRegisterAction
} from './parser';
import { ActionsDataCompiler } from './baseline';
import {
alCoerceString, alDefineObjectProperties, alForEachProperty, alIsArray, alIsFunction, alIsName, alNewObject, alToBoolean,
alToInt32,
alToNumber, alToObject, alToPrimitive, alToString, AVM1EvalFunction, AVM1NativeFunction,
AVM1PropertyFlags,
bToRuntimeBool
} from './runtime';
import { AVM1Globals, AVM1NativeActions } from './lib/AVM1Globals';
import { ErrorTypes, Telemetry, isNullOrUndefined, Debug, release, assert } from '@awayfl/swf-loader';
import { hasAwayJSAdaptee } from './lib/AVM1Utils';
import { AVM1MovieClip } from './lib/AVM1MovieClip';
import { AVM1ArrayNative } from './natives';
import { AVM1Object } from './runtime/AVM1Object';
import { AVM1Function } from './runtime/AVM1Function';
import { AVM1PropertyDescriptor } from './runtime/AVM1PropertyDescriptor';
import { MovieClipProperties } from './interpreter/MovieClipProperties';
import { TextField, FrameScriptManager } from '@awayjs/scene';
type AMV1ValidType = AVM1Object | number | string | null | undefined;
const noVarGetDebug: boolean = true;
declare class Error {
constructor(obj: string);
}
declare class InternalError extends Error {
constructor(obj: string);
}
export const Debugger = {
pause: false,
breakpoints: {}
};
function avm1Warn(message: string, arg1?: any, arg2?: any, arg3?: any, arg4?: any) {
if (avm1ErrorsEnabled.value) {
try {
throw new Error(message); // using throw as a way to break in browsers debugger
} catch (e) { /* ignoring since handled */
}
}
if (avm1WarningsEnabled.value) {
/* eslint-disable-next-line */
Debug.warning.apply(console, arguments);
}
}
export const MAX_AVM1_HANG_TIMEOUT = 1000;
export const CHECK_AVM1_HANG_EVERY = 1000;
const MAX_AVM1_ERRORS_LIMIT = 1000;
const MAX_AVM1_STACK_LIMIT = 256;
export enum AVM1ScopeListItemFlags {
DEFAULT = 0,
TARGET = 1,
REPLACE_TARGET = 2
}
export class AVM1ScopeListItem {
flags: AVM1ScopeListItemFlags;
replaceTargetBy: AVM1Object; // Very optional, set when REPLACE_TARGET used
constructor(public scope: AVM1Object, public previousScopeItem: AVM1ScopeListItem) {
this.flags = AVM1ScopeListItemFlags.DEFAULT;
}
}
// Similar to function scope, mostly for 'this'.
export class GlobalPropertiesScope extends AVM1Object {
constructor(context: AVM1Context, thisArg: AVM1Object) {
super(context);
this.alSetOwnProperty('this', new AVM1PropertyDescriptor(AVM1PropertyFlags.DATA |
AVM1PropertyFlags.DONT_ENUM |
AVM1PropertyFlags.DONT_DELETE |
AVM1PropertyFlags.READ_ONLY,
thisArg));
this.alSetOwnProperty('_global', new AVM1PropertyDescriptor(AVM1PropertyFlags.DATA |
AVM1PropertyFlags.DONT_ENUM |
AVM1PropertyFlags.DONT_DELETE |
AVM1PropertyFlags.READ_ONLY,
context.globals));
}
}
export class AVM1CallFrame {
public inSequence: boolean;
public calleeThis: AVM1Object;
public calleeSuper: AVM1Object; // set if super call was used
public calleeFn: AVM1Function;
public calleeArgs: any[];
constructor(public previousFrame: AVM1CallFrame,
public currentThis: AVM1Object,
public fn: AVM1Function,
public args: any[],
public ectx: ExecutionContext) {
this.inSequence = !previousFrame ? false :
(previousFrame.calleeThis === currentThis && previousFrame.calleeFn === fn);
this.resetCallee();
}
setCallee(thisArg: AVM1Object, superArg: AVM1Object, fn: AVM1Function, args: any[]) {
this.calleeThis = thisArg;
this.calleeSuper = superArg;
this.calleeFn = fn;
if (!release) {
this.calleeArgs = args;
}
}
resetCallee() {
this.calleeThis = null;
this.calleeSuper = null;
this.calleeFn = null;
}
}
class AVM1RuntimeUtilsImpl implements IAVM1RuntimeUtils {
private _context: AVM1Context;
constructor(context: AVM1Context) {
this._context = context;
}
public hasProperty(obj, name): boolean {
return as2HasProperty(this._context, obj, name);
}
public getProperty(obj, name): any {
return as2GetProperty(this._context, obj, name);
}
public setProperty(obj, name, value: any): void {
return as2SetProperty(this._context, obj, name, value);
}
public warn(msg: string): void {
/* eslint-disable-next-line */
avm1Warn.apply(null, arguments);
}
}
export class AVM1ContextImpl extends AVM1Context {
initialScope: AVM1ScopeListItem;
isActive: boolean;
executionProhibited: boolean;
abortExecutionAt: number;
actionTracer: ActionTracer;
stackDepth: number;
frame: AVM1CallFrame;
isTryCatchListening: boolean;
errorsIgnored: number;
deferScriptExecution: boolean;
actions: AVM1NativeActions;
constructor(swfVersion: number) {
super(swfVersion);
this.globals = AVM1Globals.createGlobalsObject(this);
this.actions = new AVM1NativeActions(this);
this.initialScope = new AVM1ScopeListItem(this.globals, null);
this.utils = new AVM1RuntimeUtilsImpl(this);
this.isActive = false;
this.executionProhibited = false;
this.actionTracer = avm1TraceEnabled.value ? new ActionTracer() : null;
this.abortExecutionAt = 0;
this.stackDepth = 0;
this.frame = null;
this.isTryCatchListening = false;
this.errorsIgnored = 0;
this.deferScriptExecution = true;
}
_getExecutionContext(): ExecutionContext {
// We probably entering this function from some native function,
// so faking execution context. Let's reuse last created context.
return this.frame.ectx;
}
resolveTarget(target: any): any {
const ectx = this._getExecutionContext();
return avm1ResolveTarget(ectx, target, true);
}
resolveRoot(): any {
const ectx = this._getExecutionContext();
return avm1ResolveRoot(ectx);
}
checkTimeout() {
if (Date.now() >= this.abortExecutionAt) {
//80pro - this fires even for short scripts:
//throw new AVM1CriticalError('long running script -- AVM1 instruction hang timeout');
}
}
pushCallFrame(thisArg: AVM1Object, fn: AVM1Function, args: any[], ectx: ExecutionContext): AVM1CallFrame {
const nextFrame = new AVM1CallFrame(this.frame, thisArg, fn, args, ectx);
this.frame = nextFrame;
return nextFrame;
}
popCallFrame() {
const previousFrame = this.frame.previousFrame;
this.frame = previousFrame;
return previousFrame;
}
executeActions(actionsData: AVM1ActionsData, scopeObj): void {
if (this.executionProhibited) {
return; // no more avm1 for this context
}
const savedIsActive = this.isActive;
if (!savedIsActive) {
this.isActive = true;
this.abortExecutionAt = avm1TimeoutDisabled.value ?
Number.MAX_VALUE : Date.now() + MAX_AVM1_HANG_TIMEOUT;
this.errorsIgnored = 0;
}
let caughtError;
//console.log("executeActions", scopeObj.aCount);
try {
executeActionsData(this, actionsData, scopeObj);
} catch (e) {
caughtError = e;
}
this.isActive = savedIsActive;
if (caughtError) {
// Note: this doesn't use `finally` because that's a no-go for performance.
console.error('error in framescripts', caughtError);
//throw caughtError;
}
}
public executeFunction(fn: AVM1Function, thisArg, args: any[]): any {
if (this.executionProhibited) {
return; // no more avm1 for this context
}
const savedIsActive = this.isActive;
if (!savedIsActive) {
this.isActive = true;
this.abortExecutionAt = avm1TimeoutDisabled.value ?
Number.MAX_VALUE : Date.now() + MAX_AVM1_HANG_TIMEOUT;
this.errorsIgnored = 0;
}
let caughtError;
let result;
result = fn.alCall(thisArg, args);
this.isActive = savedIsActive;
return result;
}
}
AVM1Context.create = function (swfVersion: number): AVM1Context {
return new AVM1ContextImpl(swfVersion);
};
class AVM1Error {
constructor(public error) {
}
}
class AVM1CriticalError extends Error {
constructor(message: string, public error?) {
super(message);
}
}
function isAVM1MovieClip(obj): boolean {
return typeof obj === 'object' && obj &&
obj instanceof AVM1MovieClip;
}
// function stopIfClipRemoved(ectx: ExecutionContext, clip: AVM1Object | AVM1Function) {
// if (isAVM1MovieClip(clip) && clip.isGhost) {
// ectx.isEndOfActions = true;
// }
// }
function as2GetType(v): string {
if (v === null) {
return 'null';
}
const type = typeof v;
if (typeof v === 'object') {
if (v instanceof AVM1MovieClip) {
return 'movieclip';
}
if (v instanceof AVM1Function) {
return 'function';
}
}
return type;
}
/**
* Performs "less" comparison of two arugments.
* @returns {boolean} Returns true if x is less than y, otherwise false
*/
function as2Compare(context: AVM1Context, x: any, y: any): boolean {
const x2 = alToPrimitive(context, x);
const y2 = alToPrimitive(context, y);
if (typeof x2 === 'string' && typeof y2 === 'string') {
const xs = alToString(context, x2), ys = alToString(context, y2);
return xs < ys;
} else {
const xn = alToNumber(context, x2), yn = alToNumber(context, y2);
return isNaN(xn) || isNaN(yn) ? undefined : xn < yn;
}
}
/**
* Performs equality comparison of two arugments. The equality comparison
* algorithm from EcmaScript 3, Section 11.9.3 is applied.
* @see http://ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%203rd%20edition,%20December%201999.pdf#page=67
* @returns {boolean} Coerces x and y to the same type and returns true if they're equal, false otherwise.
*/
function as2Equals(context: AVM1Context, x: any, y: any): boolean {
// Spec steps 1 through 13 can be condensed to ...
if (typeof x === typeof y) {
if (typeof x === 'number') {
// Calculate the difference.
const ma = Math.abs(x), mb = Math.abs(y);
const larges = ma > mb ? ma : mb;
const eps = (1e-6) * larges;
return Math.abs(x - y) <= eps;
}
return x === y;
}
// Spec steps 14 and 15.
if (x == null && y == null) {
return true;
}
/*
if (typeof x === 'undefined' && typeof y === 'string' && y=="") {
// Unfolding the recursion for `as2Equals(context, x, alToNumber(y))`
return true; // in AVM1, ToNumber('') === NaN
}
if (typeof y === 'undefined' && typeof x === 'string' && x=="") {
// Unfolding the recursion for `as2Equals(context, x, alToNumber(y))`
return true; // in AVM1, ToNumber('') === NaN
}
*/
// Spec steps 16 and 17.
if (typeof x === 'number' && typeof y === 'string') {
// Unfolding the recursion for `as2Equals(context, x, alToNumber(y))`
return y === '' ? false : x === +y; // in AVM1, ToNumber('') === NaN
}
if (typeof x === 'string' && typeof y === 'number') {
// Unfolding the recursion for `as2Equals(context, alToNumber(x), y)`
return x === '' ? false : +x === y; // in AVM1, ToNumber('') === NaN
}
// Spec step 18.
if (typeof x === 'boolean') {
// Unfolding the recursion for `as2Equals(context, alToNumber(x), y)`
x = +x; // typeof x === 'number'
if (typeof y === 'number' || typeof y === 'string') {
return y === '' ? false : x === +y;
}
// Fall through for typeof y === 'object', 'boolean', 'undefined' cases
}
// Spec step 19.
if (typeof y === 'boolean') {
// Unfolding the recursion for `as2Equals(context, x, alToNumber(y))`
y = +y; // typeof y === 'number'
if (typeof x === 'number' || typeof x === 'string') {
return x === '' ? false : +x === y;
}
// Fall through for typeof x === 'object', 'undefined' cases
}
// Spec step 20.
if ((typeof x === 'number' || typeof x === 'string') &&
typeof y === 'object' && y !== null) {
y = alToPrimitive(context, y);
if (typeof y === 'object') {
return false; // avoiding infinite recursion
}
return as2Equals(context, x, y);
}
// Spec step 21.
if (typeof x === 'object' && x !== null &&
(typeof y === 'number' || typeof y === 'string')) {
x = alToPrimitive(context, x);
if (typeof x === 'object') {
return false; // avoiding infinite recursion
}
return as2Equals(context, x, y);
}
return false;
}
function as2InstanceOf(obj, constructor): boolean {
// TODO refactor this -- quick and dirty hack for now
if (isNullOrUndefined(obj) || isNullOrUndefined(constructor)) {
return false;
}
// if (constructor === ASString) {
// return typeof obj === 'string';
// } else if (constructor === ASNumber) {
// return typeof obj === 'number';
// } else if (constructor === ASBoolean) {
// return typeof obj === 'boolean';
// } else if (constructor === ASArray) {
// return Array.isArray(obj);
// } else if (constructor === ASFunction) {
// return typeof obj === 'function';
// } else if (constructor === ASObject) {
// return typeof obj === 'object';
// }
const baseProto = constructor.alGetPrototypeProperty();
if (!baseProto) {
return false;
}
let proto = obj;
while (proto) {
if (proto === baseProto) {
return true; // found the type if the chain
}
proto = proto.alPrototype;
}
// TODO interface check
return false;
}
function as2HasProperty(context: AVM1Context, obj: any, name: any): boolean {
const avm1Obj: AVM1Object = alToObject(context, obj);
name = context.normalizeName(name);
return avm1Obj.alHasProperty(name);
}
function as2GetProperty(context: AVM1Context, obj: any, name: any): any {
const avm1Obj: AVM1Object = alToObject(context, obj);
if (!avm1Obj)
return undefined;
const value = avm1Obj.alGet(name);
//if(typeof name==="string" && name.toLowerCase()=="ox"){
// console.log("get ox", avm1Obj.adaptee.id, avm1Obj.adaptee.name, value);
//}
return value;
}
function as2SetProperty(context: AVM1Context, obj: any, name: any, value: any): void {
const avm1Obj: AVM1Object = alToObject(context, obj);
if (!avm1Obj)
return;
//if(typeof name==="string" && name.toLowerCase()=="ox"){
// console.log("set ox", avm1Obj.adaptee.id, avm1Obj.adaptee.name, value);
//}
if (name == '__proto__') {
if (value) {
const allKeys: string[] = value.alGetKeys();
for (let i = 0; i < allKeys.length; i++) {
const key = allKeys[i];
if (key != '') {
avm1Obj.alPut(key, value.alGet(key));
as2SyncEvents(context, key, avm1Obj);
}
}
avm1Obj.protoTypeChanged = !(value instanceof AVM1MovieClip);
}
} else {
avm1Obj.alPut(name, value);
if (avm1Obj.adaptee) {
// todo: this might not be the best way
// the goal is to not call as2SyncEvents when avm1Obj is a prototype object
// but idk how to identify if avm1Obj is prototype.
// for now i just use the adaptee to check, because a prototype should not have adaptee set
as2SyncEvents(context, name, avm1Obj);
}
}
}
function as2DeleteProperty(context: AVM1Context, obj: any, name: any): any {
const avm1Obj: AVM1Object = alToObject(context, obj);
name = context.normalizeName(name);
const result = avm1Obj.alDeleteProperty(name);
as2SyncEvents(context, name, avm1Obj);
return result;
}
function as2SyncEvents(context: AVM1Context, name, avm1Obj): void {
if (typeof name === 'undefined')
return;
name = alCoerceString(context, name);
name = context.normalizeName(name);
if (name[0] !== 'o' || name[1] !== 'n') { // TODO check case?
return;
}
if (avm1Obj && avm1Obj.updateEventByPropName)
avm1Obj.updateEventByPropName(name);
// Maybe an event property, trying to broadcast change.
//(<AVM1ContextImpl>context).broadcastEventPropertyChange(name);
}
function as2CastError(ex) {
if (typeof InternalError !== 'undefined' &&
ex instanceof InternalError && (<any>ex).message === 'too much recursion') {
// HACK converting too much recursion into AVM1CriticalError
//console.log('long running script -- AVM1 recursion limit is reached');
return new AVM1CriticalError('long running script -- AVM1 recursion limit is reached');
}
return ex;
}
function as2Construct(ctor, args) {
let result;
if (alIsFunction(ctor)) {
result = (<AVM1Function>ctor).alConstruct(args);
} else {
// AVM1 simply ignores attempts to invoke non-methods.
return undefined;
}
return result;
}
function as2Enumerate(obj, fn: (name) => void, thisArg): void {
// todo: better just whitelist "typeof === object" instead of blacklisting ?
if (typeof obj === 'boolean' || typeof obj === 'string' || typeof obj === 'number') {
return;
}
alForEachProperty(obj, function (name) {
if (typeof name == 'string' && name.indexOf('_internal_TF') != -1)
return;
const avmObj = obj.alGet(name);
if (avmObj?.adaptee?.isAsset(TextField) && avmObj.adaptee.isStatic)
return;
fn.call(thisArg, name);
}, thisArg);
/*
let i = props.length;
let avmObj = null;
while (i > 0) {
i--;
fn.call(thisArg, props[i]);
}*/
}
function avm1FindSuperPropertyOwner(context: AVM1Context, frame: AVM1CallFrame, propertyName: string): AVM1Object {
if (context.swfVersion < 6) {
return null;
}
let proto: AVM1Object = (frame.inSequence && frame.previousFrame.calleeSuper);
if (!proto) {
// Finding first object in prototype chain link that has the property.
proto = frame.currentThis;
while (proto && !proto.alHasOwnProperty(propertyName)) {
proto = proto.alPrototype;
}
if (!proto) {
return null;
}
}
// Skipping one chain link
proto = proto.alPrototype;
return proto;
}
const DEFAULT_REGISTER_COUNT = 4;
function executeActionsData(context: AVM1ContextImpl, actionsData: AVM1ActionsData, scope) {
const actionTracer = context.actionTracer;
const globalPropertiesScopeList = new AVM1ScopeListItem(
new GlobalPropertiesScope(context, scope), context.initialScope);
const scopeList = new AVM1ScopeListItem(scope, globalPropertiesScopeList);
scopeList.flags |= AVM1ScopeListItemFlags.TARGET;
let caughtError;
release || (actionTracer && actionTracer.message('ActionScript Execution Starts'));
release || (actionTracer && actionTracer.indent());
const ectx = ExecutionContext.create(context, scopeList, [], DEFAULT_REGISTER_COUNT);
context.pushCallFrame(scope, null, null, ectx);
try {
interpretActionsData(ectx, actionsData);
} catch (e) {
caughtError = as2CastError(e);
}
ectx.dispose();
if (caughtError instanceof AVM1CriticalError) {
context.executionProhibited = true;
console.error('Disabling AVM1 execution');
}
context.popCallFrame();
release || (actionTracer && actionTracer.unindent());
release || (actionTracer && actionTracer.message('ActionScript Execution Stops'));
if (caughtError) {
// Note: this doesn't use `finally` because that's a no-go for performance.
throw caughtError; // TODO shall we just ignore it?
}
}
function createBuiltinType(context: AVM1Context, cls, args: any[]): any {
const builtins = context.builtins;
let obj = undefined;
if (cls === builtins.Array || cls === builtins.Object ||
cls === builtins.Date || cls === builtins.String ||
cls === builtins.Function) {
obj = cls.alConstruct(args);
}
if (cls === builtins.Boolean || cls === builtins.Number) {
obj = cls.alConstruct(args).value;
}
if (obj instanceof AVM1Object) {
const desc = new AVM1PropertyDescriptor(AVM1PropertyFlags.DATA | AVM1PropertyFlags.DONT_ENUM,
cls);
(<AVM1Object>obj).alSetOwnProperty('__constructor__', desc);
}
return obj;
}
class AVM1SuperWrapper extends AVM1Object {
public callFrame: AVM1CallFrame;
public constructor(context: AVM1Context, callFrame: AVM1CallFrame) {
super(context);
this.callFrame = callFrame;
this.alPrototype = context.builtins.Object.alGetPrototypeProperty();
}
}
class AVM1Arguments extends AVM1ArrayNative {
public constructor(context: AVM1Context, args: any[], callee: AVM1Function, caller: AVM1Function) {
super(context, args);
alDefineObjectProperties(this, {
callee: {
value: callee
},
caller: {
value: caller
}
});
}
}
export class ExecutionContext {
static MAX_CACHED_EXECUTIONCONTEXTS = 20;
static cache: ExecutionContext[];
static alInitStatic() {
this.cache = [];
}
framescriptmanager: FrameScriptManager;
context: AVM1ContextImpl;
actions: AVM1NativeActions;
scopeList: AVM1ScopeListItem;
constantPool: any[];
registers: any[];
stack: any[];
frame: AVM1CallFrame;
isSwfVersion5: boolean;
isSwfVersion7: boolean;
recoveringFromError: boolean;
isEndOfActions: boolean;
constructor(context: AVM1ContextImpl, scopeList: AVM1ScopeListItem, constantPool: any[], registerCount: number) {
this.framescriptmanager = FrameScriptManager;
this.context = context;
this.actions = context.actions;
this.isSwfVersion5 = context.swfVersion >= 5;
this.isSwfVersion7 = context.swfVersion >= 7;
this.registers = [];
this.stack = [];
this.frame = null;
this.recoveringFromError = false;
this.isEndOfActions = false;
this.reset(scopeList, constantPool, registerCount);
}
reset(scopeList: AVM1ScopeListItem, constantPool: any[], registerCount: number) {
this.scopeList = scopeList;
this.constantPool = constantPool;
this.registers.length = registerCount;
}
clean(): void {
this.scopeList = null;
this.constantPool = null;
this.registers.length = 0;
this.stack.length = 0;
this.frame = null;
this.recoveringFromError = false;
this.isEndOfActions = false;
}
pushScope(newScopeList?: AVM1ScopeListItem): ExecutionContext {
const newContext = <ExecutionContext>Object.create(this);
newContext.stack = [];
if (!isNullOrUndefined(newScopeList)) {
newContext.scopeList = newScopeList;
}
return newContext;
}
dispose() {
this.clean();
const state: typeof ExecutionContext = this.context.getStaticState(ExecutionContext);
if (state.cache.length < ExecutionContext.MAX_CACHED_EXECUTIONCONTEXTS) {
state.cache.push(this);
}
}
/* eslint-disable-next-line */
static create(context: AVM1ContextImpl, scopeList: AVM1ScopeListItem, constantPool: any[], registerCount: number): ExecutionContext {
const state: typeof ExecutionContext = context.getStaticState(ExecutionContext);
let ectx: ExecutionContext;
if (state.cache.length > 0) {
ectx = state.cache.pop();
ectx.reset(scopeList, constantPool, registerCount);
} else {
ectx = new ExecutionContext(context, scopeList, constantPool, registerCount);
}
return ectx;
}
}
/**
* Interpreted function closure.
*/
class AVM1InterpreterScope extends AVM1Object {
constructor(context: AVM1ContextImpl) {
super(context);
this.alPut('toString', new AVM1NativeFunction(context, this._toString));
}
_toString() {
// It shall return 'this'
return this;
}
}
export class AVM1InterpretedFunction extends AVM1EvalFunction {
functionName: string;
actionsData: AVM1ActionsData;
parametersNames: string[];
registersAllocation: ArgumentAssignment[];
suppressArguments: ArgumentAssignmentType;
scopeList: AVM1ScopeListItem;
constantPool: any[];
skipArguments: boolean[];
registersLength: number;
constructor(context: AVM1ContextImpl,
ectx: ExecutionContext,
actionsData: AVM1ActionsData,
functionName: string,
parametersNames: string[],
registersCount: number,
registersAllocation: ArgumentAssignment[],
suppressArguments: ArgumentAssignmentType) {
super(context);
this.functionName = functionName;
this.actionsData = actionsData;
this.parametersNames = parametersNames;
this.registersAllocation = registersAllocation;
this.suppressArguments = suppressArguments;
this.scopeList = ectx.scopeList;
this.constantPool = ectx.constantPool;
let skipArguments: boolean[] = null;
const registersAllocationCount = !registersAllocation ? 0 : registersAllocation.length;
for (let i = 0; i < registersAllocationCount; i++) {
const registerAllocation = registersAllocation[i];
if (registerAllocation &&
registerAllocation.type === ArgumentAssignmentType.Argument) {
if (!skipArguments) {
skipArguments = [];
}
skipArguments[registersAllocation[i].index] = true;
}
}
this.skipArguments = skipArguments;
let registersLength = Math.min(registersCount, 255); // max allowed for DefineFunction2
registersLength = Math.max(registersLength, registersAllocationCount + 1);
this.registersLength = registersLength;
}
public alCall(thisArg: any, args?: any[]): any {
const currentContext = <AVM1ContextImpl> this.context;
if (currentContext.executionProhibited) {
return; // no more avm1 execution, ever
}
const newScope = new AVM1InterpreterScope(currentContext);
const newScopeList = new AVM1ScopeListItem(newScope, this.scopeList);
const oldScope = this.scopeList.scope;
//thisArg = thisArg || oldScope; // REDUX no isGlobalObject check?
args = args || [];
const ectx = ExecutionContext.create(currentContext, newScopeList, this.constantPool, this.registersLength);
const caller = currentContext.frame ? currentContext.frame.fn : undefined;
const frame = currentContext.pushCallFrame(thisArg, this, args, ectx);
let supperWrapper;
const suppressArguments = this.suppressArguments;
if (!(suppressArguments & ArgumentAssignmentType.Arguments)) {
newScope.alPut('arguments', new AVM1Arguments(currentContext, args, this, caller));
}
if (!(suppressArguments & ArgumentAssignmentType.This)) {
newScope.alPut('this', thisArg);
}
if (!(suppressArguments & ArgumentAssignmentType.Super)) {
supperWrapper = new AVM1SuperWrapper(currentContext, frame);
newScope.alPut('super', supperWrapper);
}
let i;
const registers = ectx.registers;
const registersAllocation = this.registersAllocation;
const registersAllocationCount = !registersAllocation ? 0 : registersAllocation.length;
for (i = 0; i < registersAllocationCount; i++) {
const registerAllocation = registersAllocation[i];
if (registerAllocation) {
switch (registerAllocation.type) {
case ArgumentAssignmentType.Argument:
registers[i] = args[registerAllocation.index];
break;
case ArgumentAssignmentType.This:
registers[i] = thisArg;
break;
case ArgumentAssignmentType.Arguments:
registers[i] = new AVM1Arguments(currentContext, args, this, caller);
break;
case ArgumentAssignmentType.Super:
supperWrapper = supperWrapper || new AVM1SuperWrapper(currentContext, frame);
registers[i] = supperWrapper;
break;
case ArgumentAssignmentType.Global:
registers[i] = currentContext.globals;
break;
case ArgumentAssignmentType.Parent: {
let parentObj = null;
if (oldScope) {
parentObj = oldScope.alGet('_parent');
if (!parentObj) {
parentObj = oldScope.alGet('this');
if (parentObj) {
parentObj = parentObj.alGet('_parent');
}
}
}
if (!parentObj) {
// if the _parent was not set from oldScope, we get it from thisArg
parentObj = thisArg;
if (parentObj) {
parentObj = parentObj.alGet('_parent');
}
// if this is a onEnter, and the _parent was not set from oldScope,
// we need to go up another parent if possible
if (parentObj && this.isOnEnter && parentObj.alGet('_parent')) {
parentObj = parentObj.alGet('_parent');
}
// for setInterval: if its still not has a parent found
// we look back at the previous-scopes until we find a scope that can provide a _parent
if (!parentObj) {
if (this.scopeList?.previousScopeItem?.scope) {
let currentScope = this.scopeList.previousScopeItem;
while (currentScope) {
if (currentScope.scope && currentScope.scope instanceof AVM1MovieClip) {
parentObj = currentScope.scope;
} else if (currentScope.scope) {
parentObj = currentScope.scope.alGet('this');
}
if (parentObj) {
parentObj = parentObj.alGet('_parent');
}
if (currentScope.previousScopeItem)
currentScope = currentScope.previousScopeItem;
else currentScope = null;
}
}
}
}
/*if(this.isOnEnter){
console.log("prepare on enter");
console.log("oldScope parent", oldScope.alGet("_parent"));
console.log("oldScope this", oldScope.alGet("this"));
console.log("newscope this", newScope.alGet("_parent"));
console.log("thisArg", thisArg);
}*/
if (parentObj) {
registers[i] = parentObj;
} else {
//console.log("_parent not defined");
}
break;
}
case ArgumentAssignmentType.Root:
registers[i] = avm1ResolveRoot(ectx);
break;
}
}
}
const parametersNames = this.parametersNames;
const skipArguments = this.skipArguments;
for (i = 0; i < args.length || i < parametersNames.length; i++) {
if (skipArguments && skipArguments[i]) {
continue;
}
newScope.alPut(parametersNames[i], args[i]);
}
let result;
let caughtError;
const actionTracer = currentContext.actionTracer;
const actionsData = this.actionsData;
release || (actionTracer && actionTracer.indent());
if (++currentContext.stackDepth >= MAX_AVM1_STACK_LIMIT) {
throw new AVM1CriticalError('long running script -- AVM1 recursion limit is reached');
}
result = interpretActionsData(ectx, actionsData);
currentContext.stackDepth--;
currentContext.popCallFrame();
ectx.dispose();
release || (actionTracer && actionTracer.unindent());
return result;
}
}
function fixArgsCount(numArgs: number /* int */, maxAmount: number): number {
if (isNaN(numArgs) || numArgs < 0) {
avm1Warn('Invalid amount of arguments: ' + numArgs);
return 0;
}
numArgs |= 0;
if (numArgs > maxAmount) {
avm1Warn('Truncating amount of arguments: from ' + numArgs + ' to ' + maxAmount);
return maxAmount;
}
return numArgs;
}
function avm1ReadFunctionArgs(stack: any[]) {
let numArgs = +stack.pop();
numArgs = fixArgsCount(numArgs, stack.length);
const args = [];
for (let i = 0; i < numArgs; i++) {
args.push(stack.pop());
}
return args;
}
function avm1SetTarget(ectx: ExecutionContext, targetPath: string) {
let newTarget = null;
if (targetPath) {
if (typeof targetPath === 'string') {
while (targetPath.length && targetPath[targetPath.length - 1] == '.') {
targetPath = targetPath.substring(0, targetPath.length - 1);
}
}
try {
newTarget = avm1ResolveTarget(ectx, targetPath, false);
if (!avm1IsTarget(newTarget)) {
avm1Warn('Invalid AVM1 target object: ' + targetPath);
newTarget = undefined;
}
} catch (e) {
avm1Warn('Unable to set target: ' + e);
}
}
if (newTarget) {
ectx.scopeList.flags |= AVM1ScopeListItemFlags.REPLACE_TARGET;
ectx.scopeList.replaceTargetBy = newTarget;
} else {
ectx.scopeList.flags &= ~AVM1ScopeListItemFlags.REPLACE_TARGET;
ectx.scopeList.replaceTargetBy = null;
}
}
function avm1DefineFunction(ectx: ExecutionContext,
actionsData: AVM1ActionsData,
functionName: string,
parametersNames: string[],
registersCount: number,
registersAllocation: ArgumentAssignment[],
suppressArguments: ArgumentAssignmentType): AVM1Function {
return new AVM1InterpretedFunction(ectx.context, ectx, actionsData, functionName,
parametersNames, registersCount, registersAllocation, suppressArguments);
}
function avm1VariableNameHasPath(variableName: string): boolean {
return variableName && (
variableName.indexOf('.') >= 0
|| variableName.indexOf(':') >= 0
|| variableName.indexOf('/') >= 0);
}
const enum AVM1ResolveVariableFlags {
READ = 1,
WRITE = 2,
DELETE = READ,
GET_VALUE = 32,
DISALLOW_TARGET_OVERRIDE = 64,
ONLY_TARGETS = 128
}
interface IAVM1ResolvedVariableResult {
scope: AVM1Object;
propertyName: string;
value: any;
}
const cachedResolvedVariableResult: IAVM1ResolvedVariableResult = {
scope: null,
propertyName: null,
value: undefined
};
function avm1IsTarget(target): boolean {
// TODO refactor
return target instanceof AVM1Object && hasAwayJSAdaptee(target);
}
/* eslint-disable-next-line */
function avm1ResolveSimpleVariable(scopeList: AVM1ScopeListItem, variableName: string, flags: AVM1ResolveVariableFlags, additionalName: string = null): IAVM1ResolvedVariableResult {
release || Debug.assert(alIsName(scopeList.scope.context, variableName));
let currentTarget;
const resolved = cachedResolvedVariableResult;
for (let p = scopeList; p; p = p.previousScopeItem) {
if ((p.flags & AVM1ScopeListItemFlags.REPLACE_TARGET) &&
!(flags & AVM1ResolveVariableFlags.DISALLOW_TARGET_OVERRIDE) &&
!currentTarget) {
currentTarget = p.replaceTargetBy;
}
if ((p.flags & AVM1ScopeListItemFlags.TARGET)) {
if ((flags & AVM1ResolveVariableFlags.WRITE)) {
// last scope/target we can modify (exclude globals)
resolved.scope = currentTarget || p.scope;
resolved.propertyName = variableName;
resolved.value = (flags & AVM1ResolveVariableFlags.GET_VALUE)
? resolved.scope.alGet(variableName)
: undefined;
return resolved;
}
if ((flags & AVM1ResolveVariableFlags.READ) && currentTarget) {
if (currentTarget.alHasProperty(variableName)) {
resolved.scope = currentTarget;
resolved.propertyName = variableName;
resolved.value = (flags & AVM1ResolveVariableFlags.GET_VALUE)
? currentTarget.alGet(variableName)
: undefined;
return resolved;
}
continue;
}
}
//console.log("scope :", p.scope.aCount);
if (p.scope.alHasProperty(variableName)) {
const value = p.scope.alGet(variableName);
if (additionalName && (
!value
|| typeof value !== 'object'
|| !value.alHasProperty
|| !value.alHasProperty(additionalName))
) {
continue;
}
resolved.scope = p.scope;
resolved.propertyName = variableName;
resolved.value = (flags & AVM1ResolveVariableFlags.GET_VALUE) ? p.scope.alGet(variableName) : undefined;
return resolved;
}
//80pro: in some cases we are trying to find a mc by name, but it is only registered as "this" within the scope
// in this cases, we check if the "this" object actually has the name that we are searching for
/* if(p.scope.alHasProperty("this")) {
var thisValue = (flags & AVM1ResolveVariableFlags.GET_VALUE) ? p.scope.alGet("this") : undefined;
if(thisValue && thisValue.adaptee && thisValue.adaptee.name && thisValue.adaptee.name==variableName){
resolved.scope = p.scope;
resolved.propertyName = variableName;
resolved.value = thisValue;
return resolved;
}
}*/
}
noVarGetDebug || console.log('avm1ResolveSimpleVariable variableName', variableName);
release || Debug.assert(!(flags & AVM1ResolveVariableFlags.WRITE));
return undefined;
}
/* eslint-disable-next-line */
function avm1ResolveVariable(ectx: ExecutionContext, variableName: string, flags: AVM1ResolveVariableFlags): IAVM1ResolvedVariableResult {
// For now it is just very much magical -- designed to pass some of the swfdec tests
// FIXME refactor
release || Debug.assert(variableName);
const len = variableName.length;
let i = 0;
let markedAsTarget = true;
let resolved, ch, needsScopeResolution;
let propertyName = null;
let scope = null;
let obj = undefined;
// Canonicalizing the name here is ok even for paths: the only thing that (potentially)
// happens is that the name is converted to lower-case, which is always valid for paths.
// The original name is saved because the final property name needs to be extracted from
// it for property name paths.
const originalName = variableName;
if (!avm1VariableNameHasPath(variableName)) {
variableName = ectx.context.normalizeName(variableName);
if (typeof variableName === 'string' && variableName.startsWith('_level')) {
resolved = cachedResolvedVariableResult;
resolved.scope = scope;
resolved.propertyName = variableName;
resolved.value = ectx.context.resolveLevel(+variableName[6]);
return resolved;
}
//noVarGetDebug || console.log("simple variableName", variableName);
const resolvedVar = avm1ResolveSimpleVariable(ectx.scopeList, variableName, flags);
noVarGetDebug || console.log('resolved', resolvedVar);
return resolvedVar;
}
noVarGetDebug || console.log('originalName', originalName);
// if this is a path, and the last item is a "." flash will not find anything
if (variableName[variableName.length - 1] == '.') {
return null;
}
if (variableName[0] === '/') {
noVarGetDebug || console.log('originalName starts with a \'/\'');
resolved = avm1ResolveSimpleVariable(
ectx.scopeList, '_root', AVM1ResolveVariableFlags.READ | AVM1ResolveVariableFlags.GET_VALUE);
if (resolved) {
noVarGetDebug || console.log('resolved', resolved);
propertyName = resolved.propertyName;
scope = resolved.scope;
obj = resolved.value;
}
i++;
needsScopeResolution = false;
} else {
resolved = null;
needsScopeResolution = true;
}
noVarGetDebug || console.log('needsScopeResolution', needsScopeResolution);
if (i >= len) {
return resolved;
}
let q = i;
while (i < len) {
if (!needsScopeResolution && !(obj instanceof AVM1Object)) {
/* eslint-disable-next-line */
noVarGetDebug || console.log('Unable to resolve variable on invalid object ' + variableName.substring(q, i - 1) + ' (expr ' + variableName + ')');
/* eslint-disable-next-line */
avm1Warn('Unable to resolve variable on invalid object ' + variableName.substring(q, i - 1) + ' (expr ' + variableName + ')');
return null;
}
q = i;
if (variableName[i] === '.' && variableName[i + 1] === '.') {
i += 2;
propertyName = '_parent';
} else {
while (i < len && ((ch = variableName[i]) !== '/' && ch !== '.' && ch !== ':')) {
i++;
}
propertyName = variableName.substring(q, i);
}
if (propertyName === '' && i < len) {
// Ignoring double delimiters in the middle of the path
i++;
continue;
}
scope = obj;
let valueFound = false;
if (markedAsTarget) {
// Trying movie clip children first
const child = obj instanceof AVM1MovieClip ? (<AVM1MovieClip>obj)._lookupChildByName(propertyName) : void 0;
if (child) {
valueFound = true;
obj = child;
}
}
if (!valueFound) {
if (needsScopeResolution) {
// 80pro:
// if we need to resolve the scope, we want to know the next property name
// if a next property name exists, we pass it as extra argument to avm1ResolveSimpleVariable
// this will make sure that avm1ResolveSimpleVariable
// returns the scope that has the property name available
q = i + 1;
let k = i + 1;
let nextPropName = '';
if (variableName[k] === '.' && variableName[k + 1] === '.') {
k += 2;
nextPropName = '_parent';
} else {
while (k < len && ((ch = variableName[k]) !== '/' && ch !== '.' && ch !== ':')) {
k++;
}
nextPropName = variableName.substring(q, k);
}
if (nextPropName == '')
nextPropName = null;
resolved = avm1ResolveSimpleVariable(ectx.scopeList, propertyName, flags, nextPropName);
if (!resolved && nextPropName) {
// if we tried to get with a nextPropName,
// and got nothing returned, we try again without any nextpropName
resolved = avm1ResolveSimpleVariable(ectx.scopeList, propertyName, flags);
}
if (resolved) {
valueFound = true;
propertyName = resolved.propertyName;
scope = resolved.scope;
obj = resolved.value;
if (i < len && !obj && scope) {
obj = scope;
}
}
needsScopeResolution = false;
} else if (obj.alHasProperty(propertyName)) {
obj = obj.alGet(propertyName);
valueFound = true;
}
}
if (!valueFound && propertyName[0] === '_') {
// FIXME hacking to pass some swfdec test cases
if (propertyName.startsWith('_level')) {
obj = ectx.context.resolveLevel(+propertyName[6]);
valueFound = true;
} else if (propertyName === '_root') {
obj = avm1ResolveRoot(ectx);
valueFound = true;
}
}
if (!valueFound && !(flags & AVM1ResolveVariableFlags.WRITE)) {
/* eslint-disable-next-line */
avm1Warn('Unable to resolve ' + propertyName + ' on ' + variableName.substring(q, i - 1) + ' (expr ' + variableName + ')');
return null;
}
if (i >= len) {
break;
}
let delimiter = variableName[i++];
if (delimiter === '/' && ((ch = variableName[i]) === ':' || ch === '.')) {
delimiter = variableName[i++];
}
markedAsTarget = delimiter === '/';
}
resolved = cachedResolvedVariableResult;
resolved.scope = scope;
resolved.propertyName = originalName.substring(q, i);
resolved.value = (flags & AVM1ResolveVariableFlags.GET_VALUE) ? obj : undefined;
return resolved;
}
function avm1GetTarget(ectx: ExecutionContext, allowOverride: boolean): AVM1Object {
const scopeList = ectx.scopeList;
for (let p = scopeList; p.previousScopeItem; p = p.previousScopeItem) {
if ((p.flags & AVM1ScopeListItemFlags.REPLACE_TARGET) && allowOverride) {
return p.replaceTargetBy;
}
if ((p.flags & AVM1ScopeListItemFlags.TARGET)) {
return p.scope;
}
}
release || Debug.assert(false, 'Shall not reach this statement');
return undefined;
}
function avm1ResolveTarget(ectx: ExecutionContext, target: any, fromCurrentTarget: boolean): AVM1Object {
let result: AVM1Object;
if (avm1IsTarget(target)) {
result = target;
} else {
target = isNullOrUndefined(target) ? '' : alToString(this, target);
if (target) {
const targetPath = alToString(ectx.context, target);
const resolved = avm1ResolveVariable(ectx, targetPath,
AVM1ResolveVariableFlags.READ |
AVM1ResolveVariableFlags.ONLY_TARGETS |
AVM1ResolveVariableFlags.GET_VALUE |
(fromCurrentTarget ? 0 : AVM1ResolveVariableFlags.DISALLOW_TARGET_OVERRIDE));
if (!resolved || !avm1IsTarget(resolved.value)) {
avm1Warn('Invalid AVM1 target object: ' + targetPath);
result = undefined;
} else {
result = resolved.value;
}
} else {
result = avm1GetTarget(ectx, true);
}
}
return result;
}
function avm1ResolveRoot(ectx: ExecutionContext): AVM1Object {
const target = avm1GetTarget(ectx, true);
return (<AVM1MovieClip>target).get_root();
}
function avm1ProcessWith(ectx: ExecutionContext, obj, withBlock) {
if (isNullOrUndefined(obj)) {
// Not executing anything in the block.
avm1Warn('The with statement object cannot be undefined.');
return;
}
const context = ectx.context;
const scopeList = ectx.scopeList;
const newScopeList = new AVM1ScopeListItem(alToObject(context, obj), scopeList);
const newEctx = ectx.pushScope(newScopeList);
interpretActionsData(newEctx, withBlock);
}
function avm1ProcessTry(ectx: ExecutionContext,
catchIsRegisterFlag, finallyBlockFlag,
catchBlockFlag, catchTarget,
tryBlock, catchBlock, finallyBlock) {
const currentContext = ectx.context;
const scopeList = ectx.scopeList;
const registers = ectx.registers;
const savedTryCatchState = currentContext.isTryCatchListening;
let caughtError;
try {
currentContext.isTryCatchListening = true;
interpretActionsData(ectx.pushScope(), tryBlock);
} catch (e) {
currentContext.isTryCatchListening = savedTryCatchState;
if (!catchBlockFlag || !(e instanceof AVM1Error)) {
caughtError = e;
} else {
if (typeof catchTarget === 'string') { // TODO catchIsRegisterFlag?
const scope = scopeList.scope;
scope.alPut(catchTarget, e.error);
} else {
registers[catchTarget] = e.error;
}
interpretActionsData(ectx.pushScope(), catchBlock);
}
}
currentContext.isTryCatchListening = savedTryCatchState;
if (finallyBlockFlag) {
interpretActionsData(ectx.pushScope(), finallyBlock);
}
if (caughtError) {
throw caughtError;
}
}
// SWF 3 actions
function avm1_0x81_ActionGotoFrame(ectx: ExecutionContext, args: any[]) {
const frame: number = args[0];
const play: boolean = args[1];
if (play) {
ectx.actions.gotoAndPlay(frame + 1);
} else {
ectx.actions.gotoAndStop(frame + 1);
}
}
function avm1_0x83_ActionGetURL(ectx: ExecutionContext, args: any[]) {
// const actions = ectx.actions;
const urlString: string = args[0];
const targetString: string = args[1];
ectx.actions.getURL(urlString, targetString);
}
function avm1_0x04_ActionNextFrame(ectx: ExecutionContext) {
ectx.actions.nextFrame();
}
function avm1_0x05_ActionPreviousFrame(ectx: ExecutionContext) {
ectx.actions.prevFrame();
}
function avm1_0x06_ActionPlay(ectx: ExecutionContext) {
ectx.actions.play();
}
function avm1_0x07_ActionStop(ectx: ExecutionContext) {
ectx.actions.stop();
}
function avm1_0x08_ActionToggleQuality(ectx: ExecutionContext) {
ectx.actions.toggleHighQuality();
}
function avm1_0x09_ActionStopSounds(ectx: ExecutionContext) {
ectx.actions.stopAllSounds();
}
function avm1_0x8A_ActionWaitForFrame(ectx: ExecutionContext, args: any[]) {
const frame: number = args[0];
// const count: number = args[1];
return !ectx.actions.ifFrameLoaded(frame);
}
function avm1_0x8B_ActionSetTarget(ectx: ExecutionContext, args: any[]) {
const targetName: string = args[0];
avm1SetTarget(ectx, targetName);
}
function avm1_0x8C_ActionGoToLabel(ectx: ExecutionContext, args: any[]) {
const label: string = args[0];
const play: boolean = args[1];
if (play) {
ectx.actions.gotoAndPlay(label);
} else {
ectx.actions.gotoAndStop(label);
}
}
// SWF 4 actions
function avm1_0x96_ActionPush(ectx: ExecutionContext, args: any[]) {
const registers = ectx.registers;
const constantPool = ectx.constantPool;
const stack = ectx.stack;
args.forEach(function (value) {
if (value instanceof ParsedPushConstantAction) {
stack.push(constantPool[(<ParsedPushConstantAction> value).constantIndex]);
} else if (value instanceof ParsedPushRegisterAction) {
const registerNumber = (<ParsedPushRegisterAction> value).registerNumber;
if (registerNumber < 0 || registerNumber >= registers.length) {
stack.push(undefined);
} else {
stack.push(registers[registerNumber]);
}
} else {
stack.push(value);
}
});
}
function avm1_0x17_ActionPop(ectx: ExecutionContext) {
const stack = ectx.stack;
stack.pop();
}
function avm1_0x0A_ActionAdd(ectx: ExecutionContext) {
const stack = ectx.stack;
let a = alToNumber(ectx.context, stack.pop());
let b = alToNumber(ectx.context, stack.pop());
if (!ectx.isSwfVersion7) {
if (typeof a === 'undefined')
a = 0;
if (typeof b === 'undefined')
b = 0;
}
if (!isFinite(a) || !isFinite(b)) {
if (a === -Infinity && b === -Infinity)
stack.push(-Infinity);
else if (isNaN(a) || isNaN(b))
stack.push(NaN);
else if (a === b)
stack.push(Infinity);
else if (!isFinite(a) && !isFinite(b))
stack.push(NaN);
else if (!isFinite(a))
stack.push(a);
else if (!isFinite(b))
stack.push(b);
} else if (b == null) {
stack.push(NaN);
} else {
stack.push(a + b);
}
}
function avm1_0x0B_ActionSubtract(ectx: ExecutionContext) {
const stack = ectx.stack;
let a = stack.pop();
let b = stack.pop();
a = alToNumber(ectx.context, a);
b = alToNumber(ectx.context, b);
if (!ectx.isSwfVersion7) {
if (a === null || typeof a === 'undefined')
a = 0;
if (b === null || typeof b === 'undefined')
b = 0;
}
if (!isFinite(a) || !isFinite(b)) {
if (isNaN(a) || isNaN(b))
stack.push(NaN);
else if (a === b)
stack.push(NaN);
else if (!isFinite(a)) {
if (a === -Infinity)
stack.push(Infinity);
else
stack.push(-Infinity);
} else if (!isFinite(b))
stack.push(b);
} else {
stack.push(b - a);
}
}
function avm1_0x0C_ActionMultiply(ectx: ExecutionContext) {
const stack = ectx.stack;
let a = stack.pop();
if (a === '\n')
a = NaN;
else
a = alToNumber(ectx.context, a);
let b = stack.pop();
if (b === '\n')
b = NaN;
else
b = alToNumber(ectx.context, b);
if (!ectx.isSwfVersion7) {
if (a == null || typeof a === 'undefined')
a = 0;
if (b == null || typeof b === 'undefined')
b = 0;
}
if (!isFinite(a) || !isFinite(b)) {
if (isNaN(a) || isNaN(b))
stack.push(NaN);
else if (a === b)
stack.push(Infinity);
else if (!isFinite(a) && !isFinite(b))
stack.push(-Infinity);
else if (!isFinite(a)) {
if (b == 0)
stack.push(NaN);
else if (a >= 0) {
if (b >= 0)
stack.push(Infinity);
else
stack.push(-Infinity);
} else {
if (b >= 0)
stack.push(-Infinity);
else
stack.push(Infinity);
}
} else if (!isFinite(b)) {
if (a == 0)
stack.push(NaN);
else if (b >= 0) {
if (a >= 0)
stack.push(Infinity);
else
stack.push(-Infinity);
} else {
if (a >= 0)
stack.push(-Infinity);
else
stack.push(Infinity);
}
}
} else {
stack.push(a * b);
}
}
function avm1_0x0D_ActionDivide(ectx: ExecutionContext) {
const stack = ectx.stack;
let a = stack.pop();
let b = stack.pop();
a = Number.isNaN(+a) ? a : +a;
b = Number.isNaN(+b) ? b : +b;
let type_a = typeof a;
let type_b = typeof b;
if (!ectx.isSwfVersion7) {
// for SWF version < 7:
// undefined and null get converted to 0
if (a === null || type_a === 'undefined') {
a = 0;
type_a = 'number';
}
if (b === null || type_b === 'undefined') {
b = 0;
type_b = 'number';
}
}
if (type_a === 'object' || type_b === 'object'
|| type_a === 'string' || type_b === 'string'
|| type_a === 'undefined' || type_b === 'undefined'
|| isNaN(a) || isNaN(b)) {
stack.push(NaN);
return;
}
if (type_a === 'boolean') {
a = +a;
} else
a = alToNumber(ectx.context, a);
if (type_b === 'boolean') {
b = +b;
} else
b = alToNumber(ectx.context, b);
if (!isFinite(a) || !isFinite(b) || (a == 0 && b == 0)) {
if ((a == 0 && b == 0) || (!isFinite(a) && !isFinite(b)))
stack.push(NaN);
else if (a == 0)
stack.push(b);
else if (b == 0)
stack.push(0);
else if (!isFinite(b)) {
if (b >= 0) {
if (a >= 0)
stack.push(Infinity);
else
stack.push(-Infinity);
} else {
if (a >= 0)
stack.push(-Infinity);
else
stack.push(Infinity);
}
} else if (!isFinite(a)) {
if (isNaN(a)) stack.push(a);
else stack.push(0);
}
} else if (a == 0) {
stack.push((b >