actions.js
JavaScript source for the SWF disassembler, 250 lines: the AVM1 instruction set, and the decoder that turns action bytes into a tree of instructions. It's lib/actions.js in swf-disassembler.zip.
// The AVM1 instruction set (ActionScript 1 and 2, SWF 3 to 8), and a decoder that turns a run of
// action bytes into a list of instructions.
//
// Actions 0x00-0x7F are one byte. From 0x80 up, each is followed by a 16-bit length and that many
// bytes of operands.
import { Reader } from './reader.js';
// Code -> [name, the SWF version it arrived in]
export const OPCODES = {
0x00: ['end', 1],
0x04: ['nextFrame', 3], 0x05: ['prevFrame', 3], 0x06: ['play', 3], 0x07: ['stop', 3],
0x08: ['toggleQuality', 3], 0x09: ['stopSounds', 3],
0x0A: ['add', 4], 0x0B: ['subtract', 4], 0x0C: ['multiply', 4], 0x0D: ['divide', 4],
0x0E: ['equals', 4], 0x0F: ['less', 4], 0x10: ['and', 4], 0x11: ['or', 4], 0x12: ['not', 4],
0x13: ['stringEquals', 4], 0x14: ['stringLength', 4], 0x15: ['stringExtract', 4],
0x17: ['pop', 4], 0x18: ['toInteger', 4], 0x1C: ['getVariable', 4], 0x1D: ['setVariable', 4],
0x20: ['setTarget2', 4], 0x21: ['stringAdd', 4], 0x22: ['getProperty', 4], 0x23: ['setProperty', 4],
0x24: ['cloneSprite', 4], 0x25: ['removeSprite', 4], 0x26: ['trace', 4], 0x27: ['startDrag', 4],
0x28: ['endDrag', 4], 0x29: ['stringLess', 4], 0x2A: ['throw', 7], 0x2B: ['castOp', 7],
0x2C: ['implementsOp', 7], 0x2D: ['fsCommand2', 4], 0x30: ['randomNumber', 4],
0x31: ['mbStringLength', 4], 0x32: ['charToAscii', 4], 0x33: ['asciiToChar', 4], 0x34: ['getTime', 4],
0x35: ['mbStringExtract', 4], 0x36: ['mbCharToAscii', 4], 0x37: ['mbAsciiToChar', 4],
0x3A: ['delete', 5], 0x3B: ['delete2', 5], 0x3C: ['defineLocal', 5], 0x3D: ['callFunction', 5],
0x3E: ['return', 5], 0x3F: ['modulo', 5], 0x40: ['newObject', 5], 0x41: ['defineLocal2', 5],
0x42: ['initArray', 5], 0x43: ['initObject', 5], 0x44: ['typeOf', 5], 0x45: ['targetPath', 5],
0x46: ['enumerate', 5], 0x47: ['add2', 5], 0x48: ['less2', 5], 0x49: ['equals2', 5],
0x4A: ['toNumber', 5], 0x4B: ['toString', 5], 0x4C: ['pushDuplicate', 5], 0x4D: ['stackSwap', 5],
0x4E: ['getMember', 5], 0x4F: ['setMember', 5], 0x50: ['increment', 5], 0x51: ['decrement', 5],
0x52: ['callMethod', 5], 0x53: ['newMethod', 5], 0x54: ['instanceOf', 6], 0x55: ['enumerate2', 6],
0x60: ['bitAnd', 5], 0x61: ['bitOr', 5], 0x62: ['bitXor', 5], 0x63: ['bitLShift', 5],
0x64: ['bitRShift', 5], 0x65: ['bitURShift', 5], 0x66: ['strictEquals', 6], 0x67: ['greater', 6],
0x68: ['stringGreater', 6], 0x69: ['extends', 7],
0x81: ['gotoFrame', 3], 0x83: ['getURL', 3], 0x87: ['storeRegister', 5], 0x88: ['constantPool', 5],
0x8A: ['waitForFrame', 3], 0x8B: ['setTarget', 3], 0x8C: ['goToLabel', 3], 0x8D: ['waitForFrame2', 4],
0x8E: ['defineFunction2', 7], 0x8F: ['try', 7], 0x94: ['with', 5], 0x96: ['push', 4], 0x99: ['jump', 4],
0x9A: ['getURL2', 4], 0x9B: ['defineFunction', 5], 0x9D: ['if', 4], 0x9E: ['call', 4],
0x9F: ['gotoFrame2', 4],
};
// The registers a DefineFunction2 preloads, in the order it allocates them from register 1
export const PRELOAD_ORDER = ['this', 'arguments', 'super', 'root', 'parent', 'global'];
// The names the preloaded values have in ActionScript
export const PRELOAD_NAMES = { this: 'this', arguments: 'arguments', super: 'super', root: '_root', parent: '_parent', global: '_global' };
// Decodes the actions between start and end. Function, with and try bodies are decoded into their
// own lists, so the result is a tree. pool is the constant pool in force when the code starts.
export function decode(buf, start, end, version = 8, pool = []) {
const state = { pool };
return decodeRange(buf, start, end, version, state);
}
function decodeRange(buf, start, end, version, state) {
const list = [];
const r = new Reader(buf, start, end);
r.utf8 = version >= 6;
while (r.pos < end) {
const offset = r.pos;
const code = r.u8();
const [name] = OPCODES[code] || [`unknown_${hex(code, 2)}`];
const ins = { offset, code, name };
list.push(ins);
if (code < 0x80) {
ins.size = 1;
if (code === 0x00)
break;
continue;
}
const length = r.u16();
const argStart = r.pos;
const argEnd = argStart + length;
ins.size = 3 + length;
const a = new Reader(buf, argStart, argEnd);
a.utf8 = r.utf8;
switch (code) {
case 0x81: // gotoFrame
ins.frame = a.u16();
break;
case 0x83: // getURL
ins.url = a.string();
ins.window = a.string();
break;
case 0x87: // storeRegister
ins.register = a.u8();
break;
case 0x88: { // constantPool
const count = a.u16();
ins.pool = [];
for (let i = 0; i < count; i++)
ins.pool.push(a.string());
state.pool = ins.pool;
break;
}
case 0x8A: // waitForFrame
ins.frame = a.u16();
ins.skip = a.u8();
break;
case 0x8D: // waitForFrame2
ins.skip = a.u8();
break;
case 0x8B: // setTarget
case 0x8C: // goToLabel
ins.value = a.string();
break;
case 0x96: // push
ins.values = readPush(a, argEnd, state.pool);
break;
case 0x99: // jump
case 0x9D: // if
ins.branch = a.s16();
ins.target = argEnd + ins.branch;
break;
case 0x9A: // getURL2
ins.flags = a.u8();
ins.method = ['none', 'GET', 'POST'][ins.flags >> 6] || 'reserved';
ins.loadTarget = !!(ins.flags & 0x02);
ins.loadVariables = !!(ins.flags & 0x01);
break;
case 0x9F: { // gotoFrame2
const flags = a.u8();
ins.play = !!(flags & 0x01);
ins.sceneBias = flags & 0x02 ? a.u16() : 0;
break;
}
case 0x9B: { // defineFunction: the body follows the action
ins.fname = a.string();
const n = a.u16();
ins.params = [];
for (let i = 0; i < n; i++)
ins.params.push({ name: a.string(), register: 0 });
const bodySize = a.u16();
ins.body = decodeRange(buf, argEnd, argEnd + bodySize, version, { pool: state.pool });
ins.size += bodySize;
r.pos = argEnd + bodySize;
continue;
}
case 0x8E: { // defineFunction2: registers for parameters, and preloaded this, super and so on
ins.fname = a.string();
const n = a.u16();
ins.registerCount = a.u8();
ins.flags = a.u16();
// The flags are stored as two bytes, most significant bit first in each
const lo = ins.flags & 0xff, hi = ins.flags >> 8;
ins.preload = {
parent: !!(lo & 0x80), root: !!(lo & 0x40), suppressSuper: !!(lo & 0x20), super: !!(lo & 0x10),
suppressArguments: !!(lo & 0x08), arguments: !!(lo & 0x04), suppressThis: !!(lo & 0x02),
this: !!(lo & 0x01), global: !!(hi & 0x01),
};
ins.params = [];
for (let i = 0; i < n; i++) {
const register = a.u8();
ins.params.push({ register, name: a.string() });
}
const bodySize = a.u16();
ins.body = decodeRange(buf, argEnd, argEnd + bodySize, version, { pool: state.pool });
ins.size += bodySize;
r.pos = argEnd + bodySize;
continue;
}
case 0x94: { // with: the body follows the action
const bodySize = a.u16();
ins.body = decodeRange(buf, argEnd, argEnd + bodySize, version, state);
ins.size += bodySize;
r.pos = argEnd + bodySize;
continue;
}
case 0x8F: { // try: try, catch and finally bodies follow the action
const flags = a.u8();
const trySize = a.u16();
const catchSize = a.u16();
const finallySize = a.u16();
ins.catchInRegister = !!(flags & 0x04);
ins.hasCatch = !!(flags & 0x01);
ins.hasFinally = !!(flags & 0x02);
if (ins.catchInRegister)
ins.catchRegister = a.u8();
else
ins.catchName = a.string();
let p = argEnd;
ins.body = decodeRange(buf, p, p + trySize, version, state);
p += trySize;
ins.catchBody = decodeRange(buf, p, p + catchSize, version, state);
p += catchSize;
ins.finallyBody = decodeRange(buf, p, p + finallySize, version, state);
p += finallySize;
ins.size += trySize + catchSize + finallySize;
r.pos = p;
continue;
}
default:
ins.raw = a.bytes(length);
}
r.pos = argEnd;
}
return list;
}
// The values of a push: each one has a type byte, then its data
function readPush(a, end, pool) {
const values = [];
while (a.pos < end) {
const type = a.u8();
switch (type) {
case 0: values.push({ type: 'string', value: a.string() }); break;
case 1: values.push({ type: 'number', value: a.f32() }); break;
case 2: values.push({ type: 'null', value: null }); break;
case 3: values.push({ type: 'undefined', value: undefined }); break;
case 4: values.push({ type: 'register', value: a.u8() }); break;
case 5: values.push({ type: 'boolean', value: a.u8() !== 0 }); break;
case 6: values.push({ type: 'number', value: a.f64swapped() }); break;
case 7: values.push({ type: 'number', value: a.s32(), integer: true }); break;
case 8:
case 9: {
const index = type === 8 ? a.u8() : a.u16();
values.push({ type: 'string', value: pool[index] ?? `<constant ${index}>`, constant: index });
break;
}
default:
throw new Error(`unknown push type ${type} at ${a.pos - 1}`);
}
}
return values;
}
// The name held in each register at the start of a defineFunction2: preloaded values take registers
// from 1 in a fixed order, and parameters say which register they're in (0 means not in one)
export function registerNames(fn) {
const names = new Map();
if (fn.code !== 0x8E)
return names;
let next = 1;
for (const p of PRELOAD_ORDER)
if (fn.preload[p])
names.set(next++, PRELOAD_NAMES[p]);
for (const p of fn.params)
if (p.register)
names.set(p.register, p.name);
return names;
}
export function hex(n, width = 4) {
return n.toString(16).toUpperCase().padStart(width, '0');
}