can1357/oh-my-pi · error · ArchiveError

Invalid ARJ method-4 compressed data: truncated bitstream

Error message

Invalid ARJ method-4 compressed data: truncated bitstream

What it means

ARJ compression method 4 (the built-in variant this reader decompresses) uses an MSB-first bit reader over the packed stream. read(count) throws when the stream runs out of bits before `count` are available (or count is not an integer in 0..24), meaning the compressed data is truncated relative to what the decoder still needs.

Solutions

  1. Check the member's compressed size in the index against the actual available bytes; re-download the archive if shorter.
  2. Test the archive externally (`arj t` / 7-Zip) to confirm which member is corrupt.
  3. Re-extract that member with a reference ARJ tool; if it also fails, the packed data is unrecoverable — restore from backup.
  4. If you slice packed data out yourself, copy exactly the compressedSize bytes the header declares, not a rounded or padded amount.

Example fix

// before: slicing packed bytes with a wrong length
const packed = bytes.subarray(dataStart, dataStart + guessedSize);
// after: use the declared compressed size and bounds-check against the buffer
const packed = bytes.subarray(dataStart, dataStart + entry.compressedSize);
if (packed.byteLength !== entry.compressedSize) {
  throw new Error(`Member ${entry.name}: truncated packed data`);
}
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the member's full packed payload is present before decompressing
function packedDataComplete(bytes: Uint8Array, dataOffset: number, compressedSize: number): boolean {
  return dataOffset >= 0 && compressedSize >= 0 && dataOffset + compressedSize <= bytes.byteLength;
}

Try / catch

try {
  return decompressArjMethod4(packed, originalSize);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("truncated bitstream")) {
    throw new Error(`Member data truncated: expected ${packed.byteLength}+ bits of packed data`);
  }
  throw err;
}

Prevention

When it happens

Trigger: decompressArjMethod4() decodes more symbols than the packed stream can supply: packed data was truncated, outSize does not match the actual compressed stream, or the packed bytes were altered so the Huffman/LZ state machine consumes bits off the end.

Common situations: A partially downloaded or truncated member (compressed size in the header larger than the bytes actually present), a corrupted stream mid-decode, or a hand-spliced extraction where the packed payload was copied with the wrong length.

Related errors


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

Appendix: source

Thrown at packages/utils/src/ar/arj.ts:104

			throw new ArchiveError("Invalid ARJ extended header CRC32");
		}
		cursor += extensionSize + 4;
		assertIndexSize(cursor - offset, options.limits, "header metadata");
	}
	return { bodyStart, bodySize, nextOffset: cursor, metadataSize: cursor - offset, isEnd: false };
}

class ArjBitReader {
	readonly #bytes: Uint8Array;
	#position = 0;

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

	read(count: number): number {
		if (!Number.isInteger(count) || count < 0 || count > 24 || this.#position + count > this.#bytes.byteLength * 8) {
			throw new ArchiveError("Invalid ARJ method-4 compressed data: truncated bitstream");
		}
		let value = 0;
		for (let index = 0; index < count; index++) {
			const position = this.#position++;
			value = value * 2 + ((this.#bytes[position >>> 3]! >>> (7 - (position & 7))) & 1);
		}
		return value;
	}

	assertZeroPadding(): void {
		while (this.#position < this.#bytes.byteLength * 8) {
			if (this.read(1) !== 0) throw new ArchiveError("Invalid ARJ method-4 compressed data: non-zero trailing bits");
		}
	}
}

function decompressArjMethod4(packed: Uint8Array, outSize: number): Uint8Array {
	const reader = new ArjBitReader(packed);

View on GitHub (pinned to 9690622007)