Skip to content

perf(odt): stream unprocessed package entries, bound XML part sizes, truncate seekable outputs - #221

Merged
vaceslav merged 1 commit into
mainfrom
perf/odt-package-streaming
Sep 26, 2026
Merged

vaceslav merged 1 commit into
mainfrom
perf/odt-package-streaming

Conversation

@vaceslav

Copy link
Copy Markdown
Contributor

Problem

Review finding #4 (MEDIUM): OdtPackage.Open inflated every ZIP entry into a byte[], and Save built a second full copy in a MemoryStream, with no bound on decompression. A 100 KB template with a 96 MB zero-filled Pictures/big.bin allocated about 353 MB.

Review finding #2 (code part): Save never truncated a seekable output. Writing into an existing, longer file opened with File.OpenWrite left the old trailing bytes behind, and the file could not be read back.

Fix

  • OdtPackage now inflates only mimetype, content.xml, styles.xml, meta.xml and META-INF/manifest.xml. All other entries are streamed at save time from the source archive entry into the output entry, so memory is bounded by the copy buffer. A non-seekable template is buffered once, in compressed form.
  • Sanity limits: each loaded XML part may be at most 256 MB uncompressed. Both the declared size and the actual inflated bytes are checked. A package may have at most 65,535 entries. Exceeding a limit gives a failed result: Invalid document: content.xml exceeds the maximum supported size (256 MB uncompressed).
  • The output is still built in memory, but only in compressed form. This keeps the existing guarantees: nothing is written on failure; mimetype comes first, stored, with no extra field and no data descriptor; .ott becomes .odt; signatures are removed; output is UTF-8 without BOM.
  • Save cuts off a seekable output after the written package (SetLength(Position)).
  • DOCX check: DocumentTemplateProcessor rejects File.OpenWrite (write-only) up front. With an existing, longer file opened ReadWrite without truncation (FileMode.OpenOrCreate), the trailing bytes made the package unreadable, and processing failed with "File contains corrupted data". It now truncates the output after the template copy. This is internal, and it only turns a failing case into a working one.
  • Docs: new "Memory Use" section in docs/for-developers/opendocument.md, plus the spec and the XML doc of the output stream parameter.

Tests

  • Allocations, measured on net10 with the review probe (100 KB template, 96 MB entry, GC.GetAllocatedBytesForCurrentThread): before 369,863,784 bytes (~353 MB), after 1,361,352 bytes (~1.3 MB).
  • OdtPackageTests:
    • Process_LargeUnprocessedEntry_IsStreamedWithoutInflatingIntoMemory and Process_NonSeekableTemplateWithLargeEntry_BuffersOnlyCompressedData: a 64 MB entry must allocate less than 1/4 of its size; the entry is copied intact and the output stays compressed.
    • Open_XmlPartLargerThanLimit_ThrowsInvalidPackage
    • Process_ExistingLongerOutputFile_IsTruncated: the review probe Docs_FileOpenWrite_OnExistingLargerFile_LeavesTrailingBytes, flipped.
    • Process_SeekableOutputWithPrefix_KeepsPrefixAndCutsOffTheRest
  • StreamValidationTests.ProcessTemplate_ExistingLongerOutputFile_IsTruncated (DOCX).
  • Release build with -p:ContinuousIntegrationBuild=true; all tests pass on net10/net9/net8 (2,097 each), including the LibreOffice-trait tests run locally (7/7). dotnet format --verify-no-changes and dotnet pack pass.

Public API impact

None. No public signatures changed. Behavior change: a seekable output stream is truncated after the written document, for ODT and for DOCX. Before, the cases this affects left a corrupt file (ODT) or failed (DOCX).

…em, bound XML part sizes, truncate seekable outputs (#138)

- OdtPackage inflates only mimetype, content.xml, styles.xml, meta.xml and the manifest
  (each at most 256 MB uncompressed, at most 65,535 entries); all other entries are
  streamed from the source archive at save time. A 100 KB template with a 96 MB entry
  allocated ~353 MB before and ~1.3 MB now.
- Save cuts off a seekable output after the written package (File.OpenWrite on a longer
  existing file no longer leaves trailing bytes).
- DocumentTemplateProcessor truncates the output after copying the template, so an
  existing longer file opened with FileMode.OpenOrCreate no longer fails as corrupted.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 86.66667% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
TriasDev.Templify/OpenDocument/OdtPackage.cs 86.36% 7 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

@vaceslav
vaceslav merged commit 0d678cb into main Sep 26, 2026
12 checks passed
@vaceslav
vaceslav deleted the perf/odt-package-streaming branch September 26, 2026 14:48
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.

2 participants