can1357/oh-my-pi · error · ArchiveError

Truncated compress (.Z) header

Error message

Truncated compress (.Z) header

What it means

A valid compress (.Z) stream begins with the 2-byte magic 0x1F 0x9D followed by a flags byte — at least 3 bytes total. This error is thrown when the input buffer is shorter than 3 bytes, so no header can even be read. (A 3+ byte input with wrong magic throws the sibling 'Invalid compress (.Z) header' instead.)

Source

Thrown at packages/utils/src/ar/codecs/lzw.ts:101

		if (needed <= this.#bytes.byteLength) {
			return;
		}
		let capacity = Math.max(needed, Math.min(this.#limit, Math.max(64, this.#bytes.byteLength * 2)));
		if (capacity > this.#limit) {
			capacity = this.#limit;
		}
		const grown = new Uint8Array(capacity);
		grown.set(this.#bytes.subarray(0, this.#length));
		this.#bytes = grown;
	}
}

function decode(bytes: Uint8Array, maxOutput: number): Uint8Array {
	if (!Number.isSafeInteger(maxOutput) || maxOutput < 0) {
		throw new ArchiveError("Invalid compress (.Z) output limit");
	}
	if (bytes.byteLength < 3) {
		throw new ArchiveError("Truncated compress (.Z) header");
	}
	if (bytes[0] !== 0x1f || bytes[1] !== 0x9d) {
		throw new ArchiveError("Invalid compress (.Z) header");
	}

	const flags = bytes[2]!;
	if ((flags & 0x60) !== 0) {
		throw new ArchiveError("Unsupported compress (.Z) header flags");
	}
	const maxBits = flags & 0x1f;
	if (maxBits < MIN_BITS || maxBits > MAX_BITS) {
		throw new ArchiveError(`Invalid compress (.Z) maximum code width ${maxBits}`);
	}
	const blockMode = (flags & 0x80) !== 0;
	const dictionaryLimit = 2 ** maxBits;
	const parents = new Uint16Array(dictionaryLimit);
	const suffixes = new Uint8Array(dictionaryLimit);
	const stack = new Uint8Array(dictionaryLimit);

View on GitHub (pinned to 9690622007)

Solutions

  1. Check bytes.byteLength >= 3 (ideally the 0x1F9D magic) before calling lzwDecompress.
  2. Verify the source file is non-empty and is actually a .Z file (`file data.Z`).
  3. Fix the slicing/read logic that produced the short buffer.
  4. Confirm you're not passing a wrong variable (e.g. a small header buffer instead of the full payload).

Example fix

// before
const data = await Bun.file(path).bytes();
lzwDecompress(data, max);
// after
const data = await Bun.file(path).bytes();
if (data.byteLength < 3 || data[0] !== 0x1f || data[1] !== 0x9d) throw new Error(`Not a compress (.Z) file: ${path}`);
lzwDecompress(data, max);
Defensive patterns

Strategy: type-guard

Validate before calling

function looksLikeCompressZ(bytes: Uint8Array): boolean {
  return bytes.byteLength >= 3 && bytes[0] === 0x1f && bytes[1] === 0x9d;
}

Type guard

function isCompressZ(v: Uint8Array): v is Uint8Array & { length: 3 } {
  return v.byteLength >= 3 && v[0] === 0x1f && v[1] === 0x9d;
}

Try / catch

try {
  return lzwDecompress(bytes, maxOutput);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("header")) {
    throw new Error(`Input is not a compress (.Z) stream (${bytes.byteLength} bytes)`, { cause: err });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling lzwDecompress with an empty, 1-byte, or 2-byte buffer — e.g. an empty file, a mis-sliced subarray, or reading zero bytes from a stream.

Common situations: Empty-file upload accepted without size checks; wrong path/column sliced from a container; a read that returned 0 bytes due to an IO error treated as success; passing the wrong variable to the decompressor.

Related errors


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