decompile.js
JavaScript source for the SWF disassembler, 1346 lines: running the instructions symbolically, rebuilding loops, branches and switches from the shapes the compiler leaves, and printing the result as ActionScript. It's lib/decompile.js in swf-disassembler.zip.
// Turns decoded AVM1 actions back into ActionScript.
//
// AVM1 is a stack machine, so the decompiler runs the code symbolically: pushes put expressions on a
// stack, operators combine them, and actions with side effects become statements. Control flow is
// rebuilt from the shapes the Flash compiler produces for if/else, while, do/while, for, for..in, &&,
// || and ?:. Anything that doesn't match a known shape is left as labels and gotos, so nothing is lost.
var registerNames = require('./actions').registerNames;
var NOT = 0x12, POP = 0x17, IF = 0x9D, JUMP = 0x99, PUSH = 0x96, STORE = 0x87, EQUALS2 = 0x49, PLAY = 0x06;
var PROPERTIES = ['_x', '_y', '_xscale', '_yscale', '_currentframe', '_totalframes', '_alpha', '_visible',
'_width', '_height', '_rotation', '_target', '_framesloaded', '_name', '_droptarget', '_url', '_highquality',
'_focusrect', '_soundbuftime', '_quality', '_xmouse', '_ymouse'];
// Binary operators: opcode -> [operator, precedence]
var BINARY = {
0x0A: ['+', 11], 0x0B: ['-', 11], 0x0C: ['*', 12], 0x0D: ['/', 12], 0x0E: ['==', 8], 0x0F: ['<', 9],
0x10: ['and', 4], 0x11: ['or', 3], 0x13: ['eq', 8], 0x21: ['add', 11], 0x29: ['lt', 9],
0x3F: ['%', 12], 0x47: ['+', 11], 0x48: ['<', 9], 0x49: ['==', 8], 0x54: ['instanceof', 9],
0x60: ['&', 7], 0x61: ['|', 5], 0x62: ['^', 6], 0x63: ['<<', 10], 0x64: ['>>', 10], 0x65: ['>>>', 10],
0x66: ['===', 8], 0x67: ['>', 9], 0x68: ['gt', 9]
};
var PRECEDENCE = {
'||': 3, 'or': 3, '&&': 4, 'and': 4, '|': 5, '^': 6, '&': 7, '==': 8, '!=': 8, '===': 8, '!==': 8, 'eq': 8, 'ne': 8,
'<': 9, '>': 9, '<=': 9, '>=': 9, 'lt': 9, 'gt': 9, 'le': 9, 'ge': 9, 'instanceof': 9,
'<<': 10, '>>': 10, '>>>': 10, '+': 11, '-': 11, 'add': 11, '*': 12, '/': 12, '%': 12
};
// One-operand actions that are really built-in functions
var BUILTIN_1 = {
0x14: 'length', 0x18: 'int', 0x30: 'random', 0x31: 'mblength', 0x32: 'ord', 0x33: 'chr', 0x36: 'mbord',
0x37: 'mbchr', 0x45: 'targetPath', 0x4A: 'Number', 0x4B: 'String', 0x25: 'removeMovieClip', 0x26: 'trace'
};
// Thrown when a range that should be a pure expression turns out to need statements
function NotAnExpression() {}
// Decompiles a list of actions into statements. names maps registers to the names they hold.
function decompile(list, options) {
options = options || {};
var ctx = makeContext(list, options.names || {}, copy(options.declared || {}));
ctx.fn2 = !!options.fn2;
var result = block(ctx, 0, list.length, [], null, false);
// If gotos were needed, run again with labels at their targets
if (Object.keys(ctx.gotos).length) {
ctx.labels = copy(ctx.gotos);
ctx.placed = {};
ctx.declared = copy(options.declared || {});
result = block(ctx, 0, list.length, [], null, false);
}
return result.stmts;
}
function makeContext(list, names, declared) {
var index = {};
list.forEach(function (ins, i) {
index[ins.offset] = i;
});
var last = list[list.length - 1];
var endOffset = last ? last.offset + last.size : 0;
index[endOffset] = list.length;
// Every backward branch marks a loop: its target is the loop's first instruction
var backEdges = {};
list.forEach(function (ins, i) {
if ((ins.code === JUMP || ins.code === IF) && ins.target <= ins.offset && index[ins.target] !== undefined) {
var h = index[ins.target];
if (backEdges[h] === undefined || backEdges[h] < i)
backEdges[h] = i;
}
});
return { list: list, index: index, endOffset: endOffset, backEdges: backEdges, names: names, declared: declared,
labels: {}, placed: {}, gotos: {}, active: {} };
}
// A shallow copy of an object, also used for the sets of offsets and registers kept as object keys
function copy(o) {
var c = {};
for (var key in o)
if (o.hasOwnProperty(key))
c[key] = o[key];
return c;
}
function repeat(s, n) {
return new Array(n + 1).join(s);
}
// ---- Expression nodes --------------------------------------------------------------------------------
function lit(v) {
return { k: 'lit', v: v };
}
function variable(name) {
return { k: 'var', name: name };
}
function member(obj, prop) {
return { k: 'mem', obj: obj, prop: prop };
}
function call(fn, args) {
return { k: 'call', fn: fn, args: args };
}
function binary(op, a, b) {
return { k: 'bin', op: op, a: a, b: b };
}
function unary(op, a) {
return { k: 'un', op: op, a: a };
}
function not(e) {
return unary('!', e);
}
// The opposite of a condition, written the way the source most likely had it
function negate(e) {
if (e.k === 'un' && e.op === '!')
return simplifyCondition(e.a);
return simplifyCondition(not(e));
}
function simplifyCondition(e) {
if (e.k === 'logic') {
var c = copy(e);
c.a = simplifyCondition(e.a);
c.b = simplifyCondition(e.b);
return c;
}
if (e.k !== 'un' || e.op !== '!')
return e;
var a = e.a;
if (a.k === 'lit' && typeof a.v === 'boolean')
return lit(!a.v);
if (a.k === 'un' && a.op === '!')
return simplifyCondition(a.a);
if (a.k === 'bin') {
var flip = { '==': '!=', '!=': '==', '===': '!==', '!==': '===', '<': '>=', '>': '<=', '<=': '>', '>=': '<', 'eq': 'ne', 'lt': 'ge', 'gt': 'le' };
if (flip.hasOwnProperty(a.op))
return binary(flip[a.op], a.a, a.b);
}
return e;
}
// A string pushed as a name becomes an identifier
function nameOf(e) {
return e.k === 'lit' && typeof e.v === 'string' ? variable(e.v) : { k: 'eval', e: e };
}
function numberOf(e) {
return e.k === 'lit' && typeof e.v === 'number' ? e.v : null;
}
// A count pushed before a call or an array, which is 0 if it isn't a number
function countOf(e) {
var n = numberOf(e);
return n === null ? 0 : n;
}
function propertyName(n) {
return PROPERTIES[n] !== undefined ? PROPERTIES[n] : 'property' + n;
}
// ---- The decompiler ----------------------------------------------------------------------------------
// Decompiles list[lo..hi). stack holds values left by code before this range. loop is the innermost
// loop around it (where break and continue go). In expression mode, any statement is a failure.
function block(ctx, lo, hi, stack, loop, exprMode) {
var list = ctx.list, index = ctx.index;
var out = [];
var off = function (i) {
return i < list.length ? list[i].offset : ctx.endOffset;
};
var idx = function (offset) {
return index[offset];
};
var emit = function (s) {
if (exprMode)
throw new NotAnExpression();
out.push(s);
};
var pop = function () {
return stack.length ? stack.pop() : { k: 'missing' };
};
var push = function (e) {
stack.push(e);
};
var popArgs = function (n) {
var args = [];
for (var j = 0; j < n; j++)
args.push(pop());
return args;
};
var tryExpr = function (a, b) {
try {
var r = block(ctx, a, b, [], loop, true);
return r.stack.length === 1 ? r.stack[0] : null;
} catch (e) {
if (e instanceof NotAnExpression)
return null;
throw e;
}
};
var goTo = function (target) {
ctx.gotos[target] = true;
return { k: 'goto', target: target };
};
var jumpStatement = function (target) {
if (loop && target === loop.brk)
return { k: 'break' };
if (loop && target === loop.cont)
return { k: 'continue' };
if (target === ctx.endOffset)
return { k: 'return' };
return goTo(target);
};
// a = b = c compiles as: c, storeRegister 0, (set b), push r:0, (set a). The first set becomes an
// assignment expression, which the push of r:0 then stands for.
var chainPending = false;
var chainValue = null;
var chained = function (target, value) {
if (!chainPending)
return false;
chainPending = false;
chainValue = { k: 'assignx', target: target, value: value };
return true;
};
// a[i++] compiles as: a, i (the old value, left on the stack), then i = i + 1, then the set that
// uses the old value. If x = x ± 1 happens while an x is still waiting on the stack, that x is x++.
var postfix = function (target, value) {
if (value.k !== 'bin' || (value.op !== '+' && value.op !== '-') || !same(value.a, target) ||
value.b.k !== 'lit' || value.b.v !== 1)
return false;
for (var n = stack.length - 1; n >= 0; n--)
if (same(stack[n], target)) {
stack[n] = { k: 'postfix', op: value.op + value.op, target: target };
return true;
}
return false;
};
var i = lo;
var ins, j, r, k, m, n, e, a, b, value, target, prop, obj, fn, args, sw, T, t, E;
while (i < hi) {
ins = list[i];
if (ctx.labels[ins.offset] && !ctx.placed[ins.offset] && !exprMode) {
ctx.placed[ins.offset] = true;
out.push({ k: 'label', target: ins.offset });
}
// ---- Loops: this instruction is the target of a later backward branch inside this range
j = ctx.backEdges[i];
if (j !== undefined && j < hi && !ctx.active[i] && !exprMode) {
ctx.active[i] = true;
var exit = off(j + 1);
var inner = { brk: exit, cont: off(i) };
var back = list[j];
var next = j + 1;
var forIn = stack.length && stack[stack.length - 1].k === 'enum' ? forInShape(list, i, j, off) : null;
if (back.code === IF && j === i + 3 && list[i].code === PUSH && list[i].values.length === 1 &&
list[i].values[0].type === 'null' && list[i + 1].code === EQUALS2 && list[i + 2].code === NOT) {
// push null, equals2, not, if (back): pops what a for..in left on the stack before a break
// or return leaves the loop. It isn't in the source.
} else if (back.code === IF) {
// do { body } while (cond): the condition is left on the stack at the backward branch
r = block(ctx, i, j, [], inner, false);
emit({ k: 'dowhile', body: r.stmts, cond: r.stack.length ? r.stack.pop() : lit(true) });
} else if (forIn) {
// for (x in obj): enumerate pushed a null and then each name, and the loop pops them in turn
obj = pop().obj;
r = block(ctx, i + 4, j, [], { brk: forIn.brk, cont: off(i) }, false);
next = forIn.next;
var body = r.stmts;
target = { k: 'var', name: '?' };
var declare = false;
if (body.length && body[0].k === 'assign' && body[0].value.k === 'reg' && body[0].value.n === 0) {
target = body[0].target;
declare = body[0].declare;
body.shift();
}
emit({ k: 'forin', target: target, declare: declare, obj: obj, body: body });
} else {
// while (cond) { body }: the condition is tested at the top, and exits past the backward jump
k = -1;
for (m = i; m < j; m++)
if (list[m].code === IF && list[m].target === exit) {
k = m;
break;
}
var cond = k >= 0 ? tryExpr(i, k) : null;
if (cond) {
r = block(ctx, k + 1, j, [], inner, false);
emit(forLoop(out, negate(cond), r.stmts));
} else {
r = block(ctx, i, j, [], inner, false);
emit({ k: 'while', cond: lit(true), body: r.stmts });
}
}
delete ctx.active[i];
i = next;
continue;
}
switch (ins.code) {
case 0x00: // end
case 0x88: // constantPool
break;
case PUSH:
ins.values.forEach(function (v, n) {
if (n === 0 && chainValue && v.type === 'register' && v.value === 0) {
push(chainValue);
chainValue = null;
} else {
push(v.type === 'register' ? reg(ctx, v.value) : lit(v.value));
}
});
break;
case POP:
e = pop();
if (e.k !== 'missing' && e.k !== 'enum' && hasEffect(e))
emit({ k: 'expr', e: e });
break;
case 0x4C: // pushDuplicate: the start of a && or || if a branch and a pop follow
k = i + 1;
var and = false;
if (list[k] && list[k].code === NOT) {
and = true;
k++;
}
if (list[k] && list[k].code === IF && list[k].target > list[k].offset && list[k + 1] && list[k + 1].code === POP) {
var end = idx(list[k].target);
if (end !== undefined && end <= hi) {
var right = tryExpr(k + 2, end);
if (right) {
push({ k: 'logic', op: and ? '&&' : '||', a: pop(), b: right });
i = end;
continue;
}
}
}
var top = pop();
push(top);
push(top);
break;
case 0x4D: // stackSwap
a = pop();
b = pop();
push(a);
push(b);
break;
case 0x1C: // getVariable
push(nameOf(pop()));
break;
case 0x1D: // setVariable
value = pop();
target = nameOf(pop());
if (!chained(target, value) && !postfix(target, value))
emit({ k: 'assign', target: target, value: value });
break;
case 0x3C: // defineLocal
value = pop();
emit({ k: 'assign', target: nameOf(pop()), value: value, declare: true });
break;
case 0x41: // defineLocal2
emit({ k: 'declare', target: nameOf(pop()) });
break;
case 0x4E: // getMember
prop = pop();
push(member(pop(), prop));
break;
case 0x4F: // setMember
value = pop();
prop = pop();
target = member(pop(), prop);
if (!chained(target, value) && !postfix(target, value))
emit({ k: 'assign', target: target, value: value });
break;
case STORE: // storeRegister: copies the top of the stack into a register, leaving it there
if (!exprMode) {
sw = switchAt(ctx, i, hi, stack[stack.length - 1], loop);
if (sw) {
pop();
emit(sw.stmt);
i = sw.next;
continue;
}
}
// The start of a chained assignment: leave the value where it is for the set that follows
var nextSet = list[i + 1] && (list[i + 1].code === 0x4F || list[i + 1].code === 0x1D);
var thenR0 = list[i + 2] && list[i + 2].code === PUSH && list[i + 2].values[0] &&
list[i + 2].values[0].type === 'register' && list[i + 2].values[0].value === 0;
if (ins.register === 0 && nextSet && thenR0 && i + 2 < hi) {
chainPending = true;
break;
}
value = pop();
r = reg(ctx, ins.register);
if (exprMode) {
push({ k: 'assignx', target: r, value: value });
break;
}
var popped = list[i + 1] && list[i + 1].code === POP && i + 1 < hi;
if (popped && postfix(r, value)) {
i += 2;
continue;
}
var declared = ctx.fn2 && !ctx.declared[ins.register];
if (declared)
ctx.declared[ins.register] = true;
emit({ k: 'assign', target: r, value: value, declare: declared });
if (popped) {
i += 2;
continue;
}
push(r);
break;
case 0x3D: // callFunction
fn = nameOf(pop());
n = countOf(pop());
push(call(fn, popArgs(n)));
break;
case 0x52: // callMethod
case 0x53: // newMethod
var name = pop();
obj = pop();
n = countOf(pop());
args = popArgs(n);
fn = name.k === 'lit' && (name.v === undefined || name.v === '') ? obj : member(obj, name);
push(ins.code === 0x52 ? call(fn, args) : { k: 'new', fn: fn, args: args });
break;
case 0x40: // newObject
fn = nameOf(pop());
n = countOf(pop());
push({ k: 'new', fn: fn, args: popArgs(n) });
break;
case 0x42: // initArray
n = countOf(pop());
push({ k: 'array', items: popArgs(n) });
break;
case 0x43: // initObject
n = countOf(pop());
var pairs = [];
for (m = 0; m < n; m++) {
value = pop();
pairs.unshift([pop(), value]);
}
push({ k: 'object', pairs: pairs });
break;
case NOT:
push(not(pop()));
break;
case 0x50: // increment
push(binary('+', pop(), lit(1)));
break;
case 0x51: // decrement
push(binary('-', pop(), lit(1)));
break;
case 0x44: // typeOf
push(unary('typeof ', pop()));
break;
case 0x3A: // delete
prop = pop();
push(unary('delete ', member(pop(), prop)));
break;
case 0x3B: // delete2
push(unary('delete ', nameOf(pop())));
break;
case 0x22: // getProperty
prop = numberOf(pop());
target = pop();
push({ k: 'prop', target: target, name: propertyName(prop) });
break;
case 0x23: // setProperty
value = pop();
prop = numberOf(pop());
target = pop();
emit({ k: 'assign', target: { k: 'prop', target: target, name: propertyName(prop) }, value: value });
break;
case 0x15: // stringExtract
case 0x35: // mbStringExtract
var count = pop(), at = pop(), s = pop();
push(call(variable(ins.code === 0x15 ? 'substring' : 'mbsubstring'), [s, at, count]));
break;
case 0x34: // getTime
push(call(variable('getTimer'), []));
break;
case 0x2B: // castOp
obj = pop();
push(call(pop(), [obj]));
break;
case 0x46: // enumerate
push({ k: 'enum', obj: nameOf(pop()) });
break;
case 0x55: // enumerate2
push({ k: 'enum', obj: pop() });
break;
case 0x3E: // return
emit({ k: 'return', e: pop() });
break;
case 0x2A: // throw
emit({ k: 'throw', e: pop() });
break;
case 0x26: // trace
emit({ k: 'expr', e: call(variable('trace'), [pop()]) });
break;
case 0x69: // extends
var superclass = pop();
emit({ k: 'extends', sub: pop(), superclass: superclass });
break;
case 0x2C: // implementsOp
var ctor = pop();
n = countOf(pop());
emit({ k: 'implements', ctor: ctor, interfaces: popArgs(n) });
break;
case 0x9B: // defineFunction
case 0x8E: // defineFunction2
fn = functionNode(ins, ctx);
if (ins.fname)
emit({ k: 'function', fn: fn });
else
push(fn);
break;
case 0x94: // with
obj = pop();
emit({ k: 'with', obj: obj, body: decompile(ins.body, { names: ctx.names, declared: ctx.declared }) });
break;
case 0x8F: // try
var opts = { names: ctx.names, declared: ctx.declared };
emit({
k: 'try',
body: decompile(ins.body, opts),
catchName: ins.hasCatch ? (ins.catchInRegister ? regName(ctx, ins.catchRegister) : ins.catchName) : null,
catchBody: ins.hasCatch ? decompile(ins.catchBody, opts) : null,
finallyBody: ins.hasFinally ? decompile(ins.finallyBody, opts) : null
});
break;
// ---- Timeline and movie clip actions
case 0x04: emit(callStatement('nextFrame')); break;
case 0x05: emit(callStatement('prevFrame')); break;
case PLAY: emit(callStatement('play')); break;
case 0x07: emit(callStatement('stop')); break;
case 0x08: emit(callStatement('toggleHighQuality')); break;
case 0x09: emit(callStatement('stopAllSounds')); break;
case 0x28: emit(callStatement('stopDrag')); break;
case 0x81: // gotoFrame: frames count from 0 here and from 1 in ActionScript
if (list[i + 1] && list[i + 1].code === PLAY && i + 1 < hi) {
emit(callStatement('gotoAndPlay', [lit(ins.frame + 1)]));
i += 2;
continue;
}
emit(callStatement('gotoAndStop', [lit(ins.frame + 1)]));
break;
case 0x9F: // gotoFrame2
emit(callStatement(ins.play ? 'gotoAndPlay' : 'gotoAndStop', [pop()]));
break;
case 0x8C: // goToLabel
emit(callStatement('gotoAndStop', [lit(ins.value)]));
break;
case 0x9E: // call
emit(callStatement('call', [pop()]));
break;
case 0x83: // getURL
emit(getURLStatement(ins.url, ins.window));
break;
case 0x9A: // getURL2
target = pop();
var url = pop();
args = [url, target];
if (ins.method !== 'none')
args.push(lit(ins.method));
if (ins.loadVariables)
emit(callStatement(ins.loadTarget ? 'loadVariables' : 'loadVariablesNum', args));
else if (ins.loadTarget)
emit(callStatement('loadMovie', args));
else
emit(callStatement('getURL', args));
break;
case 0x8B: // setTarget
emit(callStatement('setTarget', [lit(ins.value)]));
break;
case 0x20: // setTarget2
emit(callStatement('setTarget', [pop()]));
break;
case 0x8A: // waitForFrame
emit({ k: 'raw', text: 'ifFrameLoaded (' + (ins.frame + 1) + ') // skips ' + ins.skip + ' actions if not' });
break;
case 0x8D: // waitForFrame2
emit({ k: 'raw', text: 'ifFrameLoaded (' + print(pop()) + ') // skips ' + ins.skip + ' actions if not' });
break;
case 0x27: // startDrag: Flash 4 pushes the flags as the strings '0' and '1'
target = pop();
var lock = pop(), constrain = pop();
args = [target, lock];
if (constrain.k !== 'lit' || Number(constrain.v)) {
var y2 = pop(), x2 = pop(), y1 = pop(), x1 = pop();
args.push(x1, y1, x2, y2);
}
emit(callStatement('startDrag', args));
break;
case 0x24: // cloneSprite
var depth = pop();
target = pop();
var source = pop();
emit(callStatement('duplicateMovieClip', [source, target, depth]));
break;
case 0x2D: // fsCommand2
emit(callStatement('fscommand2', popArgs(countOf(pop()))));
break;
// ---- Branches
case IF:
value = pop();
T = ins.target;
t = idx(T);
if (t === undefined || T <= ins.offset) {
emit({ k: 'if', cond: value, then: [goTo(T)], 'else': null });
break;
}
// value === a, if L1, value === b, if L2, jump L3: a switch from the SWF 6 compiler
if (!exprMode && value.k === 'bin' && value.op === '===') {
sw = switchChain(ctx, i, hi, value, loop);
if (sw) {
emit(sw.stmt);
i = sw.next;
continue;
}
}
// c ? a : b compiles as c, if L1, b, jump L2, L1: a, L2:
if (t - 1 > i && list[t - 1].code === JUMP && list[t - 1].target > T) {
e = idx(list[t - 1].target);
if (e !== undefined && e <= hi) {
var ifFalse = tryExpr(i + 1, t - 1);
var ifTrue = ifFalse && tryExpr(t, e);
if (ifFalse && ifTrue) {
push({ k: 'cond', c: value, a: ifTrue, b: ifFalse });
i = e;
continue;
}
}
}
// A branch out of this range: break, continue or goto
if (t > hi) {
emit({ k: 'if', cond: value, then: [jumpStatement(T)], 'else': null });
break;
}
// if (c) { a } else { b } compiles as c, not, if L1, a, jump L2, L1: b, L2:
if (t - 1 > i && list[t - 1].code === JUMP && list[t - 1].target > T) {
E = list[t - 1].target;
e = idx(E);
if (e !== undefined && e <= hi) {
a = block(ctx, i + 1, t - 1, [], loop, false);
b = block(ctx, t, e, [], loop, false);
emit({ k: 'if', cond: negate(value), then: a.stmts, 'else': b.stmts });
i = e;
continue;
}
}
// if (c) { a } compiles as c, not, if L1, a, L1:
a = block(ctx, i + 1, t, [], loop, false);
emit({ k: 'if', cond: negate(value), then: a.stmts, 'else': null });
i = t;
continue;
case JUMP:
// A jump to the next instruction, or to the end of this block, is the same as carrying on,
// unless it's a break
if (ins.target === off(i + 1) || (ins.target === off(hi) && !(loop && ins.target === loop.brk)))
break;
emit(jumpStatement(ins.target));
break;
default:
if (BINARY[ins.code]) {
b = pop();
push(binary(BINARY[ins.code][0], pop(), b));
} else if (BUILTIN_1[ins.code]) {
e = call(variable(BUILTIN_1[ins.code]), [pop()]);
if (ins.code === 0x25 || ins.code === 0x26)
emit({ k: 'expr', e: e });
else
push(e);
} else {
emit({ k: 'raw', text: '// ' + ins.name });
}
}
i++;
}
return { stmts: out, stack: stack };
}
// A switch compiles as: the value, storeRegister r, then for each case push (r,) value, strictEquals,
// if (case label), and finally a jump to the default case or past the end. Each break jumps to the end.
function switchAt(ctx, i, hi, disc, loop) {
var list = ctx.list;
var STRICT = 0x66;
var r = list[i].register;
var isCase = function (k, first) {
return list[k] && list[k].code === PUSH && list[k + 1] && list[k + 1].code === STRICT &&
list[k + 2] && list[k + 2].code === IF && list[k + 2].target > list[k + 2].offset &&
(first ? list[k].values.length === 1
: list[k].values.length === 2 && list[k].values[0].type === 'register' && list[k].values[0].value === r);
};
if (!disc || !isCase(i + 1, true))
return null;
var cases = [];
var k = i + 1;
var first = true;
while (isCase(k, first)) {
var v = list[k].values[first ? 0 : 1];
cases.push({ value: v.type === 'register' ? reg(ctx, v.value) : lit(v.value), target: list[k + 2].target });
k += 3;
first = false;
}
return buildSwitch(ctx, cases, k, hi, disc, loop);
}
// The older compiler (SWF 6) evaluates the value again for each case: value === case, if (label).
// This starts at the first if, with its comparison already popped.
function switchChain(ctx, i, hi, first, loop) {
var list = ctx.list;
var disc = first.a;
var cases = [{ value: first.b, target: list[i].target }];
var k = i + 1;
while (k < hi && list[k].code !== JUMP) {
var m = k;
while (m < hi && list[m].code !== IF && list[m].code !== JUMP)
m++;
if (m >= hi || list[m].code !== IF || list[m].target <= list[m].offset)
return null;
var e = null;
try {
var r = block(ctx, k, m, [], loop, true);
e = r.stack.length === 1 ? r.stack[0] : null;
} catch (err) {
if (!(err instanceof NotAnExpression))
throw err;
}
if (!e || e.k !== 'bin' || e.op !== '===' || !same(e.a, disc))
return null;
cases.push({ value: e.b, target: list[m].target });
k = m + 1;
}
return buildSwitch(ctx, cases, k, hi, disc, loop);
}
// The case bodies follow the jump at index k, which goes to the default case or past the end
function buildSwitch(ctx, cases, k, hi, disc, loop) {
var list = ctx.list, index = ctx.index;
if (!list[k] || list[k].code !== JUMP || list[k].target <= list[k].offset)
return null;
var D = list[k].target;
var bodyStart = k + 1;
var off = function (n) {
return n < list.length ? list[n].offset : ctx.endOffset;
};
var limit = off(hi);
// The end is where the breaks go: the furthest forward jump that stays inside this range
var starts = cases.map(function (c) { return c.target; }).concat([D]);
var lastStart = Math.max.apply(Math, starts);
var E = lastStart;
for (var m = bodyStart; m < hi && list[m].offset < E; m++)
if (list[m].code === JUMP && list[m].target > E && list[m].target <= limit)
E = list[m].target;
if (starts.concat([E]).some(function (t) { return index[t] === undefined; }))
return null;
if (off(bodyStart) !== Math.min.apply(Math, starts))
return null;
// If nothing breaks past the default label, there is no default: D is the end
var hasDefault = E > D;
var end = hasDefault ? E : D;
var inner = { brk: end, cont: loop ? loop.cont : undefined };
// Each body runs from its label to the next label; cases sharing a label share a body
var labels = [];
cases.forEach(function (c) {
if (labels.indexOf(c.target) < 0)
labels.push(c.target);
});
if (hasDefault && labels.indexOf(D) < 0)
labels.push(D);
labels.sort(function (a, b) { return a - b; });
var bodies = {};
labels.forEach(function (L, n) {
var to = n + 1 < labels.length ? labels[n + 1] : end;
bodies[L] = block(ctx, index[L], index[to], [], inner, false).stmts;
});
var groups = labels.map(function (L) {
return {
values: cases.filter(function (c) { return c.target === L; }).map(function (c) { return c.value; }),
isDefault: hasDefault && L === D,
body: bodies[L]
};
});
return { stmt: { k: 'switch', disc: disc, groups: groups }, next: index[end] };
}
// A for..in loop starts storeRegister 0, push null, equals2, if (exit), with its backward jump at j.
// If the body has a break, the break jumps to a little loop after the backward jump that pops the
// names left on the stack (push null, equals2, not, if back to itself), and the real exit is after it.
function forInShape(list, h, j, off) {
var isNull = function (k) {
return list[k] && list[k].code === PUSH && list[k].values.length === 1 && list[k].values[0].type === 'null';
};
if (!(list[h] && list[h].code === STORE && list[h].register === 0 && isNull(h + 1) &&
list[h + 2] && list[h + 2].code === EQUALS2 && list[h + 3] && list[h + 3].code === IF))
return null;
var X = list[h + 3].target;
if (X === off(j + 1))
return { brk: X, next: j + 1 };
if (isNull(j + 1) && list[j + 2] && list[j + 2].code === EQUALS2 && list[j + 3] && list[j + 3].code === NOT &&
list[j + 4] && list[j + 4].code === IF && list[j + 4].target === off(j + 1) && X === off(j + 5))
return { brk: off(j + 1), next: j + 5 };
return null;
}
// A while loop that sets a variable just before it and changes it at the end of its body is a for loop
function forLoop(out, cond, body) {
var init = out[out.length - 1];
var update = body[body.length - 1];
var hasContinue = JSON.stringify(body).indexOf('"k":"continue"') >= 0;
if (init && update && !hasContinue && init.k === 'assign' && update.k === 'assign' &&
same(init.target, update.target) && mentions(cond, init.target) && isStep(update)) {
out.pop();
return { k: 'for', init: init, cond: cond, update: update, body: body.slice(0, -1) };
}
return { k: 'while', cond: cond, body: body };
}
function isStep(s) {
return s.value.k === 'bin' && ['+', '-', '*', '/'].indexOf(s.value.op) >= 0 && same(s.value.a, s.target);
}
function mentions(e, target) {
if (same(e, target))
return true;
return Object.keys(e).some(function (key) {
var v = e[key];
return v && typeof v === 'object' && !Array.isArray(v) && mentions(v, target);
});
}
function same(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
function hasEffect(e) {
return e.k === 'call' || e.k === 'new' || e.k === 'assignx' || e.k === 'postfix' || (e.k === 'un' && e.op === 'delete ');
}
// A register, named after the parameter or preloaded value it holds where there is one
function reg(ctx, n) {
return { k: 'reg', n: n, name: ctx.names[n] };
}
function callStatement(name, args) {
return { k: 'expr', e: call(variable(name), args || []) };
}
function getURLStatement(url, win) {
if (url.indexOf('FSCommand:') === 0)
return callStatement('fscommand', [lit(url.slice(10)), lit(win)]);
var level = /^_level(\d+)$/.exec(win);
if (level && url === '')
return callStatement('unloadMovieNum', [lit(Number(level[1]))]);
if (level)
return callStatement('loadMovieNum', [lit(url), lit(Number(level[1]))]);
return callStatement('getURL', win ? [lit(url), lit(win)] : [lit(url)]);
}
// A function definition, with its body decompiled in its own register context
function functionNode(ins, ctx) {
var names = registerNames(ins);
var declared = {};
var n;
for (n in names)
declared[n] = true;
var list = ins.body;
var locals = ins.code === 0x8E;
if (ins.code === 0x9B) {
var unsaved = unsaveRegisters(list, ins.params.map(function (p) { return p.name; }), names);
if (unsaved) {
list = unsaved;
locals = true;
for (n in names)
declared[n] = true;
}
}
var body = decompile(list, { names: names, declared: declared, fn2: locals });
return { k: 'fn', name: ins.fname, params: ins.params.map(function (p) { return p.name; }), body: body };
}
// The Flash MX compiler keeps locals in registers even in a plain defineFunction. It saves the
// registers it uses on the stack (push r:1, r:2), copies this and the parameters into some of them,
// and puts the saved values back at the end. Every return jumps to that shared ending with its value
// on the stack:
//
// L: storeRegister r:0, pop, storeRegister r:2, pop, storeRegister r:1, pop, push r:0, return
//
// Taking the saving and restoring out, and turning each jump to L back into a return, leaves the
// function as it was written. Registers holding this or a parameter get its name. Returns null if
// the function isn't in this shape.
function unsaveRegisters(list, params, names) {
var first = list[0];
if (!first || first.code !== PUSH || !first.values.length ||
!first.values.every(function (v) { return v.type === 'register' && v.value > 0; }))
return null;
var saved = first.values.map(function (v) { return v.value; });
var n = saved.length;
var end = list.length;
if (list[end - 1].code === 0x00)
end--;
// The restores come last saved first: storeRegister, pop for each
var restores = function (k) {
return saved.every(function (r, m) {
var s = list[k + 2 * (n - 1 - m)];
return s && s.code === STORE && s.register === r && list[k + 2 * (n - 1 - m) + 1].code === POP;
});
};
var isReg0 = function (s) {
return s && s.code === PUSH && s.values.length === 1 && s.values[0].type === 'register' && s.values[0].value === 0;
};
var returns = end >= 2 && isReg0(list[end - 2]) && list[end - 1].code === 0x3E;
var E = returns ? end - 2 - 2 * n - 2 : end - 2 * n;
if (E < 1 || !restores(returns ? E + 2 : E))
return null;
if (returns && !(list[E].code === STORE && list[E].register === 0 && list[E + 1].code === POP))
return null;
if (list.some(function (s) { return s.target === first.offset; }))
return null;
var exit = list[E].offset;
var body = list.slice(1, E);
// With return values, falling off the end pushes undefined for the shared ending to return
var last = body[body.length - 1];
if (returns && last && last.code === PUSH && last.values.length === 1 && last.values[0].type === 'undefined')
body = body.slice(0, -1);
if (returns)
body = body.map(function (s) {
if (s.code !== JUMP || s.target !== exit)
return s;
var ret = copy(s);
ret.code = 0x3E;
ret.name = 'return';
return ret;
});
// push 'this' (or a parameter), getVariable, storeRegister r, pop: the register stands for that name
var assigned = function (r, from) {
return body.some(function (s, k) { return k >= from && s.code === STORE && s.register === r; });
};
var aliases = {};
var k = 0;
while (body[k] && body[k].code === PUSH && body[k].values.length === 1 && body[k].values[0].type === 'string' &&
body[k + 1] && body[k + 1].code === 0x1C && body[k + 2] && body[k + 2].code === STORE &&
body[k + 3] && body[k + 3].code === POP) {
var name = body[k].values[0].value;
var r = body[k + 2].register;
if (saved.indexOf(r) < 0 || names.hasOwnProperty(r) || aliases.hasOwnProperty(r) ||
!(params.indexOf(name) >= 0 || (name === 'this' && !assigned(r, k + 3))))
break;
aliases[r] = name;
k += 4;
}
if (body.slice(0, k).some(function (s) { return body.some(function (t) { return t.target === s.offset; }); }))
return null;
for (r in aliases)
names[r] = aliases[r];
return body.slice(k);
}
function regName(ctx, n) {
return ctx.names[n] !== undefined ? ctx.names[n] : 'r' + n;
}
// ---- Printing ----------------------------------------------------------------------------------------
var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
// Prints statements as ActionScript, indented by depth
function printStatements(stmts, names, depth) {
var lines = [];
stmts.forEach(function (s) {
printStatement(s, names || {}, depth || 0, lines);
});
return lines.join('\n');
}
function printStatement(s, names, depth, lines) {
var pad = repeat(' ', depth);
var p = function (e, prec) {
return print(e, names, prec || 0, depth);
};
var body = function (stmts) {
stmts.forEach(function (t) {
printStatement(t, names, depth + 1, lines);
});
};
switch (s.k) {
case 'expr':
lines.push(pad + p(s.e) + ';');
break;
case 'assign':
// var a, b; compiles to registers set to undefined
if (s.declare && s.value.k === 'lit' && s.value.v === undefined)
lines.push(pad + 'var ' + p(s.target) + ';');
else
lines.push(pad + (s.declare ? 'var ' : '') + printAssignment(s.target, s.value, names, depth) + ';');
break;
case 'declare':
lines.push(pad + 'var ' + p(s.target) + ';');
break;
case 'return':
lines.push(s.e && s.e.k !== 'missing' && !(s.e.k === 'lit' && s.e.v === undefined) ? pad + 'return ' + p(s.e) + ';' : pad + 'return;');
break;
case 'throw':
lines.push(pad + 'throw ' + p(s.e) + ';');
break;
case 'if':
lines.push(pad + 'if (' + p(s.cond) + ') {');
body(s.then);
var els = s['else'];
// else { if ... } is written as else if
while (els && els.length === 1 && els[0].k === 'if') {
lines.push(pad + '} else if (' + p(els[0].cond) + ') {');
body(els[0].then);
els = els[0]['else'];
}
if (els && els.length) {
lines.push(pad + '} else {');
body(els);
}
lines.push(pad + '}');
break;
case 'while':
lines.push(pad + 'while (' + p(s.cond) + ') {');
body(s.body);
lines.push(pad + '}');
break;
case 'dowhile':
lines.push(pad + 'do {');
body(s.body);
lines.push(pad + '} while (' + p(s.cond) + ');');
break;
case 'for':
var init = (s.init.declare ? 'var ' : '') + printAssignment(s.init.target, s.init.value, names, depth);
var update = printAssignment(s.update.target, s.update.value, names, depth);
lines.push(pad + 'for (' + init + '; ' + p(s.cond) + '; ' + update + ') {');
body(s.body);
lines.push(pad + '}');
break;
case 'forin':
lines.push(pad + 'for (' + (s.declare ? 'var ' : '') + p(s.target) + ' in ' + p(s.obj) + ') {');
body(s.body);
lines.push(pad + '}');
break;
case 'with':
lines.push(pad + 'with (' + p(s.obj) + ') {');
body(s.body);
lines.push(pad + '}');
break;
case 'try':
lines.push(pad + 'try {');
body(s.body);
if (s.catchBody) {
lines.push(pad + '} catch (' + s.catchName + ') {');
body(s.catchBody);
}
if (s.finallyBody) {
lines.push(pad + '} finally {');
body(s.finallyBody);
}
lines.push(pad + '}');
break;
case 'function':
var fn = s.fn;
lines.push(pad + 'function ' + fn.name + '(' + fn.params.join(', ') + ') {');
var inner = fnNames(fn);
fn.body.forEach(function (t) {
printStatement(t, inner, depth + 1, lines);
});
lines.push(pad + '}');
break;
case 'switch':
lines.push(pad + 'switch (' + p(s.disc) + ') {');
s.groups.forEach(function (g) {
g.values.forEach(function (v) {
lines.push(pad + ' case ' + p(v) + ':');
});
if (g.isDefault)
lines.push(pad + ' default:');
g.body.forEach(function (t) {
printStatement(t, names, depth + 2, lines);
});
});
lines.push(pad + '}');
break;
case 'extends':
lines.push(pad + p(s.sub) + ' extends ' + p(s.superclass) + ';');
break;
case 'implements':
lines.push(pad + p(s.ctor) + ' implements ' + s.interfaces.map(function (e) { return p(e); }).join(', ') + ';');
break;
case 'break':
lines.push(pad + 'break;');
break;
case 'continue':
lines.push(pad + 'continue;');
break;
case 'goto':
lines.push(pad + 'goto ' + labelName(s.target) + ';');
break;
case 'label':
lines.push(repeat(' ', Math.max(0, depth - 1)) + labelName(s.target) + ':');
break;
case 'raw':
lines.push(pad + s.text);
break;
default:
lines.push(pad + '// ' + s.k);
}
}
// Assignments are written as x++, x += y and so on where they can be
function printAssignment(target, value, names, depth) {
var t = print(target, names, 0, depth);
if (value.k === 'bin' && same(value.a, target)) {
if (value.b.k === 'lit' && value.b.v === 1 && (value.op === '+' || value.op === '-'))
return t + value.op + value.op;
if (['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>'].indexOf(value.op) >= 0)
return t + ' ' + value.op + '= ' + print(value.b, names, 1, depth);
}
return t + ' = ' + print(value, names, 1, depth);
}
function fnNames(fn) {
return fn.names || {};
}
function labelName(offset) {
return 'label_' + offset.toString(16).toUpperCase();
}
// Prints an expression. prec is the precedence of the context, so brackets go in only where needed.
function print(e, names, prec, depth) {
names = names || {};
prec = prec || 0;
depth = depth || 0;
var p = function (x, q) {
return print(x, names, q, depth);
};
var wrap = function (mine, text) {
return mine < prec ? '(' + text + ')' : text;
};
var q;
switch (e.k) {
case 'lit':
return literal(e.v);
case 'reg':
return e.name !== undefined ? e.name : 'r' + e.n;
case 'var':
return e.name;
case 'eval':
return 'eval(' + p(e.e, 0) + ')';
case 'missing':
return '/*?*/';
case 'mem':
var obj = p(e.obj, 14);
if (e.prop.k === 'lit' && typeof e.prop.v === 'string' && IDENTIFIER.test(e.prop.v))
return obj + '.' + e.prop.v;
return obj + '[' + p(e.prop, 0) + ']';
case 'prop':
// An empty target is the timeline the code is on
if (e.target.k === 'lit' && e.target.v === '')
return e.name;
return p(e.target, 14) + '.' + e.name;
case 'call':
var fn = e.fn.k === 'fn' ? '(' + p(e.fn, 0) + ')' : p(e.fn, 14);
return fn + '(' + e.args.map(function (a) { return p(a, 1); }).join(', ') + ')';
case 'new':
return wrap(14, 'new ' + p(e.fn, 15) + '(' + e.args.map(function (a) { return p(a, 1); }).join(', ') + ')');
case 'bin':
// -x compiles as 0 - x
if (e.op === '-' && e.a.k === 'lit' && e.a.v === 0)
return wrap(13, '-' + p(e.b, 13));
q = PRECEDENCE.hasOwnProperty(e.op) ? PRECEDENCE[e.op] : 9;
return wrap(q, p(e.a, q) + ' ' + e.op + ' ' + p(e.b, q + 1));
case 'un':
return wrap(13, e.op + p(e.a, 13));
case 'logic':
q = PRECEDENCE[e.op];
return wrap(q, p(e.a, q) + ' ' + e.op + ' ' + p(e.b, q + 1));
case 'cond':
return wrap(2, p(e.c, 3) + ' ? ' + p(e.a, 2) + ' : ' + p(e.b, 2));
case 'assignx':
// Used as a value, x = x + 1 is ++x
var v = e.value;
if (v.k === 'bin' && (v.op === '+' || v.op === '-') && v.b.k === 'lit' && v.b.v === 1 && same(v.a, e.target))
return wrap(13, v.op + v.op + p(e.target, 13));
return wrap(1, printAssignment(e.target, e.value, names, depth));
case 'postfix':
return wrap(14, p(e.target, 14) + e.op);
case 'array':
return '[' + e.items.map(function (a) { return p(a, 1); }).join(', ') + ']';
case 'object':
if (!e.pairs.length)
return '{}';
return '{' + e.pairs.map(function (pair) {
var k = pair[0];
return (k.k === 'lit' && IDENTIFIER.test(String(k.v)) ? k.v : p(k, 0)) + ': ' + p(pair[1], 1);
}).join(', ') + '}';
case 'fn':
var lines = [];
var inner = fnNames(e);
e.body.forEach(function (t) {
printStatement(t, inner, depth + 1, lines);
});
var head = 'function' + (e.name ? ' ' + e.name : '') + '(' + e.params.join(', ') + ') {';
return lines.length ? head + '\n' + lines.join('\n') + '\n' + repeat(' ', depth) + '}' : head + '}';
case 'enum':
return '/* enumerate ' + p(e.obj, 0) + ' */';
default:
return '/* ' + e.k + ' */';
}
}
// Rounds to the nearest 32-bit float, the precision a push stores most numbers in
var float32 = new Float32Array(1);
function toFloat32(v) {
float32[0] = v;
return float32[0];
}
function literal(v) {
if (v === undefined)
return 'undefined';
if (v === null)
return 'null';
if (typeof v === 'string')
return JSON.stringify(v);
if (typeof v === 'number') {
if (v % 1 === 0)
return String(v);
// 32-bit floats print with a long tail: keep the digits a person would have typed
var short = Number(v.toPrecision(7));
return String(toFloat32(short) === toFloat32(v) && Math.abs(short - v) < 1e-6 * Math.abs(v) ? short : v);
}
return String(v);
}
exports.decompile = decompile;
exports.printStatements = printStatements;
exports.labelName = labelName;
exports.print = print;