Hmbown/CodeWhale · error · Error
Invalid update archive length.
Error message
Invalid update archive length.
What it means
After processing all `count` central-directory entries, the validator requires the cursor to land exactly on the start of the end-of-central-directory record (`position===end`). Leftover unaccounted bytes between the last central-directory entry and the EOCD mean the index declared fewer/differently-sized entries than the archive contains — the archive structure doesn't reconcile, so it's rejected.
Solutions
- Rebuild the archive with a standard tool (`ditto -c -k` or `zip -r`) so EOCD count/size are computed by the writer.
- Do not prepend SFX stubs or append signatures inside the central-directory span; if a stub is required, it must be handled outside this validator's contract.
- Run `zipinfo` or `unzip -t` to confirm the archive reconciles before uploading.
- Verify the downloaded asset's SHA-256 to detect transfer corruption or tampering.
Defensive patterns
Strategy: validation
Validate before calling
const tail = bytes.subarray(Math.max(0,bytes.length-64)).toString("latin1");
if (!tail.includes("PK\x05\x06")) throw new Error("No EOCD at end"); Try / catch
try { validateReleaseZip(bytes); } catch (e) { if (e.message === "Invalid update archive length.") throw new Error("EOCD count/size don't reconcile — rebuild the archive"); throw e; } Prevention
- Let the zip writer compute EOCD fields; never edit them
- Don't concatenate, strip, or stub-augment release zips
- Run `zipinfo` before upload
- Verify asset digest on download
When it happens
Trigger: An EOCD whose entry count (at end+10) or central-directory size (at end+12) doesn't match the actual entries walked — e.g. extra bytes appended before the EOCD, a prepended self-extracting stub, or an archive where entries were removed but the count/size fields weren't updated.
Common situations: Concatenating or stripping entries from a zip with a hex editor or script without rewriting the EOCD; prepending an SFX stub to a prebuilt zip; a zip writer bug emitting padding before the EOCD; tampered update payloads.
Related errors
- Inconsistent update file header.
- Invalid update entry.
- Invalid update file header.
- 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/97b117995546a766.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:80
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) {
if(!update?.available) throw new Error("Check for an available update first.");
if(!newerVersion(update.version,APP_VERSION)||update.url!==`${repository}/releases/download/v${update.version}/Codewhale-Computer-Use-${update.version}-macos-universal.zip`||!Number.isSafeInteger(update.size)||update.size<=0||update.size>limit) throw new Error("The update identity is invalid.");
// Only GitHub's fixed release URL and its asset CDN can serve the bytes.
let url=update.url, response;
for(let redirects=0;redirects<4;redirects++) {
response=await fetch(url,{redirect:"manual",signal:AbortSignal.timeout(60_000)});
if(![301,302,303,307,308].includes(response.status)) break;
const next=new URL(response.headers.get("location"),url);
if(next.protocol!=="https:"||!["github.com","release-assets.githubusercontent.com","objects.githubusercontent.com"].includes(next.hostname)) throw new Error("The update download redirected to an unexpected host.");
url=next.href;
}
if(!response?.ok) throw new Error("The update could not be downloaded. Your current app is unchanged.");
const bytes=await responseBytes(response,update.size);
if(bytes.length!==update.size||crypto.createHash("sha256").update(bytes).digest("hex")!==update.sha256) throw new Error("The update checksum did not match. Your current app is unchanged.");View on GitHub (pinned to 73e0f67d83)