can1357/oh-my-pi · error · ArchiveError

Invalid XZ stream: truncated filter properties

Error message

Invalid XZ stream: truncated filter properties

What it means

A block header declares a filter whose properties blob (propertySize bytes) extends past the end of the header, so the declared property bytes are not present. The decoder throws rather than reading out of bounds. This indicates the block header is inconsistent or the stream is corrupt.

Source

Thrown at packages/utils/src/ar/codecs/xz.ts:447

	if (offset >= bytes.byteLength || bytes[offset] === 0)
		throw new ArchiveError("Invalid XZ stream: missing block header");
	const headerSize = (bytes[offset]! + 1) * 4;
	if (offset + headerSize > bytes.byteLength || headerSize < 8)
		throw new ArchiveError("Invalid XZ stream: truncated block header");
	if (crc32(bytes.subarray(offset, offset + headerSize - 4)) !== read32LE(bytes, offset + headerSize - 4))
		throw new ArchiveError("Invalid XZ stream: block header CRC32 mismatch");
	const cursor: Cursor = { bytes, pos: offset + 1, limit: offset + headerSize - 4 };
	const flags = bytes[cursor.pos++]!;
	if ((flags & 0x3c) !== 0) throw new ArchiveError("Unsupported XZ block flags");
	const filterCount = (flags & 3) + 1;
	const declaredCompressed = (flags & 0x40) !== 0 ? readVarInt(cursor) : undefined;
	const declaredUncompressed = (flags & 0x80) !== 0 ? readVarInt(cursor) : undefined;
	const filters: XzFilter[] = [];
	for (let index = 0; index < filterCount; index++) {
		const id = readVarInt(cursor);
		const propertySize = readVarInt(cursor);
		if (propertySize > cursor.limit - cursor.pos)
			throw new ArchiveError("Invalid XZ stream: truncated filter properties");
		filters.push({ id, properties: bytes.slice(cursor.pos, cursor.pos + propertySize) });
		cursor.pos += propertySize;
	}
	while (cursor.pos < cursor.limit)
		if (bytes[cursor.pos++] !== 0) throw new ArchiveError("Invalid XZ stream: non-zero block header padding");
	const integritySize = checkSize(checkId);
	const compressedSize = record.unpaddedSize - headerSize - integritySize;
	if (!Number.isSafeInteger(compressedSize) || compressedSize <= 0)
		throw new ArchiveError("Invalid XZ stream: compressed block size is invalid");
	if (declaredCompressed !== undefined && declaredCompressed !== compressedSize)
		throw new ArchiveError("Invalid XZ stream: block compressed size mismatch");
	if (declaredUncompressed !== undefined && declaredUncompressed !== record.uncompressedSize)
		throw new ArchiveError("Invalid XZ stream: block uncompressed size mismatch");
	const compressedStart = offset + headerSize;
	const compressedEnd = compressedStart + compressedSize;
	const paddingSize = (4 - ((headerSize + compressedSize) & 3)) & 3;
	const checkStart = compressedEnd + paddingSize;
	if (checkStart + integritySize > bytes.byteLength) throw new ArchiveError("Invalid XZ stream: truncated block data");

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-acquire the archive and verify its integrity (checksum or `xz -t`).
  2. Check that the byte buffer passed to xzDecompress is the complete file, not truncated by your own read logic.
  3. If the file comes from an untrusted source, treat it as invalid input and reject it before decompression.
  4. Re-compress the data with a standard `xz` encoder if the file came from a custom tool.

Example fix

// before
const bytes = (await Bun.file('a.xz').arrayBuffer()).slice(0, 100); // accidental truncation
await xzDecompress(new Uint8Array(bytes), maxOutput);
// after
const bytes = await Bun.file('a.xz').bytes(); // full file
await xzDecompress(bytes, maxOutput);
Defensive patterns

Strategy: validation

Validate before calling

if (bytes.byteLength < 32) throw new Error('File too small to be a valid XZ stream');
if (!isXz(bytes)) throw new Error('Missing XZ magic — refusing to decompress');

Try / catch

try {
	const out = await xzDecompress(bytes, maxOutput);
} catch (err) {
	if (err instanceof ArchiveError && /truncated/i.test(err.message)) {
		throw new Error(`Archive ${name} is corrupt or truncated; re-download it`);
	}
	throw err;
}

Prevention

When it happens

Trigger: xzDecompress encounters a block header where a filter's varint propertySize exceeds the bytes remaining between the cursor and the header's CRC field (cursor.limit - cursor.pos).

Common situations: Truncated or bit-corrupted .xz files, archives mangled by an intermediate transfer, or fuzzed/malicious inputs designed to make parsers misbehave.

Related errors


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