reader.js
JavaScript source for the SWF disassembler, 165 lines: reading the integers, floats, strings and bit fields a SWF file is made of. It's lib/reader.js in swf-disassembler.zip.
// Reads the values a SWF file is made of: little-endian integers and floats, null-terminated strings,
// and the bit fields used by rectangles and matrices.
export class Reader {
constructor(buf, pos = 0, end = buf.length) {
this.buf = buf;
this.pos = pos;
this.end = end;
this.bitBuf = 0;
this.bitCount = 0;
this.utf8 = true; // SWF 6 and later store strings as UTF-8, older files as Latin-1
}
get remaining() {
return this.end - this.pos;
}
check(n) {
if (this.pos + n > this.end)
throw new Error(`read past end at ${this.pos} (+${n}, end ${this.end})`);
}
u8() {
this.check(1);
return this.buf[this.pos++];
}
u16() {
this.check(2);
const v = this.buf.readUInt16LE(this.pos);
this.pos += 2;
return v;
}
s16() {
this.check(2);
const v = this.buf.readInt16LE(this.pos);
this.pos += 2;
return v;
}
u32() {
this.check(4);
const v = this.buf.readUInt32LE(this.pos);
this.pos += 4;
return v;
}
s32() {
this.check(4);
const v = this.buf.readInt32LE(this.pos);
this.pos += 4;
return v;
}
f32() {
this.check(4);
const v = this.buf.readFloatLE(this.pos);
this.pos += 4;
return v;
}
// AVM1 doubles are little-endian with their two 32-bit halves swapped: the high word comes first
f64swapped() {
this.check(8);
const b = Buffer.concat([this.buf.subarray(this.pos + 4, this.pos + 8), this.buf.subarray(this.pos, this.pos + 4)]);
this.pos += 8;
return b.readDoubleLE(0);
}
bytes(n) {
this.check(n);
const b = this.buf.subarray(this.pos, this.pos + n);
this.pos += n;
return b;
}
skip(n) {
this.check(n);
this.pos += n;
}
// A null-terminated string
string() {
const start = this.pos;
while (this.pos < this.end && this.buf[this.pos] !== 0)
this.pos++;
if (this.pos >= this.end)
throw new Error(`unterminated string at ${start}`);
const s = this.buf.toString(this.utf8 ? 'utf8' : 'latin1', start, this.pos);
this.pos++;
return s;
}
// Bit fields: read most significant bit first, and start again on a byte boundary after align()
bits(n) {
let v = 0;
for (let i = 0; i < n; i++) {
if (this.bitCount === 0) {
this.bitBuf = this.u8();
this.bitCount = 8;
}
this.bitCount--;
v = v * 2 + ((this.bitBuf >> this.bitCount) & 1);
}
return v;
}
sbits(n) {
if (n === 0)
return 0;
const v = this.bits(n);
return v >= 2 ** (n - 1) ? v - 2 ** n : v;
}
align() {
this.bitCount = 0;
}
// A RECT: four signed fields of the same width, in twips (1/20 of a pixel)
rect() {
this.align();
const n = this.bits(5);
const r = { xmin: this.sbits(n), xmax: this.sbits(n), ymin: this.sbits(n), ymax: this.sbits(n) };
this.align();
return r;
}
// A MATRIX: optional scale and rotate pairs as 16.16 fixed point, then the translation in twips
matrix() {
this.align();
const m = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
if (this.bits(1)) {
const n = this.bits(5);
m.a = this.sbits(n) / 65536;
m.d = this.sbits(n) / 65536;
}
if (this.bits(1)) {
const n = this.bits(5);
m.b = this.sbits(n) / 65536;
m.c = this.sbits(n) / 65536;
}
const n = this.bits(5);
m.tx = this.sbits(n);
m.ty = this.sbits(n);
this.align();
return m;
}
// A CXFORM or CXFORMWITHALPHA: optional multiply and add terms, read only to step over them
cxform(withAlpha) {
this.align();
const hasAdd = this.bits(1);
const hasMult = this.bits(1);
const n = this.bits(4);
const terms = withAlpha ? 4 : 3;
if (hasMult)
for (let i = 0; i < terms; i++)
this.sbits(n);
if (hasAdd)
for (let i = 0; i < terms; i++)
this.sbits(n);
this.align();
}
}