can1357/oh-my-pi · error · ArchiveError

Unsupported RAR4 PPMd compressed block

Error message

Unsupported RAR4 PPMd compressed block

What it means

readTables in the RAR4 decoder reads a block-type bit from the compressed stream: a 1 indicates a PPMd (PPM version with variant H) compressed block. The decoder only implements the LZSS+Huffman path, so PPMd blocks are rejected with this error rather than producing garbage output.

Source

Thrown at packages/utils/src/ar/rar/rar4-decoder.ts:176

		const prior =
			this.#history.byteLength > dictionarySize
				? this.#history.subarray(this.#history.byteLength - dictionarySize)
				: this.#history;
		const output = new Uint8Array(prior.byteLength + unpackedSize + 260);
		output.set(prior);
		const outputStart = prior.byteLength;
		const outputEnd = outputStart + unpackedSize;
		let outputPosition = outputStart;
		const bits = new Bits(packed);
		const main = new Huffman(MAIN_SIZE);
		const distanceDecoder = new Huffman(DIST_SIZE);
		const lowDistanceDecoder = new Huffman(LOW_DIST_SIZE);
		const lengthDecoder = new Huffman(LEN_SIZE);
		const filters: PendingFilter[] = [];

		const readTables = (): void => {
			bits.align();
			if (bits.read(1) !== 0) throw new ArchiveError("Unsupported RAR4 PPMd compressed block");
			const keepPrevious = bits.read(1) !== 0;
			this.#previousLowDistance = 0;
			this.#lowDistanceRepeats = 0;
			if (!keepPrevious) this.#carriedLengths.fill(0);
			const lengths = readLengthTable(bits, this.#carriedLengths, TOTAL_SIZE);
			let offset = 0;
			main.build(lengths, offset, MAIN_SIZE);
			offset += MAIN_SIZE;
			distanceDecoder.build(lengths, offset, DIST_SIZE);
			offset += DIST_SIZE;
			lowDistanceDecoder.build(lengths, offset, LOW_DIST_SIZE);
			offset += LOW_DIST_SIZE;
			lengthDecoder.build(lengths, offset, LEN_SIZE);
			this.#carriedLengths.set(lengths);
		};

		const copyMatch = (distance: number, length: number): void => {
			if (distance <= 0 || distance > dictionarySize || distance > outputPosition) corrupt("invalid LZ distance");

View on GitHub (pinned to 9690622007)

Solutions

  1. Repack the archive using standard LZ compression in WinRAR/7-Zip (avoid the PPMd option), then retry.
  2. Extract with a full-featured tool (7-Zip, unrar) that supports PPMd and feed the unpacked files to this library.
  3. Detect the RAR compression method up front and warn/fallback for PPMd members instead of failing mid-extract.
  4. If the flag bit appears in an implausible position, suspect corruption — validate the archive with an external tool.

Example fix

// before
await extractArchive("text-heavy.ppmd.rar", dest);
// after (shell pre-conversion)
7z x text-heavy.ppmd.rar -otmp && 7z a -mx=9 text-heavy.lz.zip ./tmp/*
Defensive patterns

Strategy: fallback

Validate before calling

// Inspect the RAR main/file header compression method; PPMd is method 3-5 with the PPMd flag.
// If a member uses PPMd, route it to an external extractor instead of this decoder.
const usesPpmd = detectRarCompressionMethod(p) === "ppmd";
if (usesPpmd) return extractWithExternalTool(p, dest);

Try / catch

try {
  await extractArchive(p, dest);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("PPMd")) {
    return extractWith7zip(p, dest); // fallback decoder
  }
  throw err;
}

Prevention

When it happens

Trigger: Decoding a RAR4 file whose archive creator selected PPMd compression (RAR 3.x 'ppmd' method, -mcppmd or compression method 3–5 with PPMd) and the decoder reaches the block-table reader and sees the PPMd flag bit set.

Common situations: Archives compressed with WinRAR's PPMd option; archives where large text payloads were stored with PPMd for better ratios; extracting only some members works (LZ ones) while PPMd ones fail.

Related errors


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