Hmbown/CodeWhale · error · Error
Unsupported update entry.
Error message
Unsupported update entry.
What it means
After reading each central-directory entry's metadata, validateReleaseZip enforces an allowlist: the entry must not be encrypted (general-purpose flag bit 0), its compression method must be 0 (stored) or 8 (deflate), its POSIX mode-derived kind must be a regular file or a directory (0x4000/0x8000 bits — symlinks and special files are rejected), the running total of declared uncompressed sizes must stay under 512 MB, and the variable-length name+extra+comment must stay inside the central directory. Any violation throws this error because such entries are unsafe to inflate or extract into the app bundle.
Solutions
- Inspect the zip (`unzip -v file.zip`) and remove/rebuild entries that are symlinks, encrypted, or use non-deflate compression.
- Repackage with `zip -r -X` (or ditto -c -k --sequesterRsrc --keepParent) so entries are plain files/directories with deflate compression.
- Check total uncompressed size (`unzip -l`) is under 512 MB and slim the bundle if not.
- If a symlink is intentional, replace it with a real file/copy in the packaging step — the validator will never accept symlinks.
Example fix
// before zip -r app.zip MyApp.app # preserves symlinks → 'Unsupported update entry' // after ditto -c -k --sequesterRsrc --keepParent MyApp.app app.zip # real files only, deflate
Defensive patterns
Strategy: validation
Validate before calling
const listing = execFileSync("unzip", ["-Z1", zipPath]).toString();
if (!listing.split("\n").every(n => n.startsWith(`${APP_NAME}.app/`))) throw new Error("entries outside app bundle"); Try / catch
try { validateReleaseZip(bytes); } catch (e) { if (e.message === "Unsupported update entry.") throw new Error("Archive contains symlinks/encryption/unusual compression — repackage with ditto -c -k"); throw e; } Prevention
- Package with `ditto -c -k --sequesterRsrc --keepParent` (no symlink preservation)
- Check `unzip -v` for method 0/8 only and no encryption flags
- Keep total uncompressed size under 512 MB
- Never include symlinks or special files in release bundles
When it happens
Trigger: An update zip containing an encrypted entry, a compression method other than stored/deflate (e.g. bzip2/LZMA), a symlink or device entry (common when zipping with symlinks preserved), declared sizes summing above 512 MB, or a malformed entry whose name/extra/comment lengths overflow the central directory.
Common situations: Packaging a .app bundle that contains a symlink (e.g. Frameworks pointing into Versions) — symlinks are rejected by design; switching the build to a compression method like zstd; a build accidentally zipping a huge intermediate artifact past the 512 MB budget; a crafted zip in a security test.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Inconsistent update sizes or compression.
- Inconsistent update file header.
- Invalid update archive.
- Invalid update archive index.
- Invalid update archive length.
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/d37dd1bf4a4279cd.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:63
if(!response.ok) throw new Error(`The update service is unavailable (${response.status}). Try again later.`);
return releaseUpdate(JSON.parse((await responseBytes(response,1024*1024)).toString("utf8")));
}
/** 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;View on GitHub (pinned to 73e0f67d83)