Skip to content

fix(zip): write an entry's size into its local header - #755

Merged
andiwand merged 1 commit into
mainfrom
fix/zip-save-local-header-sizes
Aug 28, 2026
Merged

fix(zip): write an entry's size into its local header#755
andiwand merged 1 commit into
mainfrom
fix/zip-save-local-header-sizes

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

The report

A user edited an .odt on the tablet, saved it, copied it back to the PC, and LibreOffice 26.2 says the file is damaged and offers to repair it. (Same mail also asks about ODS editing and about typing into an empty table cell — both known, neither touched here.)

What was wrong

ZipArchive::save appends every entry through mz_zip_writer_add_read_buf_callback. That is miniz's streaming form: it writes the local header before it knows the entry's size, so crc and both sizes go out as zero and are repeated afterwards in a trailing data descriptor (general purpose bit 3).

LibreOffice tolerates that on a deflated entry — it writes such entries itself — but rejects it on a stored one. Every odf package stores mimetype, so every odt/ods/odp/odg we saved was unopenable. A docx got away with it because nothing in one is stored.

Bisected against LibreOffice by rebuilding the saved package with one framing property changed at a time: descriptors on deflated entries only → opens; descriptor on mimetype alone → "source file could not be loaded". Versions, external attributes and entry order made no difference.

The fix

Pass MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE, so miniz returns to the local header and fills in the real crc and sizes instead of emitting a descriptor. That needs the sink to honour the offset it is given, which the write callback ignored — it now seeks, relative to wherever the stream started. save therefore wants a seekable stream, noted on the declaration; util::file::create opens one, and the tests now open theirs binary.

Verified

  • The 31 odf files in the public test data, round-tripped through Document::save, all open in LibreOffice 26.2 headless. Before: Error: source file could not be loaded.
  • New ZipArchive.save_sizes_local_headers walks the saved local headers by the lengths they carry — which only terminates because they carry them — and checks bit 3 is clear and crc/size are filled. It fails on the old writer.
  • Full odr_test: 1241 passed, the 6 usual skips.

Saving went through miniz's streaming append, which leaves the crc and
both sizes to a trailing data descriptor and zeroes them in the local
header. LibreOffice rejects that on a stored entry, and every odf package
stores `mimetype` — so an odt edited and saved by the apps came back as
"the file is damaged, repair it?".

`MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE` makes miniz come back to the local
header once the entry is written, which the write sink has to follow: it
now seeks to the offset miniz names instead of appending blindly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiPhteiEmPp8iPQLBfnthA
@andiwand
andiwand force-pushed the fix/zip-save-local-header-sizes branch from a21c992 to 339c1da Compare August 28, 2026 05:16
@andiwand
andiwand merged commit db6d491 into main Aug 28, 2026
25 checks passed
@andiwand
andiwand deleted the fix/zip-save-local-header-sizes branch August 28, 2026 05:16
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