Skip to content

Read Word, Excel, PowerPoint and OpenDocument files in the sandboxed reader - #57

Merged
nicklongmore86 merged 4 commits into
mainfrom
feat/office-documents
Oct 24, 2026
Merged

nicklongmore86 merged 4 commits into
mainfrom
feat/office-documents

Conversation

@nicklongmore86

Copy link
Copy Markdown
Owner

Design PR 4 of the document-reading plan (docs/document-reader.md): Word, Excel, PowerPoint and OpenDocument support in the sandboxed document reader (worker + broker from #52), exposed through read_document and attachments.

What changed

Readers (all inside the sandboxed worker, same 30 s timeout and memory ceiling)

  • xml.ts: our own XML parser for hostile input. It refuses any DOCTYPE, so there is no XXE and no entity expansion (billion laughs). Only predefined entities and character references are decoded. Nesting depth is capped at 256 and elements per part at 4M. Names are namespace-resolved, and each reader keeps only the elements it uses.
  • package.ts: OOXML/ODF packages go through the existing guarded zip.ts (entry count, declared sizes and ratios, real inflated bytes counted). New limits.office caps sit on top: 64 MB per part and 256 MB per package, uncompressed.
  • docx.ts:
    • Output is Markdown-like text: headings (from style names, outline levels and basedOn), bullet and numbered lists from numbering.xml, tables as Markdown (spans padded, nested tables flattened), text boxes, and [Image: alt] placeholders.
    • Deleted text, field codes and mc:Fallback are dropped.
    • Positions are characters of that text, with start/next continuation cut at line breaks.
  • pptx.ts: slides in sldIdLst order, each with its title, shape text, tables, pictures and speaker notes; hidden slides are marked.
  • sheets.ts:
    • SheetJS CE 0.20.3 reads .xlsx, .xlsm, .xls and .ods. It never sees the ZIP: parts are inflated by the guarded reader and handed to parse_zip as an in-memory container.
    • Parts with a DTD are refused, and vbaProject.bin, media and embedded objects are never handed over. Macros are never run or read.
    • Each sheet is a Markdown table with column letters and row numbers (cell addresses). The default view is the sheet list plus the first 20×30 cells of each sheet.
    • A1 ranges are accepted: Sheet1!A1:H200, 'Q3 sales'!B2:F40, A:C, 5:20, A21:H.
    • Each call is capped at 10,000 cells and the text budget, and names the next range to read.
    • Cell values are cached values; include_formulas adds the formula text. sheetRows bounds parsing.
    • CSV/TSV is read by name with our own RFC 4180 parser.
  • odf.ts: .odt reads like Word and .odp like PowerPoint; .ods goes through SheetJS.
  • ole.ts:
    • .xls goes to SheetJS.
    • .doc gets best-effort text from the piece table (8-bit and UTF-16 pieces, field instructions dropped).
    • .ppt, .xlsb and Word 95 get a clear "save it as .pptx/.xlsx/.docx" message.
    • An encrypted OOXML package is reported as password-protected.
  • reply.ts checks the new shapes:
    • new units: slide (treated like page) and sheet;
    • meta.sheets: at most 200, consistent with the position;
    • section.sheet ranges must be A1 references (regex) and sheet names at most 200 characters;
    • sheet data is allowed only on workbooks.

Tool and attachments

  • read_document takes new sheet, range and include_formulas arguments. These are exclusive with pages, start and region, and recorded in the audit entry.
  • Output gains slide headers, a sheet list, per-sheet tables and lines naming where to continue. The "not supported yet" message is gone for these types.
  • Office/ODF attachments, plus Word and Excel 97-2003, are matched by READABLE_DOCUMENT_MIMES. They are sent as fenced untrusted text, with no images, through the existing byte-bounded cache (cachedPdfReading, which now takes a MIME type).
  • The chip rule is updated: canReceive(kind, vision, mime). Older PowerPoint, RTF and mail are still greyed.

Dependencies added

  • xlsx 0.20.3 (SheetJS Community Edition), Apache-2.0:
    • Installed from the official tarball https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz, not the outdated npm package.
    • Pinned by version in package.json and by its sha512 integrity in pnpm-lock.yaml; I checked the hash against the downloaded tarball.
    • It is a devDependency, bundled into the worker only. It adds about 500 KB minified (about 160 KB gzipped) to out/document-worker/assets/index.js.
    • Its LICENSE ships in out/document-worker/licenses/sheetjs-LICENSE.txt, and THIRD-PARTY-NOTICES.md is updated.
    • The bundle has no eval or new Function, so the worker's CSP is unchanged.

How I verified

  • pnpm typecheck, pnpm lint, pnpm format:check: clean.
  • pnpm test: 2865 test cases passed (34 skipped) across 133 test files. New coverage:
    • xml.test.ts and office.test.ts: every format from real files built in memory (workbooks written by SheetJS; Word, PowerPoint and ODF from their XML parts; .doc and .ppt as OLE files), plus ranges, cell caps and continuation.
    • Hostile files in office.test.ts: zip bombs by ratio and by a lying entry, XXE, billion laughs, a DTD in a part handed to SheetJS, oversized shared strings, tables nested past the depth cap, too many elements. Every view is also put through checkReply.
    • Plus Office cases in reply.test.ts, read-document.test.ts, attachments.test.ts and the renderer chip tests.
  • pnpm build, then xvfb-run -a pnpm test:e2e: all 132 passed. e2e/document-worker.spec.ts covers every format and the hostile Office corpus inside the real sandboxed worker. e2e/read-document.spec.ts covers Word, a workbook listing followed by a range, and slide-by-slide continuation through the tool.
  • I also ran the readers locally against files produced by python-docx, python-pptx and openpyxl. Those files are not committed.

🤖 Generated with Claude Code

…reader

Design PR 4 of the document-reading plan (docs/document-reader.md).

- Word (.docx) as Markdown-like text: headings, lists, tables, image alt
  text; character positions with start/next. Word 97-2003 (.doc): best-effort
  text from the piece table.
- Excel (.xlsx, .xlsm, .xls), OpenDocument spreadsheets and CSV: per-sheet
  tables with cell addresses, a sheet list and preview by default, A1 ranges
  ("Sheet1!A1:H200"), cell and character caps with the next range named,
  cached values with optional formula text. SheetJS CE 0.20.3 from
  cdn.sheetjs.com, pinned by integrity in the lockfile; it never sees the ZIP
  (parts are inflated by the guarded reader) and never gets macro projects.
- PowerPoint (.pptx) and OpenDocument presentations slide by slide with
  speaker notes; .ppt, .xlsb and Word 95 get a "save as" message.
- OpenDocument text (.odt) like Word.
- A hardened XML parser: DTDs refused (no XXE, no billion laughs), depth and
  element caps, namespace-resolved names. Office part and package size caps
  on top of the zip-bomb limits. reply.ts checks the new shapes.
- read_document takes sheet, range and include_formulas; Office attachments
  are sent as text through the byte-bounded cache, and their chips are no
  longer greyed.

Co-authored-by: omnigent <noreply@omnigent.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicklongmore86 nicklongmore86 added the windows-ci Run Windows CI on this PR label Oct 24, 2026
nicklongmore86 and others added 3 commits October 24, 2026 18:45
…ck OLE files

- sheets.ts / unsaved.ts: a formula cell saved without a result (openpyxl-style
  <f> with no <v>, an empty <v/>, a t="str" formula with no <v>, an OpenDocument
  formula with no value, or a SheetJS stub that carries a formula) renders as
  "=SUM(A1:A3) (no saved value)" with a note to recalculate in Excel and save,
  never as a number that was not stored. A cached 0 still shows as 0.
- reply.ts: checkView parses every sheet range (used, shown, next) against
  Excel's 1,048,576 x 16,384 limits, requires shown inside used, and caps the
  cells the sections claim to show at office.maxCellsPerCall, in the broker and
  again in main.
- cfb.ts: the OLE header, DIFAT, FAT, every FAT chain SheetJS walks up front,
  the directory (entry cap, link trees), mini FAT, mini stream and stream chains
  are checked before SheetJS's CFB reader sees the file, with each sector claimed
  once; loops, overlaps, truncation and tangled chains fail as 'malformed',
  streams declared larger than the file as 'zip-bomb'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Co-authored-by: omnigent <noreply@omnigent.ai>
The raw-XML scan for formulas saved without a value now stores bands of
rows holding sorted runs of columns: an OpenDocument cell repeated over
rows and columns is one rectangle, equal adjacent bands merge (a
filled-down column is one band), and a lookup is two binary searches.
Work and memory are linear in the part's length whatever its repeat
attributes claim; repeats are clamped to the sheet. The 100,000-cell cap
is gone. Past MAX_RUNS stored runs, cells out of order, or two tables of
one name, the sheet is marked unchecked and its formulas are shown as
formulas with a warning, never as values that could not be vouched for.

Tests: 150,000 unsaved xlsx shared formulas and 600,000 repeated ODS
ones read at the end of the sheet; a hostile ODS with 3,000 rows of
1,048,576-row / 16,384+-column repeats reads in well under 5 s; the
unchecked fallback; an .xls fixture with patched FORMULA records whose
cached results are placeholders, asserting no 0 is shown.

Co-authored-by: omnigent <noreply@omnigent.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…holders

Writers that do not calculate (such as xlwt) store 0 as a BIFF8 formula's
result, which cannot be told from a computed 0. An .xls formula cell whose
cached result is numeric 0 now shows as `0 (=formula; may be unsaved)`, and
the view notes it once per sheet. Non-zero results and .xlsx/.ods are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: omnigent <noreply@omnigent.ai>
@nicklongmore86
nicklongmore86 merged commit 7e5c71a into main Oct 24, 2026
4 checks passed
@nicklongmore86
nicklongmore86 deleted the feat/office-documents branch October 24, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

windows-ci Run Windows CI on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant