different-ai/openwork · error · Error

ZIP data for ${entry.name} is out of bounds.

Error message

ZIP data for ${entry.name} is out of bounds.

What it means

Thrown while decompressing a ZIP local file entry inside an Office (docx/xlsx/pptx) attachment: the computed end of the entry's compressed data (dataStart + entry.compressedSize) exceeds the buffer's byte length. The parser validates every entry against the central directory, so this means the local record claims more data than the file physically contains — a truncated or corrupted ZIP.

Source

Thrown at apps/server/src/opencode-plugins/openwork-office-attachments.ts:326

}

function readZipEntryData(buffer: Buffer, entry: ZipEntry): Buffer {
  const cursor = entry.localOffset;
  if (cursor + 30 > buffer.byteLength || buffer.readUInt32LE(cursor) !== ZIP_LOCAL_FILE_HEADER) throw new Error(`Invalid local ZIP header for ${entry.name}.`);
  const localFlags = buffer.readUInt16LE(cursor + 6);
  const localMethod = buffer.readUInt16LE(cursor + 8);
  const localCompressedSize = buffer.readUInt32LE(cursor + 18);
  const localUncompressedSize = buffer.readUInt32LE(cursor + 22);
  const nameLength = buffer.readUInt16LE(cursor + 26);
  const extraLength = buffer.readUInt16LE(cursor + 28);
  rejectUnsafeZipFlags(localFlags, entry.name);
  if (localMethod !== entry.method) throw new Error(`ZIP method mismatch for ${entry.name}.`);
  if (localCompressedSize !== entry.compressedSize || localUncompressedSize !== entry.uncompressedSize) throw new Error(`ZIP size mismatch for ${entry.name}.`);
  if (cursor + 30 + nameLength + extraLength > buffer.byteLength) throw new Error(`ZIP local header for ${entry.name} is out of bounds.`);
  const localName = buffer.toString("utf8", cursor + 30, cursor + 30 + nameLength);
  if (localName !== entry.name) throw new Error(`ZIP local header name mismatch for ${entry.name}.`);
  const dataStart = cursor + 30 + nameLength + extraLength;
  if (dataStart + entry.compressedSize > buffer.byteLength) throw new Error(`ZIP data for ${entry.name} is out of bounds.`);
  const compressed = buffer.subarray(dataStart, dataStart + entry.compressedSize);
  const data = entry.method === ZIP_STORED ? compressed : inflateRawSync(compressed);
  if (data.byteLength !== entry.uncompressedSize) throw new Error(`ZIP uncompressed size mismatch for ${entry.name}.`);
  return data;
}

function relevantXmlEntry(kind: OfficeKind, name: string): boolean {
  if (!name.endsWith(".xml")) return false;
  if (kind === "docx") {
    return name === "word/document.xml"
      || /^word\/header\d+\.xml$/.test(name)
      || /^word\/footer\d+\.xml$/.test(name)
      || name === "word/footnotes.xml"
      || name === "word/endnotes.xml"
      || name === "word/comments.xml";
  }
  return /^ppt\/slides\/slide\d+\.xml$/.test(name) || /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(name);
}

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Re-upload or re-download the Office file; verify byte size matches the original.
  2. Check that the caller passes the complete file Buffer, not a truncated slice.
  3. Remove any transfer that could mangle bytes (e.g., text-mode FTP, bad base64 decode) and retest.
  4. If files are intentionally larger than limits, confirm MAX_ENTRY_UNCOMPRESSED_BYTES/limits are not causing upstream truncation.

Example fix

// before: passing a possibly truncated slice
const data = readZipEntryData(buffer.subarray(0, 1000), entry);
// after: pass the full original buffer
const data = readZipEntryData(fullBytes, entry);
Defensive patterns

Strategy: validation

Validate before calling

import { stat } from "node:fs/promises";
async function zipLooksComplete(filePath: string): Promise<boolean> {
  const buf = await readFile(filePath);
  // EOCD signature "PK\x05\x06" must appear in the final 64KB
  const eocd = buf.subarray(Math.max(0, buf.byteLength - 65558)).lastIndexOf(Buffer.from("PK0506", "hex"));
  return eocd !== -1;
}

Type guard

function isTruncated(buf: Buffer, declaredEnd: number): boolean {
  return declaredEnd > buf.byteLength; // true => refuse to parse
}

Try / catch

try {
  const text = extractOfficeText(kind, bytes);
} catch (err) {
  if (err instanceof Error && err.message.includes("is out of bounds")) {
    throw new ApiError(400, "corrupt_attachment", "Attachment archive is truncated or corrupted; please re-upload.");
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling extractOfficeText (via the office-attachments plugin) on a Buffer whose local header's data region extends past the end of the file: truncated download/upload, byte-sliced buffer, or central-directory sizes disagreeing with a locally re-parsed truncated file.

Common situations: Partially uploaded or interrupted transfers of docx/xlsx files stored via the attachments plugin; files corrupted by text-mode transfer; a caller passing a truncated slice of the original buffer.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/5b2a7a6779f53137. Report an issue: GitHub.