actions.js

JavaScript source for the SWF disassembler, 255 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.

var Reader = require('./reader').Reader;

//  Code -> [name, the SWF version it arrived in]
var 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
var PRELOAD_ORDER = ['this', 'arguments', 'super', 'root', 'parent', 'global'];

//  The names the preloaded values have in ActionScript
var 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.
function decode(buf, start, end, version, pool) {
  var state = { pool: pool || [] };
  return decodeRange(buf, start, end, version === undefined ? 8 : version, state);
}

function decodeRange(buf, start, end, version, state) {
  var list = [];
  var r = new Reader(buf, start, end);
  r.utf8 = version >= 6;
  var i, n, flags, bodySize;

  while (r.pos < end) {
    var offset = r.pos;
    var code = r.u8();
    var ins = { offset: offset, code: code, name: OPCODES[code] ? OPCODES[code][0] : 'unknown_' + hex(code, 2) };
    list.push(ins);

    if (code < 0x80) {
      ins.size = 1;
      if (code === 0x00)
        break;
      continue;
    }

    var length = r.u16();
    var argStart = r.pos;
    var argEnd = argStart + length;
    ins.size = 3 + length;
    var 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
        n = a.u16();
        ins.pool = [];
        for (i = 0; i < n; 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
        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();
        n = a.u16();
        ins.params = [];
        for (i = 0; i < n; i++)
          ins.params.push({ name: a.string(), register: 0 });
        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();
        n = a.u16();
        ins.registerCount = a.u8();
        ins.flags = a.u16();
        //  The flags are stored as two bytes, most significant bit first in each
        var 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 (i = 0; i < n; i++) {
          var register = a.u8();
          ins.params.push({ register: register, name: a.string() });
        }
        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
        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
        flags = a.u8();
        var trySize = a.u16();
        var catchSize = a.u16();
        var 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();
        var 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) {
  var values = [];
  while (a.pos < end) {
    var 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:
        var index = type === 8 ? a.u8() : a.u16();
        values.push({ type: 'string', value: pool[index] !== undefined ? 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)
function registerNames(fn) {
  var names = {};
  if (fn.code !== 0x8E)
    return names;
  var next = 1;
  PRELOAD_ORDER.forEach(function (p) {
    if (fn.preload[p])
      names[next++] = PRELOAD_NAMES[p];
  });
  fn.params.forEach(function (p) {
    if (p.register)
      names[p.register] = p.name;
  });
  return names;
}

function hex(n, width) {
  var s = n.toString(16).toUpperCase();
  while (s.length < (width || 4))
    s = '0' + s;
  return s;
}

exports.OPCODES = OPCODES;
exports.PRELOAD_ORDER = PRELOAD_ORDER;
exports.PRELOAD_NAMES = PRELOAD_NAMES;
exports.decode = decode;
exports.registerNames = registerNames;
exports.hex = hex;