can1357/oh-my-pi · error · ArchiveError

Invalid archive: truncated data

Error message

Invalid archive: truncated data

What it means

After reading all bytes, readTar compares the byte count to the declared source size. A mismatch means the source delivered fewer bytes than promised — the archive is truncated. This is a hard structural failure because tar parsing cannot know how much trailing data was lost.

Source

Thrown at packages/utils/src/ar/tar.ts:588

			storage: { type: "member", source: new TarMemberSource(buffer, dataOffset, sparse) },
		});
	}
	if (!sawTerminator) throw new ArchiveError("Not a valid tar archive: missing terminating zero block");
	resolvePendingLinks(entries, pendingLinks, limits);
	return [...entries.values()];
}

/** Read and index a tar source after one bounded whole-stream read. */
export const readTar: FormatReader = async (source, options) => {
	assertInMemorySize(source.size, options.limits);
	let bytes: Uint8Array;
	try {
		bytes = await readAllBytes(source);
	} catch (error) {
		if (error instanceof ArchiveError) throw error;
		throw new ArchiveError(error instanceof Error ? error.message : "Failed to read tar archive");
	}
	if (bytes.byteLength !== source.size) throw new ArchiveError("Invalid archive: truncated data");
	return readTarEntriesFromBuffer(bytes, options);
};

/** Detect a tar header, including legacy pre-ustar archives, by its checksum. */
export function sniffTar(bytes: Uint8Array): boolean {
	if (bytes.byteLength < BLOCK_SIZE) return false;
	if (isZeroBlock(bytes, 0)) return true;
	try {
		if (readTarString(bytes, NAME_OFFSET, NAME_LENGTH).length === 0) return false;
		const size = readTarSize(bytes, SIZE_OFFSET);
		return Number.isSafeInteger(paddedSize(size)) && checksumMatches(bytes, 0);
	} catch {
		return false;
	}
}

function writeField(target: Uint8Array, offset: number, length: number, value: Uint8Array): void {
	if (value.byteLength > length) throw new ArchiveError("Tar header field is too long");

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-fetch/re-copy the archive and confirm the final byte length matches the expected size
  2. Do not pass a size value larger than the actual available data; compute size from the bytes you actually have
  3. Wait for the producing process/stream to finish (check file size stability or completion event) before reading
  4. If the size came from Content-Length, compare against the actual received length and treat mismatch as a failed download

Example fix

// before: trusting a potentially stale stat size
await readTar({ size: stat.size, read: () => stream });
// after: size = what was actually read
const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
await readTar({ size: bytes.byteLength, read: async () => bytes });
Defensive patterns

Strategy: validation

Validate before calling

if (bytes.byteLength !== declaredSize) {
  throw new Error(`archive incomplete: got ${bytes.byteLength} of ${declaredSize} bytes`);
}

Try / catch

try {
  return await readTar(source, options);
} catch (err) {
  if (err instanceof ArchiveError && err.message === "Invalid archive: truncated data") {
    // re-download / wait for producer to finish
  }
  throw err;
}

Prevention

When it happens

Trigger: Providing a tar source whose `size` metadata says N but whose stream yields fewer than N bytes — a size header taken from an HTTP Content-Length while the body was cut short, a stat() size on a file being concurrently truncated, or a source that reports pre-decompression size.

Common situations: Interrupted downloads with a Content-Length-driven progress bar, files still being written while read, mismatched size metadata in custom sources, gz not fully decompressed before being handed to readTar with the compressed size.

Related errors


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