Conversation
The Open XML SDK models a part as its root element, so the comments and processing instructions between the XML declaration and that element are not part of the tree and are gone the first time the part is re-serialized. The declaration itself survives (the SDK re-emits it), so the save is silent: the part stays well-formed, the command exits 0, and `raw` returns the root element only. Capture each part's prolog from the package as opened, and re-attach it to the written zip in both places that write the file: the mid-session flush and the close-time atomic write (next to the existing whole-part and self-closing rewrites). A package whose parts have the usual declaration-then-root shape captures nothing, so those saves are byte-for-byte what they were. Refs iOfficeAI#409 (docx; xlsx/pptx still drop it).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
WordHandlerre-serializes each part through the Open XML SDK on every save. The SDK models a part as its root element, and a prolog — the comments and processing instructions between the XML declaration and that element — is not part of that tree, so the first write drops it. The declaration itself survives, because the SDK re-emits it, and that is what makes this silent: the saved part is still well-formed, the command still exits 0, andrawreturns the root element only, so nothing on the read surface mentions what was lost.Measured on a build of
mainbefore this change, one ordinary edit each,rc=0in every row:word/document.xmlword/styles.xmlThe declaration is byte-identical across the edit, so the diff is exactly the prolog — both a comment and a
<?xml-stylesheet?>PI in one write:What changed
Two files. A new
Core/XmlPrologPreserver.cs, and three lines inWordHandler:Capturereads the prolog out of every XML part of the package as opened;Restorere-attaches it to the written zip. Both share one scanner for the declaration / misc / root shape, andRestoreskips any entry that still has a prolog of its own, so a part the SDK copied through verbatim is never given a second copy.Save) and the close-timeAtomicWriteBack, next to the existingFlushPendingWholeParts/NormalizeSelfClosingInDocxrewrites.Why the mid-session path needed its own hook — measured, not assumed: on the unpatched build a resident
setfollowed bysave(no close) loses the prolog, andsaveis a persist path a caller can stop at. The neighbouring rewrites are deliberately skipped there on the understanding that they need the file unlocked; the post-process actually runs against the temp file before the swap (AtomicPackageWriter.Flushwrites the temp, callspostProcessTemp, releases the lock, thenFile.Replace), so it is safe at save time too. I wired the restore that way and left the neighbouring steps alone — that is a separate concern and this PR should stay one.Rule 1 self-check
Asked of this diff — can it be decomposed into PRs that could be merged or reverted independently? — no. The helper has no consumer until the hooks land, so alone it would be dead code; the hooks alone would not compile against it. One root cause, one fix. The format axis is genuinely separable and is deliberately left out — see Scope.
Validation
verify409.py— unpatched build: exit 1, 5 checks fail. Patched build: exit 0, all pass. It asserts the prolog is byte-for-byte part of the document header after an edit, survives a mid-sessionsavetoo, that a part with no prolog is untouched, and that the edit still lands andvalidatestays clean:Is the fix inert where there is no prolog?
Six fixtures, built and edited with both binaries, comparing the bytes of every
*.xml/*.relspart. The differing set for the two no-prolog fixtures is exactly the timestamp carriers (docProps/core.xml, the audit stamp,.rels) — which is also the noise floor measured by running the same binary twice:word/document.xmlsha256docx-no-prolog_rels/.rels,docProps/core.xml,docProps/custom.xml,word/_rels/document.xml.rels— = the timestamp noise floor78a3527d8ef86851identical on all three runsdocx-ws-only-prolog78a3527d8ef86851identicaldocx-prologword/document.xml78a3527d8ef86851→df06967112416e45(the prolog)docx-styles-prologword/styles.xml;document.xmluntoucheda9cd7030e0ed85e8, unchangedAnd no duplication, on any part
A part the SDK never models is copied through with its prolog intact; if
Restorefailed to notice, it would insert a second copy. Injecting the same prolog into every XML part of a docx and then editing it leaves all 11 parts at exactly one copy,validateclean:Surrounding surfaces, patched vs unpatched
Same commands on both binaries; every field identical except the one this PR targets:
raw <f> /word/document.xmlstill element-only, prolog hiddendump→batchround trip (13 items)batchon a prolog-bearing fileget+validate) leaves the file's bytes untouchedScope
Docx only — #409 stays open after this. It fixes docx, and it is one format because Rule 1 asks for independently mergeable pieces: xlsx and pptx still drop the prolog on this branch (measured with the same harness —
xl/worksheets/sheet1.xml1→0,ppt/slides/slide1.xml1→0, bothrc=0). The helper is format-agnostic, so each follow-up is a capture call in that handler's open path and a restore call in its write path; I would rather land one format at a time than one PR that has to be reverted across three. Say the word if you would prefer them all in one PR instead.Not in scope
WorksheetBloatFilterdrops them) and its internal subset needs real parsing to skip safely. The scan stops there, so comments/PIs after a DOCTYPE are not restored either; the part is then left exactly as the SDK wrote it.Refs #409.