swfdis.js
JavaScript source for the SWF disassembler, 88 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)
import fs from 'node:fs';
import path from 'node:path';
import { parse } from '../lib/swf.js';
import { decode, hex } from '../lib/actions.js';
import { disassemble } from '../lib/disassemble.js';
import { decompile, printStatements } from '../lib/decompile.js';
const args = process.argv.slice(2);
const flag = (name) => args.includes(name);
const option = (name) => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const file = args.find((a, i) => !a.startsWith('--') && !['--out', '--only'].includes(args[i - 1]));
if (!file) {
console.error('usage: swfdis <movie.swf> [--asm | --both | --info] [--out dir] [--only n]');
process.exit(1);
}
const swf = parse(fs.readFileSync(file));
const only = option('--only') !== undefined ? Number(option('--only')) : null;
const outDir = option('--out');
if (flag('--info')) {
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');
const counts = new Map();
(function count(tags) {
for (const t of tags) {
counts.set(t.name, (counts.get(t.name) || 0) + 1);
if (t.sprite)
count(t.sprite.tags);
}
})(swf.tags);
console.log('\ntags: ' + [...counts].sort((a, b) => b[1] - a[1]).map(([n, c]) => `${n} ${c}`).join(', '));
if (swf.exports.size)
console.log('\nexported symbols: ' + [...swf.exports].map(([id, n]) => `${id} ${n}`).join(', '));
if (swf.labels.length)
console.log('\nframe labels: ' + swf.labels.map((l) => `${l.sprite === null ? '' : `sprite ${l.sprite} `}frame ${l.frame} "${l.name}"`).join(', '));
console.log('\nscripts:');
swf.scripts.forEach((s, n) => console.log(` ${String(n).padStart(3)} ${s.where} (${s.end - s.start} bytes at ${hex(s.start, 6)})`));
process.exit(0);
}
if (outDir)
fs.mkdirSync(outDir, { recursive: true });
swf.scripts.forEach((script, n) => {
if (only !== null && n !== only)
return;
const list = decode(script.buf, script.start, script.end, swf.version);
const title = `// ${n}: ${script.where}${script.kind === 'init' && swf.exports.has(script.sprite) ? ` (${swf.exports.get(script.sprite)})` : ''}`;
const 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${disassemble(list, script.start)}\n`]);
for (const [ext, text] of parts) {
if (outDir) {
const name = `${String(n).padStart(3, '0')} ${script.where.replace(/[^\w #,()-]+/g, '_').slice(0, 80)}.${ext}`;
fs.writeFileSync(path.join(outDir, name), text);
} else {
console.log(text);
}
}
});
// A script's statements, inside its on() or onClipEvent() handler if it has one
function actionScript(script, list) {
const stmts = decompile(list);
if (!script.handler)
return printStatements(stmts);
return `${script.handler} {\n${printStatements(stmts, new Map(), 1)}\n}`;
}