Hmbown/CodeWhale · error · Error
Invalid update file header.
Error message
Invalid update file header.
What it means
For each central-directory entry the validator follows its stored local-header offset and requires (a) the 30-byte fixed local header fits inside the region already scanned, and (b) the bytes there carry the local file header signature 0x04034b50 (PK\x03\x04). If the offset is out of range or the signature is missing, the central directory and local entries disagree — the archive is malformed or hand-forged — and extraction is refused.
Solutions
- Regenerate the archive with a standard tool (`ditto -c -k` or `zip -r`) instead of post-processing or concatenating zip files.
- Verify with `unzip -t file.zip` — it reports mismatched local headers as 'bad zipfile' — before publishing.
- Re-download and re-verify the SHA-256 of the release asset; the bytes may be truncated or tampered in transit.
- Never edit zip offsets by hand in build scripts; rebuild the artifact from source.
Defensive patterns
Strategy: validation
Validate before calling
if (execFileSync("unzip", ["-t", zipPath], {stdio:"pipe"}).status !== 0) throw new Error("Archive fails unzip -t"); Try / catch
try { validateReleaseZip(bytes); } catch (e) { if (e.message === "Invalid update file header.") throw new Error("Central-directory offsets are corrupt — rebuild the archive"); throw e; } Prevention
- Never post-process, prepend stubs to, or hand-edit finished zips
- Run `unzip -t` as a packaging gate
- Keep one zip writer for the whole artifact
- SHA-256-verify downloaded bytes before validation
When it happens
Trigger: A central-directory entry whose `offset` (read at position+42) points beyond the current central-directory position, into the central directory itself, or at bytes that are not a local file header — e.g. data-descriptor-based zips with corrupted offsets, zip-bomb constructions, or archives modified after the central directory was written.
Common situations: Appending data to a zip without rewriting the central directory; tools that strip or relocate local headers (some 'zip optimizer' utilities); a deliberately crafted archive probing the parser; truncated file where offsets were written for a longer original.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Inconsistent update file header.
- Invalid update archive length.
- Invalid update entry.
- The update entry size did not match its contents.
- Inconsistent update sizes or compression.
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/1c11940eee8171e5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:66
/** Inspect both ZIP headers before extraction: no links, traversal or bombs. */
export function validateReleaseZip(bytes) {
const minimum=Math.max(0,bytes.length-65557); let end=-1;
for(let i=bytes.length-22;i>=minimum;i--) if(bytes.readUInt32LE(i)===0x06054b50&&i+22+bytes.readUInt16LE(i+20)===bytes.length) { end=i; break; }
if(end<0||bytes.readUInt16LE(end+4)||bytes.readUInt16LE(end+6)) throw new Error("Invalid update archive.");
const count=bytes.readUInt16LE(end+10); let position=bytes.readUInt32LE(end+16),total=0;
if(!count||count>2000||bytes.readUInt16LE(end+8)!==count||position+bytes.readUInt32LE(end+12)!==end) throw new Error("Invalid update archive index.");
const seen=new Set();
for(let i=0;i<count;i++) {
if(position+46>end||bytes.readUInt32LE(position)!==0x02014b50) throw new Error("Invalid update entry.");
const flags=bytes.readUInt16LE(position+8),method=bytes.readUInt16LE(position+10),length=bytes.readUInt16LE(position+28),extra=bytes.readUInt16LE(position+30),comment=bytes.readUInt16LE(position+32);
const name=bytes.subarray(position+46,position+46+length).toString("utf8");
const kind=(bytes.readUInt32LE(position+38)>>>16)&0xf000,offset=bytes.readUInt32LE(position+42),compressed=bytes.readUInt32LE(position+20);
const size=bytes.readUInt32LE(position+24); total+=size;
if(flags&1||![0,8].includes(method)||![0,0x4000,0x8000].includes(kind)||total>512*1024*1024||position+46+length+extra+comment>end) throw new Error("Unsupported update entry.");
if(!name.startsWith(`${APP_NAME}.app/`)||name.includes("\\")||name.includes(":")||name.includes("\0")||name.split("/").some(part=>part===".."||part===".")||seen.has(name)) throw new Error("Unsafe update path.");
seen.add(name);
if(offset+30>position||bytes.readUInt32LE(offset)!==0x04034b50) throw new Error("Invalid update file header.");
const localLength=bytes.readUInt16LE(offset+26),localExtra=bytes.readUInt16LE(offset+28);
if(offset+30+localLength+localExtra+compressed>bytes.readUInt32LE(end+16)||bytes.subarray(offset+30,offset+30+localLength).toString("utf8")!==name) throw new Error("Inconsistent update file header.");
if(bytes.readUInt16LE(offset+8)!==method||bytes.readUInt16LE(offset+6)!==flags||(!(flags&8)&&(bytes.readUInt32LE(offset+18)!==compressed||bytes.readUInt32LE(offset+22)!==size))) throw new Error("Inconsistent update sizes or compression.");
const start=offset+30+localLength+localExtra;
// Header sizes are untrusted. Bound actual expansion before ditto writes
// anything, including a compressed payload whose headers understate size.
const payload=bytes.subarray(start,start+compressed);
let expanded;
try { expanded=method===0?payload.length:inflateRawSync(payload,{maxOutputLength:Math.max(size,1)}).length; }
catch { throw new Error("Invalid or oversized compressed update entry."); }
if(expanded!==size) throw new Error("The update entry size did not match its contents.");
position+=46+length+extra+comment;
}
if(position!==end) throw new Error("Invalid update archive length.");
return count;
}
export async function prepareUpdate(update) {View on GitHub (pinned to 73e0f67d83)