Skip to content

fix(xlsx): write DIFAT sectors so encrypted workbooks over ~7 MB can be read back - #619

Open
dmythro wants to merge 1 commit into
productdevbook:mainfrom
dmythro:fix/cfb-difat
Open

dmythro wants to merge 1 commit into
productdevbook:mainfrom
dmythro:fix/cfb-difat

Conversation

@dmythro

@dmythro dmythro commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

A password-protected workbook over about 7 MB is written broken. writeXlsx with encryption produced an 8.6 MB container (80,000 cells of random text), and readXlsx(enc, { password }) on that same file failed with DecryptionError: Not a valid encrypted workbook container. The cause is writeCfb: a CFB header holds only 109 FAT sector locations, which map 109 × 128 sectors of 512 bytes, just under 7 MB. A larger container must list the rest of its FAT in DIFAT sectors, and the writer never wrote any: it set "number of DIFAT sectors" to 0 and stopped at the header's 109. Every sector past the first ~7 MB was unreachable, so the 8 MB and 16 MB streams the new tests write both read back at exactly 7,143,936 bytes.

The tests did not catch it because the largest stream any of them wrote was 20 KB. readCfb already follows DIFAT chains; it just never met a container from writeCfb that had one.

What landed: writeCfb sizes the FAT and DIFAT together, since the FAT must also cover the DIFAT sectors, which it marks DIFSECT. It writes the DIFAT sectors after the FAT: 127 FAT sector locations each, then the next DIFAT sector's location, ENDOFCHAIN on the last and FREESECT padding. The header now points at the first one (offset 68) and gives the count (offset 72). A container that fits in 109 FAT sectors is written exactly as before.

Tests: two writeCfb → readCfb round trips that need one and two chained DIFAT sectors (8 and 16 MB), checking every byte and the header's DIFAT count. Plus a writeXlsx → readXlsx round trip of an encrypted workbook whose package is over 7 MB (~1 s). All three fail against main: the first two read back 7,143,936 bytes, and the third throws the DecryptionError above. bun run test is green.

Beyond hucre's own reader, I checked the containers with a separate stdlib-only Python parser written to [MS-CFB] at 1, 8, 16 and 24 MiB (0 to 3 DIFAT sectors). The header and DIFAT chain agree, FAT and DIFAT sectors carry their markers, and the stream bytes are identical.

Not verified: I did not open a large encrypted workbook in Excel or LibreOffice, so the claim rests on the spec, hucre's own reader and the separate parser. Only 512-byte sectors (version 3) are written, as before.

Summary by CodeRabbit

  • Bug Fixes

    • Improved support for large encrypted workbooks and container files exceeding previous size limits.
    • Ensured large files preserve their contents correctly during encryption and decryption.
  • Tests

    • Added regression coverage for large files requiring additional storage sectors.
    • Added round-trip validation for encrypted workbooks containing substantial amounts of data.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

📝 Walkthrough

The CFB writer now allocates and writes DIFAT sectors when the FAT exceeds the header’s 109-entry capacity. Regression tests cover large CFB streams and encrypted XLSX packages.

Changes: CFB DIFAT support

Layer / File(s) Summary
DIFAT allocation and serialization
src/xlsx/crypto/cfb.ts
The writer calculates and allocates FAT and DIFAT sectors. It records DIFAT metadata in the header and writes FAT-sector locations and links into DIFAT sectors.
Large stream regression tests
test/encryption.test.ts
Tests verify exact round trips for 8 MiB and 16 MiB CFB streams, and an encrypted XLSX round trip for a generated package larger than 7,500,000 bytes.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: productdevbook

Merge Risk: ⚪ Minimal · up to fef06

The large-container tests exercise the changed behavior, and no current failure has been established. The change is ready to merge after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarizes the main change: adding DIFAT sector support so encrypted XLSX workbooks larger than approximately 7 MB can be read back.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔇 Additional comments: 6
src/xlsx/crypto/cfb.ts: 4

279-301: LGTM!


317-317: LGTM!


400-403: LGTM!


421-438: LGTM!

test/encryption.test.ts: 2

126-141: LGTM!


58-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Use the Uint8Array view bounds in the header check.

new DataView(file.buffer) reads the whole underlying buffer, not the file view. writeCfb allocates its own buffer, so this works now. The check breaks if writeCfb later returns a subarray. Pass file.byteOffset and file.byteLength to keep the test robust.

Proposed fix
-    expect(new DataView(file.buffer).getUint32(72, true)).toBe(difatSectors)
+    expect(new DataView(file.buffer, file.byteOffset, file.byteLength).getUint32(72, true)).toBe(
+      difatSectors,
+    )

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0285bb39-b64b-4859-b342-8593be6af3e6
📥 Commits

Reviewing files that changed from the base of the PR and between c991ad0 and fef068f.

📒 Files selected for processing: 2
  • src/xlsx/crypto/cfb.ts
  • test/encryption.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant