can1357/oh-my-pi · error · ArchiveError

Invalid CAB archive: misaligned LZX byte stream

Error message

Invalid CAB archive: misaligned LZX byte stream

What it means

LZX interleaves bit-level (Huffman) and byte-level (uncompressed block) reads; byte reads are only valid when the bit reader is word-aligned (no buffered bits). This error means readByte() was called while bits remained in the current 16-bit word — the decoder attempted to read raw bytes for an uncompressed block (type 3) or padding without aligning first, indicating a corrupt/misdeserialized stream.

Source

Thrown at packages/utils/src/ar/codecs/lzx.ts:45

				this.#word = this.#bytes[this.#offset]! | (this.#bytes[this.#offset + 1]! << 8);
				this.#offset += 2;
				this.#remaining = 16;
			}
			const take = Math.min(needed, this.#remaining);
			value = value * 2 ** take + ((this.#word >>> (this.#remaining - take)) & (2 ** take - 1));
			this.#remaining -= take;
			needed -= take;
		}
		return value;
	}

	alignWord(): void {
		this.#remaining = 0;
	}

	readByte(): number {
		if (this.#remaining !== 0) {
			throw new ArchiveError("Invalid CAB archive: misaligned LZX byte stream");
		}
		if (this.#offset >= this.#bytes.byteLength) {
			throw new ArchiveError("Invalid CAB archive: truncated LZX data");
		}
		return this.#bytes[this.#offset++]!;
	}

	readUInt32LE(): number {
		const b0 = this.readByte();
		const b1 = this.readByte();
		const b2 = this.readByte();
		const b3 = this.readByte();
		return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
	}
}

class LzxHuffmanTable {
	readonly #counts = new Uint32Array(17);

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure each decompressFrame() call receives exactly one frame's compressed data in archive order, on a single LzxDecoder per folder.
  2. Verify the CAB block stream integrity — the preceding compressed block length probably disagrees with its header; re-extract the archive.
  3. If writing a custom parser, honor LZX framing: block headers, blockLength, and the unconditional alignWord() at end of frame must all be respected.
  4. Test the same CAB with a reference tool (7z, cabextract) to confirm the archive itself is valid before debugging the decoder usage.

Example fix

// before
// feeding two half-frames to the decoder
decoder.decompressFrame(data.subarray(0, 1000), 32768);
decoder.decompressFrame(data.subarray(500), 32768); // overlapping/desynced
// after
let offset = 0;
for (const block of cfdataBlocks) {
  const out = decoder.decompressFrame(data.subarray(offset, offset + block.cbCompressed), block.cbUncompressed);
  offset += block.cbCompressed;
}
Defensive patterns

Strategy: validation

Validate before calling

// per folder: one decoder, frames in order, one frame's exact bytes per call
if (folder.lzxDecoder === undefined) folder.lzxDecoder = new LzxDecoder(folder.windowBits);
const out = folder.lzxDecoder.decompressFrame(data.subarray(off, off + cbCompressed), cbUncompressed);

Try / catch

try {
  return decoder.decompressFrame(frameBytes, outputSize);
} catch (e) {
  if (e instanceof Error && e.message.includes("misaligned LZX byte stream")) {
    throw new Error("LZX frame/block accounting desynced — verify frame order, sizes, and decoder-per-folder usage");
  }
  throw e;
}

Prevention

When it happens

Trigger: A block-type-3 (uncompressed) header or its padding byte read while the bit reader still holds unconsumed bits: the preceding block's bitstream length doesn't match what the code computed (corrupt blockLength), or the caller's outputSize/frame accounting desynced the block stream.

Common situations: Corrupt CAB archives where a compressed block's declared length is wrong; hand-rolled CAB extraction feeding frames in the wrong order or skipping alignWord() boundaries; mixing frames from different folders on one decoder instance.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/234c2ae30b83ae87. Report an issue: GitHub.