can1357/oh-my-pi · error · ArchiveError

Invalid ZIP archive: malformed extra field

Error message

Invalid ZIP archive: malformed extra field

What it means

Thrown by parseExtra when an extra-field record's declared data size (id:u16, size:u16, then size bytes) extends beyond the end of the extra-field blob. The record header parses but its payload overruns — the enclosing extra_field_length is larger than the actual contained records.

Source

Thrown at packages/utils/src/ar/zip.ts:264

	const day = date & 0x1f;
	const hour = (time >>> 11) & 0x1f;
	const minute = (time >>> 5) & 0x3f;
	const second = (time & 0x1f) * 2;
	if (month < 0 || month > 11 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return undefined;
	const value = new Date(year, month, day, hour, minute, second).getTime();
	return Number.isFinite(value) ? value : undefined;
}

function parseExtra(extra: Uint8Array, rawName: Uint8Array): ParsedExtra {
	const result: ParsedExtra = {};
	let offset = 0;
	while (offset < extra.byteLength) {
		if (offset + 4 > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: truncated extra-field header");
		const id = readUInt16LE(extra, offset);
		const size = readUInt16LE(extra, offset + 2);
		const dataStart = offset + 4;
		const dataEnd = dataStart + size;
		if (dataEnd > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: malformed extra field");
		const data = extra.subarray(dataStart, dataEnd);
		if (id === 0x0001) {
			result.zip64 = data;
		} else if (id === 0x7075) {
			if (data.byteLength < 5) throw new ArchiveError("Invalid ZIP archive: Unicode path extra field is too small");
			if (data[0] === 1 && readUInt32LE(data, 1) === crc32(rawName)) {
				try {
					result.unicodePath = UTF8_FATAL_DECODER.decode(data.subarray(5));
				} catch {
					// A bad optional Unicode path falls back to the header name.
				}
			}
		} else if (id === 0x5455) {
			if (data.byteLength < 1)
				throw new ArchiveError("Invalid ZIP archive: extended timestamp extra field is too small");
			if ((data[0]! & 1) !== 0) {
				if (data.byteLength < 5)
					throw new ArchiveError("Invalid ZIP archive: extended timestamp extra field is too small");

View on GitHub (pinned to 9690622007)

Solutions

  1. Run `unzip -t archive.zip` to confirm corruption and identify the offending entry.
  2. Re-zip the files with a standard tool to get consistent extra fields.
  3. Repair with `zip -FF broken.zip --out fixed.zip`.
  4. Verify transfer integrity (checksum) so corruption is caught before parsing.

Example fix

// before: parsing a checksum-unverified transfer
await download(url, path);
const zip = readZip(path);
// after: verify then parse
await download(url, path);
if (await sha256File(path) !== manifest.sha256) throw new Error("corrupted download");
const zip = readZip(path);
Defensive patterns

Strategy: validation

Validate before calling

const probe = Bun.spawnSync(["unzip", "-t", path]);
if (probe.exitCode !== 0) throw new Error(`archive structurally invalid per unzip -t; refusing to read: ${path}`);

Try / catch

try {
  const zip = readZip(path);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("malformed extra field")) {
    // treat as corrupt input: repair or reject
    await $`zip -FF ${path} --out fixed.zip`.nothrow();
    return readZip("fixed.zip");
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling zip info/read on an archive whose entry extra field contains a record whose size field exceeds the remaining bytes, e.g. corrupted size bytes or a writer that emitted oversized record headers.

Common situations: Bit corruption in the extra-field region; hand-edited archives; writers that compute record sizes in the wrong units (bytes vs words); fuzzed or malicious zips.

Understand the failure class

Related errors


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