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