@awayfl/avm1
Version:
Virtual machine for executing AS1 and AS2 code
438 lines (437 loc) • 19.1 kB
JavaScript
/*
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//module Shumway.AVM1 {
var _a;
import { CHECK_AVM1_HANG_EVERY, generateActionCalls } from './interpreter';
import { ParsedPushConstantAction, ParsedPushRegisterAction } from './parser';
import { AVM1ActionsData } from './context';
import { avm1DebuggerEnabled } from './settings';
import { ActionsDataStream } from './stream';
import { notImplemented } from '@awayfl/swf-loader';
var IS_INVALID_NAME = /[^A-Za-z0-9_/]+/g;
var cachedActionsCalls = null;
function getActionsCalls() {
if (!cachedActionsCalls) {
cachedActionsCalls = generateActionCalls();
}
return cachedActionsCalls;
}
var CustomOperationAction = /** @class */ (function () {
function CustomOperationAction(inlinePop) {
if (inlinePop === void 0) { inlinePop = false; }
this.inlinePop = inlinePop;
}
return CustomOperationAction;
}());
var ActionOptMap = (_a = {},
_a[78 /* ActionCode.ActionGetMember */] = {
AllowReturnValue: true,
AllowStackToArgs: true,
AllowCallapsDouble: true,
ArgsCount: 2,
},
_a[79 /* ActionCode.ActionSetMember */] = {
AllowStackToArgs: true,
ArgsCount: 3,
},
_a[71 /* ActionCode.ActionAdd2 */] = {
AllowStackToArgs: true,
AllowReturnValue: true,
PlainArgs: true,
ArgsCount: 2,
},
_a[103 /* ActionCode.ActionGreater */] = {
AllowStackToArgs: true,
AllowReturnValue: true,
ArgsCount: 2,
},
_a);
/**
* Bare-minimum JavaScript code generator to make debugging better.
*/
var ActionsDataCompiler = /** @class */ (function () {
function ActionsDataCompiler() {
}
ActionsDataCompiler.prototype.convertArgs = function (args, id, res, ir) {
var parts = [];
var arg;
var argsLen = args.length;
var constant;
var hint;
var currentConstantPool;
var registerNumber;
var resName;
for (var i = 0; i < argsLen; i++) {
arg = args[i];
if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) {
if (arg instanceof ParsedPushConstantAction) {
if (ir.singleConstantPool) {
constant = ir.singleConstantPool[arg.constantIndex];
parts.push(constant === undefined ? 'undefined' : JSON.stringify(constant));
}
else {
hint = '';
currentConstantPool = res.constantPool;
if (currentConstantPool) {
constant = currentConstantPool[arg.constantIndex];
hint = constant === undefined ? 'undefined' : JSON.stringify(constant);
// preventing code breakage due to bad constant
hint = hint.indexOf('*/') >= 0 ? '' : ' /* ' + hint + ' */';
}
parts.push('constantPool[' + arg.constantIndex + ']' + hint);
}
}
else if (arg instanceof ParsedPushRegisterAction) {
registerNumber = arg.registerNumber;
if (registerNumber < 0 || registerNumber >= ir.registersLimit) {
parts.push('undefined'); // register is out of bounds -- undefined
}
else {
parts.push('registers[' + registerNumber + ']');
}
}
else if (arg instanceof AVM1ActionsData) {
resName = 'code_' + id + '_' + i;
res[resName] = arg;
parts.push('res.' + resName);
}
else if (arg instanceof CustomOperationAction) {
if (arg.inlinePop)
parts.push('stack.pop()');
}
else {
notImplemented('Unknown AVM1 action argument type');
}
}
else if (arg === undefined) {
parts.push('undefined'); // special case
}
else {
parts.push(JSON.stringify(arg));
}
}
return parts.join(',');
};
/* eslint-disable-next-line max-len */
ActionsDataCompiler.prototype.convertAction = function (item, id, res, indexInBlock, ir, items, hoists) {
// const calls = getActionsCalls();
var prevItem = items[indexInBlock - 1];
var flags = item.flags;
var result = '';
if (flags === null || flags === void 0 ? void 0 : flags.optimised) {
result = " /* ".concat(item.action.actionName, " optimised */\n");
}
if (flags === null || flags === void 0 ? void 0 : flags.killed) {
return " /* ".concat(item.action.actionName, " killed by optimiser */\n");
}
if (!item.action.knownAction) {
return " // unknown actionCode ".concat(item.action.actionCode, " at ").concat(item.action.position, "\n");
}
switch (item.action.actionCode) {
case 153 /* ActionCode.ActionJump */:
case 62 /* ActionCode.ActionReturn */:
return '';
case 136 /* ActionCode.ActionConstantPool */:
res.constantPool = item.action.args[0];
hoists.forward["constPool".concat(id)] = "[".concat(this.convertArgs(item.action.args[0], id, res, ir), "]");
return " constantPool = ectx.constantPool = constPool".concat(id, ";\n");
case 150 /* ActionCode.ActionPush */:
return result + ' stack.push(' + this.convertArgs(item.action.args, id, res, ir) + ');\n';
case 135 /* ActionCode.ActionStoreRegister */: {
var registerNumber = item.action.args[0];
if (registerNumber < 0 || registerNumber >= ir.registersLimit) {
return ''; // register is out of bounds -- noop
}
return ' registers[' + registerNumber + '] = stack[stack.length - 1];\n';
}
case 138 /* ActionCode.ActionWaitForFrame */:
case 141 /* ActionCode.ActionWaitForFrame2 */: {
var args = this.convertArgs(item.action.args, id, res, ir);
return ' if (calls.' + item.action.actionName + '(ectx,[' +
args + '])) { position = ' + item.conditionalJumpTo + '; ' +
'checkTimeAfter -= ' + (indexInBlock + 1) + '; break; }\n';
}
case 157 /* ActionCode.ActionIf */:
return ' if (!!stack.pop()) { position = ' + item.conditionalJumpTo + '; ' +
'checkTimeAfter -= ' + (indexInBlock + 1) + '; break; }\n';
default: {
var args = item.action.args ? this.convertArgs(item.action.args, id, res, ir) : '';
if (args && !flags.PlainArgs) {
args = '[' + args + ']';
}
if (item.action.actionCode === 142 /* ActionCode.ActionDefineFunction2 */) {
var name_1 = "defFunArgs".concat(id);
hoists.forward[name_1] = args;
args = name_1;
}
result += ' calls.' + item.action.actionName + '(ectx' +
(args ? ', ' + args : '') + ');\n';
if (item.action.actionName == 'ActionCallMethod') {
if (!prevItem) {
result = "// strange oppcode at ".concat(item.action.position, "\n") + result;
}
if (prevItem && prevItem.action.actionCode == 150 /* ActionCode.ActionPush */) {
var args_1 = this.convertArgs(prevItem.action.args, id - 1, res, ir);
if (args_1 == '"gotoAndStop"' || args_1 == '"gotoAndPlay"') {
//|| args=='"nextFrame"' || args=='"prevFrame"'){
/* eslint-disable-next-line max-len */
result += ' if(ectx.scopeList && ectx.scopeList.scope && ectx.scopeList.scope.adaptee && !ectx.scopeList.scope.adaptee.parent){ ectx.framescriptmanager.execute_avm1_constructors(); return;}\n';
}
}
}
return result;
}
}
};
ActionsDataCompiler.prototype.inlineStackOpt = function (index, items, pushStack) {
if (!pushStack.length) {
return false;
}
var item = items[index];
var itemFlags = item.flags;
var code = item.action.actionCode;
var pushItem = items[pushStack[0]];
pushStack.length = 0;
var flags = ActionOptMap[code];
if (!flags || !flags.AllowStackToArgs) {
return false;
}
itemFlags.PlainArgs = flags.PlainArgs;
var stackArgs = pushItem.action.args;
// push stack args to getMemberArgs, to reduce array movements
if (stackArgs.length === flags.ArgsCount) {
pushItem.flags.killed = true;
item.action.args = stackArgs;
}
else if (stackArgs.length > flags.ArgsCount) {
var index_1 = stackArgs.length - flags.ArgsCount;
item.action.args = stackArgs.slice(index_1);
stackArgs.length = index_1;
pushItem.flags.optimised = true;
}
else {
var delta = flags.ArgsCount - stackArgs.length;
if (delta > 1) {
// Optimiser pop operands to arguments in reverse order
// this is BUGed if args more that 1. Skip this;
return false;
}
pushItem.flags.killed = true;
item.action.args = stackArgs.slice();
for (var i = 0; i < delta; i++) {
item.action.args.unshift(new CustomOperationAction(true));
}
}
itemFlags.optimised = true;
return true;
};
ActionsDataCompiler.prototype.optimiser = function (block) {
var items = block.items;
var pushStack = [];
var lastStackOpPassed = true;
for (var i = 0, l = items.length; i < l; i++) {
var item = items[i];
var code = item.action.actionCode;
item.flags = item.flags || {};
if (code !== 150 /* ActionCode.ActionPush */) {
/*
optimise push, push, push instruction to one push (arg, arg1, arg2)
to many used in obfuscated code
*/
if (pushStack.length > 1) {
var from = pushStack[0];
var to = pushStack[pushStack.length - 1];
var target = items[from].action;
for (var i_1 = from + 1; i_1 <= to; i_1++) {
target.args = target.args.concat(items[i_1].action.args);
items[i_1].flags.killed = true;
}
}
// we should skip optimiser if can't shure, that stack is inlined clear
if (lastStackOpPassed) {
lastStackOpPassed = this.inlineStackOpt(i, items, pushStack);
}
else {
lastStackOpPassed = false;
}
pushStack.length = 0;
}
switch (code) {
case 150 /* ActionCode.ActionPush */: {
pushStack.push(i);
// last op is pushStack, optimiser sould be passed
lastStackOpPassed = true;
break;
}
case 78 /* ActionCode.ActionGetMember */: {
// collaps doubled calls to args
var selfArgs = item.action.args;
// args not inlined, skip
if (!(selfArgs === null || selfArgs === void 0 ? void 0 : selfArgs.length)) {
break;
}
var first = selfArgs[0];
// first item should be inline pop, that we shure that opp not statically passed
if (!(first instanceof CustomOperationAction) || !first.inlinePop) {
break;
}
var j = i - 1;
// skip oppcodes, that is killed
for (; j >= 0; j--) {
if (!items[j].flags.killed) {
break;
}
}
if (j >= 0) {
var topOpp = items[j];
// chain
var topArgs = topOpp.action.args;
if (topOpp.action.actionCode === 78 /* ActionCode.ActionGetMember */ && (topArgs === null || topArgs === void 0 ? void 0 : topArgs.length)) {
topOpp.flags.killed = true;
item.flags.optimised = true;
// prepend arguments from top, drop first, because it pop
selfArgs.shift();
for (var i_2 = topArgs.length - 1; i_2 >= 0; i_2--) {
selfArgs.unshift(topArgs[i_2]);
}
}
}
}
}
}
};
ActionsDataCompiler.prototype.generate = function (ir, debugPath) {
var _this = this;
if (debugPath === void 0) { debugPath = null; }
var blocks = ir.blocks;
var res = {};
var hoists = {
forward: {}, back: {}
};
var debugName = ir.dataId.replace(IS_INVALID_NAME, '_');
var header = 'return function ' + debugName + '(ectx) {\n' +
'var position = 0;\n' +
'var checkTimeAfter = 0;\n' +
'var constantPool = ectx.constantPool, registers = ectx.registers, stack = ectx.stack;\n';
var beforeHeader = '';
var fn = '';
if (avm1DebuggerEnabled.value) {
fn += '/* Running ' + debugName + ' */ ' +
'if (Shumway.AVM1.Debugger.pause || Shumway.AVM1.Debugger.breakpoints.' +
debugName + ') { debugger; }\n';
}
fn += 'while (!ectx.isEndOfActions) {\n' +
"if (checkTimeAfter <= 0) { checkTimeAfter = ".concat(CHECK_AVM1_HANG_EVERY, "; ectx.context.checkTimeout(); }\n\n") +
'switch(position) {\n';
var uuid = 0;
blocks.forEach(function (b) {
fn += ' case ' + b.label + ':\n';
_this.optimiser(b);
var actLines = b.items.map(function (item, index) {
return _this.convertAction(item, uuid++, res, index, ir, b.items, hoists);
});
fn += actLines.join('');
fn += ' position = ' + b.jump + ';\n' +
' checkTimeAfter -= ' + b.items.length + ';\n' +
' break;\n';
});
fn += ' default: ectx.isEndOfActions = true; break;\n}\n}\n' +
'return stack.pop();};';
beforeHeader += '\n// hoisted vars\n';
for (var key in hoists.forward) {
beforeHeader += "var ".concat(key, " = ").concat(hoists.forward[key], ";\n");
}
beforeHeader += '\n';
fn = beforeHeader + header + fn;
fn += '//# sourceURL=http://jit/' + (debugPath && !IS_INVALID_NAME.test(debugPath) ? debugPath : debugName);
try {
return (new Function('calls', 'res', fn))(getActionsCalls(), res);
}
catch (e) {
// eslint-disable-next-line no-debugger
debugger;
throw e;
}
};
return ActionsDataCompiler;
}());
export { ActionsDataCompiler };
// Instead of compiling, we can match frequently used actions patterns and use
// the dictionary functions without analyzing or compilations of the code.
// The functions/patterns were selected by analyzing the large amount of
// real-life SWFs.
export function findWellknowCompilation(actionsData, context) {
var bytes = actionsData.bytes;
var fn = null;
if (bytes.length === 0 || bytes[0] === 0 /* ActionCode.None */) {
// Empty/no actions or first command is ActionEnd.
fn = actionsNoop;
}
else if (bytes.length >= 2 && bytes[1] === 0 /* ActionCode.None */) {
// Single bytes actions: ActionPlay, ActionStop, ActionStopSounds
// Example: 07 00
switch (bytes[0]) {
case 6 /* ActionCode.ActionPlay */:
fn = actionsPlay;
break;
case 7 /* ActionCode.ActionStop */:
fn = actionsStop;
break;
case 9 /* ActionCode.ActionStopSounds */:
fn = actionsStopSounds;
break;
}
}
else if (bytes.length >= 7 && bytes[6] === 0 /* ActionCode.None */ &&
bytes[0] === 129 /* ActionCode.ActionGotoFrame */ &&
bytes[1] === 2 && bytes[2] === 0 &&
bytes[5] === 6 /* ActionCode.ActionPlay */) {
// ActionGotoFrame n, ActionPlay
// Example: 81 02 00 04 00 06 00
var frameIndex = bytes[3] | (bytes[4] << 8);
fn = actionsGotoFrame.bind(null, [frameIndex, true]);
}
else if (bytes.length >= 6 && bytes[0] === 140 /* ActionCode.ActionGoToLabel */ &&
bytes[2] === 0 && bytes.length >= bytes[1] + 5 &&
bytes[bytes[1] + 4] === 0 /* ActionCode.None */ &&
bytes[bytes[1] + 3] === 6 /* ActionCode.ActionPlay */) {
// ActionGoToLabel s, ActonPlay
// Example: 8c 03 00 73 31 00 06 00
var stream = new ActionsDataStream(bytes.subarray(3, 3 + bytes[1]), context.swfVersion);
var label = stream.readString();
fn = actionsGotoLabel.bind(null, [label, true]);
}
// TODO debugger pause and breakpoints ?
return fn;
}
function actionsNoop(ectx) {
// no operations stub
}
function actionsPlay(ectx) {
getActionsCalls().ActionPlay(ectx);
}
function actionsStop(ectx) {
getActionsCalls().ActionStop(ectx);
}
function actionsStopSounds(ectx) {
getActionsCalls().ActionStopSounds(ectx);
}
function actionsGotoFrame(args, ectx) {
getActionsCalls().ActionGotoFrame(ectx, args);
}
function actionsGotoLabel(args, ectx) {
getActionsCalls().ActionGoToLabel(ectx, args);
}