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');
}