swf.js
JavaScript source for the SWF disassembler, 342 lines: decompressing a SWF, reading its header and tags, and finding every script in frames, sprites, buttons and clip events. It's lib/swf.js in swf-disassembler.zip.
// Reads a SWF file: the header, the tags, the sprites nested inside them, and every place a script
// can live (frame actions, #initclip blocks, button events and clip events).
var zlib = require('zlib');
var Reader = require('./reader').Reader;
var TAG_NAMES = {
0: 'End', 1: 'ShowFrame', 2: 'DefineShape', 4: 'PlaceObject', 5: 'RemoveObject', 6: 'DefineBits',
7: 'DefineButton', 8: 'JPEGTables', 9: 'SetBackgroundColor', 10: 'DefineFont', 11: 'DefineText',
12: 'DoAction', 13: 'DefineFontInfo', 14: 'DefineSound', 15: 'StartSound', 17: 'DefineButtonSound',
18: 'SoundStreamHead', 19: 'SoundStreamBlock', 20: 'DefineBitsLossless', 21: 'DefineBitsJPEG2',
22: 'DefineShape2', 23: 'DefineButtonCxform', 24: 'Protect', 26: 'PlaceObject2', 28: 'RemoveObject2',
32: 'DefineShape3', 33: 'DefineText2', 34: 'DefineButton2', 35: 'DefineBitsJPEG3',
36: 'DefineBitsLossless2', 37: 'DefineEditText', 39: 'DefineSprite', 41: 'ProductInfo',
43: 'FrameLabel', 45: 'SoundStreamHead2', 46: 'DefineMorphShape', 48: 'DefineFont2',
56: 'ExportAssets', 57: 'ImportAssets', 58: 'EnableDebugger', 59: 'DoInitAction',
60: 'DefineVideoStream', 61: 'VideoFrame', 62: 'DefineFontInfo2', 63: 'DebugID', 64: 'EnableDebugger2',
65: 'ScriptLimits', 66: 'SetTabIndex', 69: 'FileAttributes', 70: 'PlaceObject3', 71: 'ImportAssets2',
73: 'DefineFontAlignZones', 74: 'CSMTextSettings', 75: 'DefineFont3', 76: 'SymbolClass',
77: 'Metadata', 78: 'DefineScalingGrid', 82: 'DoABC', 83: 'DefineShape4', 84: 'DefineMorphShape2',
86: 'DefineSceneAndFrameLabelData', 87: 'DefineBinaryData', 88: 'DefineFontName', 89: 'StartSound2',
90: 'DefineBitsJPEG4', 91: 'DefineFont4'
};
// The events a clip can have actions for, by bit in the 32-bit event flags (SWF 6 and later)
var CLIP_EVENTS = [
[0x00000001, 'load'], [0x00000002, 'enterFrame'], [0x00000004, 'unload'], [0x00000008, 'mouseMove'],
[0x00000010, 'mouseDown'], [0x00000020, 'mouseUp'], [0x00000040, 'keyDown'], [0x00000080, 'keyUp'],
[0x00000100, 'data'], [0x00000200, 'initialize'], [0x00000400, 'press'], [0x00000800, 'release'],
[0x00001000, 'releaseOutside'], [0x00002000, 'rollOver'], [0x00004000, 'rollOut'],
[0x00008000, 'dragOver'], [0x00010000, 'dragOut'], [0x00020000, 'keyPress'], [0x00040000, 'construct']
];
// The button state changes a DefineButton2 action can be attached to
var BUTTON_CONDITIONS = [
[0x0001, 'rollOver'], [0x0002, 'rollOut'], [0x0004, 'press'], [0x0008, 'release'],
[0x0010, 'dragOut'], [0x0020, 'dragOver'], [0x0040, 'releaseOutside'], [0x0080, 'dragOver'],
[0x0100, 'dragOut']
];
// Decompresses a SWF if it needs it, and calls back with the whole file uncompressed
function inflate(buf, callback) {
var sig = buf.toString('binary', 0, 3);
if (sig === 'FWS')
return callback(null, buf);
if (sig === 'CWS') {
return zlib.inflate(buf.slice(8), function (err, body) {
if (err)
return callback(err);
var out = Buffer.concat([buf.slice(0, 8), body]);
out[0] = 0x46; // F: it isn't compressed any more
callback(null, out);
});
}
if (sig === 'ZWS')
return callback(new Error('LZMA-compressed SWF (ZWS) is not supported: these are ActionScript 3 files'));
callback(new Error('not a SWF file (signature ' + JSON.stringify(sig) + ')'));
}
// Parses a SWF into its header, its tags and the scripts inside them, and calls back with it
function parse(input, callback) {
inflate(input, function (err, buf) {
if (err)
return callback(err);
var swf;
try {
swf = read(buf, input.toString('binary', 0, 1) === 'C');
} catch (e) {
return callback(e);
}
callback(null, swf);
});
}
function read(buf, compressed) {
var r = new Reader(buf);
r.skip(3);
var swf = {
version: r.u8(),
fileLength: r.u32(),
compressed: compressed,
tags: [],
scripts: [],
exports: {}, // character id -> linkage name, from ExportAssets
exportIds: [], // the ids in the order they were exported
labels: [],
sprites: {}, // character id -> { frameCount, tags }
as3: false
};
r.utf8 = swf.version >= 6;
var frame = r.rect();
swf.width = (frame.xmax - frame.xmin) / 20;
swf.height = (frame.ymax - frame.ymin) / 20;
var fraction = r.u8();
swf.frameRate = r.u8() + fraction / 256;
swf.frameCount = r.u16();
swf.tags = readTags(r, buf.length, swf, null);
return swf;
}
// Reads tags until an End tag, collecting scripts as it goes. sprite is the DefineSprite they're in.
function readTags(r, end, swf, sprite) {
var tags = [];
var frame = 1;
var id, count, i;
while (r.pos < end) {
var header = r.u16();
var code = header >> 6;
var length = header & 0x3f;
if (length === 0x3f)
length = r.u32();
var start = r.pos;
var tag = { code: code, name: TAG_NAMES[code] || 'Tag' + code, offset: start, length: length, frame: frame };
tags.push(tag);
var where = sprite === null ? 'frame ' + frame : 'sprite ' + sprite + ' frame ' + frame;
var body = new Reader(r.buf, start, start + length);
body.utf8 = r.utf8;
switch (code) {
case 1: // ShowFrame
frame++;
break;
case 12: // DoAction
addScript(swf, 'frame', where, body.buf, start, start + length, { sprite: sprite, frame: frame });
break;
case 59: // DoInitAction
id = body.u16();
addScript(swf, 'init', '#initclip sprite ' + id, body.buf, body.pos, start + length, { sprite: id, initclip: true });
break;
case 39: // DefineSprite: its own timeline of tags
id = body.u16();
var s = { id: id, frameCount: body.u16(), tags: [] };
swf.sprites[id] = s;
s.tags = readTags(body, start + length, swf, id);
tag.sprite = s;
break;
case 7: // DefineButton
readButton1(body, swf, start + length);
break;
case 34: // DefineButton2
readButton2(body, swf, start + length);
break;
case 26: // PlaceObject2
case 70: // PlaceObject3
readPlaceObject(body, swf, code, where, start + length);
break;
case 56: // ExportAssets: linkage names, which are also the names of AS2 classes
count = body.u16();
for (i = 0; i < count; i++) {
id = body.u16();
if (!swf.exports.hasOwnProperty(id))
swf.exportIds.push(id);
swf.exports[id] = body.string();
}
break;
case 43: // FrameLabel
swf.labels.push({ sprite: sprite, frame: frame, name: body.string() });
break;
case 82: // DoABC
case 72:
swf.as3 = true;
break;
}
r.pos = start + length;
if (code === 0)
break;
}
return tags;
}
function addScript(swf, kind, where, buf, start, end, extra) {
var script = { kind: kind, where: where, buf: buf, start: start, end: end };
for (var key in extra)
script[key] = extra[key];
swf.scripts.push(script);
}
// DefineButton: button records up to a zero flags byte, then one set of actions for release
function readButton1(r, swf, end) {
var id = r.u16();
while (r.u8() !== 0) {
r.u16(); // character id
r.u16(); // depth
r.matrix();
}
addScript(swf, 'button', 'button ' + id + ' on (release)', r.buf, r.pos, end, { button: id, handler: 'on (release)' });
}
// DefineButton2: button records, then a list of actions, each with the conditions that run it
function readButton2(r, swf, end) {
var id = r.u16();
r.u8(); // track as menu
var start = r.pos;
var actionOffset = r.u16();
if (actionOffset === 0)
return;
r.pos = start + actionOffset;
while (true) {
var recordStart = r.pos;
var size = r.u16();
var conditions = r.u16();
var recordEnd = size === 0 ? end : recordStart + size;
var names = [];
BUTTON_CONDITIONS.forEach(function (c) {
if (conditions & c[0] && names.indexOf(c[1]) < 0)
names.push(c[1]);
});
var key = conditions >> 9;
if (key)
names.push('keyPress ' + keyName(key));
var handler = 'on (' + names.join(', ') + ')';
addScript(swf, 'button', 'button ' + id + ' ' + handler, r.buf, r.pos, recordEnd, { button: id, handler: handler });
if (size === 0)
break;
r.pos = recordEnd;
}
}
// PlaceObject2 and PlaceObject3: step over the placement fields to reach the clip actions
function readPlaceObject(r, swf, code, where, end) {
var flags = r.u8();
var flags2 = code === 70 ? r.u8() : 0;
if (!(flags & 0x80))
return;
var depth = r.u16();
if (code === 70 && (flags2 & 0x08 || (flags2 & 0x10 && flags & 0x02)))
r.string(); // class name
var character = flags & 0x02 ? r.u16() : null;
if (flags & 0x04)
r.matrix();
if (flags & 0x08)
r.cxform(true);
if (flags & 0x10)
r.u16(); // ratio
var name = flags & 0x20 ? r.string() : null;
if (flags & 0x40)
r.u16(); // clip depth
if (code === 70) {
if (flags2 & 0x01)
skipFilters(r);
if (flags2 & 0x02)
r.u8(); // blend mode
if (flags2 & 0x04)
r.u8(); // cache as bitmap
if (flags2 & 0x20)
r.u8(); // visible
if (flags2 & 0x40)
r.u32(); // background colour
}
var wide = swf.version >= 6;
r.u16(); // reserved
if (wide) // all the events used
r.u32();
else
r.u16();
var clip = [];
if (character !== null)
clip.push('character ' + character);
if (name)
clip.push('"' + name + '"');
clip.push('depth ' + depth);
clip = clip.join(' ');
while (r.pos < end) {
var events = wide ? r.u32() : r.u16();
if (events === 0)
break;
var size = r.u32();
var recordEnd = r.pos + size;
var key = '';
if (events & 0x00020000)
key = ' ' + keyName(r.u8());
var names = [];
CLIP_EVENTS.forEach(function (c) {
if (events & c[0])
names.push(c[1] === 'keyPress' ? c[1] + key : c[1]);
});
var handler = 'onClipEvent (' + names.join(', ') + ')';
addScript(swf, 'clip', where + ', clip ' + clip + ' ' + handler, r.buf, r.pos, recordEnd, { depth: depth, name: name, handler: handler });
r.pos = recordEnd;
}
}
// PlaceObject3 filter lists: each filter has its own fixed or counted size
function skipFilters(r) {
var count = r.u8();
for (var i = 0; i < count; i++) {
var id = r.u8();
switch (id) {
case 0: r.skip(23); break; // drop shadow
case 1: r.skip(9); break; // blur
case 2: r.skip(15); break; // glow
case 3: r.skip(27); break; // bevel
case 4: // gradient glow
case 7: r.skip(r.u8() * 5 + 19); break; // gradient bevel
case 5: // convolution
var x = r.u8();
var y = r.u8();
r.skip(8 + x * y * 4 + 5);
break;
case 6: r.skip(80); break; // colour matrix
default: throw new Error('unknown filter ' + id);
}
}
}
// Key codes used by on (keyPress "...") handlers
function keyName(code) {
var special = { 1: '<Left>', 2: '<Right>', 3: '<Home>', 4: '<End>', 5: '<Insert>', 6: '<Delete>',
8: '<Backspace>', 13: '<Enter>', 14: '<Up>', 15: '<Down>', 16: '<PageUp>', 17: '<PageDown>',
18: '<Tab>', 19: '<Escape>', 32: '<Space>' };
return JSON.stringify(special[code] || String.fromCharCode(code));
}
exports.TAG_NAMES = TAG_NAMES;
exports.CLIP_EVENTS = CLIP_EVENTS;
exports.inflate = inflate;
exports.parse = parse;