can1357/oh-my-pi · error · ArchiveError

Invalid CAB archive: truncated LZX bitstream

Error message

Invalid CAB archive: truncated LZX bitstream

What it means

The LZX bit reader refills from the CAB CFDATA block two bytes at a time; this error means it needed more bits but the input buffer ran out. The compressed data block is truncated relative to what the stream's decoded output requires — either the CFDATA block was cut short or the caller passed fewer bytes than the block header promises (cbCompressed).

Source

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

const POSITION_SLOTS = [30, 32, 34, 36, 38, 42, 50] as const;

class LzxBitReader {
	readonly #bytes: Uint8Array;
	#offset = 0;
	#word = 0;
	#remaining = 0;

	constructor(bytes: Uint8Array) {
		this.#bytes = bytes;
	}

	readBits(count: number): number {
		let value = 0;
		let needed = count;
		while (needed > 0) {
			if (this.#remaining === 0) {
				if (this.#offset + 2 > this.#bytes.byteLength) {
					throw new ArchiveError("Invalid CAB archive: truncated LZX bitstream");
				}
				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 {

View on GitHub (pinned to 9690622007)

Solutions

  1. Pass the full CFDATA block bytes for the frame — exactly cbCompressed bytes as declared in the CAB header, read contiguously.
  2. Verify the CAB file is complete (file size vs. total declared blocks) and re-download if truncated.
  3. If the folder spans multiple data blocks, concatenate/reserve the folder's compressed data before decoding frames rather than feeding partial buffers.
  4. Validate your CAB parser's cbCompressed/cbUncompressed accounting against the remaining file length.

Example fix

// before
const frame = cabData.subarray(0, guessed); // guessed too small
const out = decoder.decompressFrame(frame, 32768);
// after
const frame = cabData.subarray(offset, offset + cfdatum.cbCompressed);
const out = decoder.decompressFrame(frame, cfdatum.cbUncompressed);
Defensive patterns

Strategy: validation

Validate before calling

if (cfdatum.cbCompressed > data.byteLength - offset) {
  throw new Error(`CAB truncated: need ${cfdatum.cbCompressed} bytes at ${offset}, have ${data.byteLength - offset}`);
}
const frame = data.subarray(offset, offset + cfdatum.cbCompressed);

Type guard

function hasFullCfdataBlock(data: Uint8Array, offset: number, cbCompressed: number): boolean {
  return offset >= 0 && cbCompressed >= 0 && offset + cbCompressed <= data.byteLength;
}

Try / catch

try {
  return decoder.decompressFrame(frame, outputSize);
} catch (e) {
  if (e instanceof Error && e.message.includes("truncated LZX bitstream")) {
    throw new Error("CFDATA block shorter than declared — CAB file is truncated or misparsed");
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling LzxDecoder.decompressFrame(bytes, outputSize) with a bytes buffer shorter than the frame's compressed size: reading CFDATA blocks with a wrong cbCompressed value, slicing the archive incorrectly, or a corrupt CAB whose data block length field doesn't match actual stored bytes.

Common situations: Partial downloads of .cab files; custom CAB parsers passing only part of a multi-block folder's data; corrupted archives; using the LZX decoder directly with per-block buffers when the caller accumulated the wrong byte counts.

Related errors


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