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