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

  1. Rebuild the archive with a standard tool (`ditto -c -k` or `zip -r`) so EOCD count/size are computed by the writer.
  2. 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.
  3. Run `zipinfo` or `unzip -t` to confirm the archive reconciles before uploading.
  4. 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

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


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)