swfdis.js

JavaScript source for the SWF disassembler, 118 lines: the command line, which reads a SWF and prints its scripts as ActionScript, as disassembly, or both. It's bin/swfdis.js in swf-disassembler.zip.

#!/usr/bin/env node
//  swfdis: disassembles and decompiles the ActionScript 1 and 2 in a SWF file.
//
//  swfdis movie.swf              decompiled ActionScript for every script
//  swfdis movie.swf --asm        AVM1 disassembly instead
//  swfdis movie.swf --both       each script as ActionScript, then as disassembly
//  swfdis movie.swf --info       the header, the tags and where the scripts are
//  swfdis movie.swf --out dir    one .as file (and with --asm or --both, one .asm file) per script
//  swfdis movie.swf --only 3     just script 3 (numbered as --info lists them)

var fs = require('fs');
var path = require('path');
var parse = require('../lib/swf').parse;
var actions = require('../lib/actions');
var listing = require('../lib/disassemble');
var decompiler = require('../lib/decompile');

var args = process.argv.slice(2);
var flag = function (name) {
  return args.indexOf(name) >= 0;
};
var option = function (name) {
  var i = args.indexOf(name);
  return i >= 0 ? args[i + 1] : undefined;
};
var file = null;
for (var i = 0; i < args.length && !file; i++)
  if (args[i].indexOf('--') !== 0 && args[i - 1] !== '--out' && args[i - 1] !== '--only')
    file = args[i];

if (!file) {
  console.error('usage: swfdis <movie.swf> [--asm | --both | --info] [--out dir] [--only n]');
  process.exit(1);
}

parse(fs.readFileSync(file), function (err, swf) {
  if (err) {
    console.error(file + ': ' + err.message);
    process.exit(1);
  }
  if (flag('--info'))
    info(swf);
  else
    scripts(swf);
});

function info(swf) {
  console.log(path.basename(file) + ': SWF ' + swf.version + (swf.compressed ? ', compressed' : '') + ', ' + swf.width + 'x' + swf.height + ', ' + swf.frameRate + ' fps, ' + swf.frameCount + ' frames');
  if (swf.as3)
    console.log('contains ActionScript 3 (DoABC), which this tool does not decode');
  var counts = {};
  (function count(tags) {
    tags.forEach(function (t) {
      counts[t.name] = (counts[t.name] || 0) + 1;
      if (t.sprite)
        count(t.sprite.tags);
    });
  })(swf.tags);
  var names = Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a]; });
  console.log('\ntags: ' + names.map(function (n) { return n + ' ' + counts[n]; }).join(', '));
  if (swf.exportIds.length)
    console.log('\nexported symbols: ' + swf.exportIds.map(function (id) { return id + ' ' + swf.exports[id]; }).join(', '));
  if (swf.labels.length)
    console.log('\nframe labels: ' + swf.labels.map(function (l) {
      return (l.sprite === null ? '' : 'sprite ' + l.sprite + ' ') + 'frame ' + l.frame + ' "' + l.name + '"';
    }).join(', '));
  console.log('\nscripts:');
  swf.scripts.forEach(function (s, n) {
    console.log('  ' + listing.padLeft(String(n), 3) + '  ' + s.where + '  (' + (s.end - s.start) + ' bytes at ' + actions.hex(s.start, 6) + ')');
  });
}

function scripts(swf) {
  var only = option('--only') !== undefined ? Number(option('--only')) : null;
  var outDir = option('--out');
  if (outDir)
    makeDirectory(outDir);

  swf.scripts.forEach(function (script, n) {
    if (only !== null && n !== only)
      return;
    var list = actions.decode(script.buf, script.start, script.end, swf.version);
    var title = '//  ' + n + ': ' + script.where;
    if (script.kind === 'init' && swf.exports.hasOwnProperty(script.sprite))
      title += ' (' + swf.exports[script.sprite] + ')';

    var parts = [];
    if (!flag('--asm') || flag('--both'))
      parts.push(['as', title + '\n\n' + actionScript(script, list) + '\n']);
    if (flag('--asm') || flag('--both'))
      parts.push(['asm', ';  ' + n + ': ' + script.where + '\n\n' + listing.disassemble(list, script.start) + '\n']);

    parts.forEach(function (part) {
      if (outDir) {
        var name = listing.padLeft(String(n), 3, '0') + ' ' + script.where.replace(/[^\w #,()-]+/g, '_').slice(0, 80) + '.' + part[0];
        fs.writeFileSync(path.join(outDir, name), part[1]);
      } else {
        console.log(part[1]);
      }
    });
  });
}

//  A script's statements, inside its on() or onClipEvent() handler if it has one
function actionScript(script, list) {
  var stmts = decompiler.decompile(list);
  if (!script.handler)
    return decompiler.printStatements(stmts);
  return script.handler + ' {\n' + decompiler.printStatements(stmts, {}, 1) + '\n}';
}

//  Makes a folder, and any folders above it that aren't there yet
function makeDirectory(dir) {
  if (fs.existsSync(dir))
    return;
  makeDirectory(path.dirname(dir));
  fs.mkdirSync(dir);
}