Skip to content

fix: preserve source resources during document path saves - #1148

Merged
andiwand merged 3 commits into
mainfrom
review/130-document-path-save
Oct 6, 2026
Merged

andiwand merged 3 commits into
mainfrom
review/130-document-path-save

Conversation

@andiwand

@andiwand andiwand commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Saving a ZIP-backed document over its source truncated unread resources. Path saves now write and close a temporary file beside the destination, then rename it over the destination (util::file::write_atomic, TemporaryDiskFile::persist). A failed write leaves the destination unchanged.

Lazily read packages need one more step. ReadSource opens the source again by path for each new read stream, and on Windows a rename over a file with open handles fails. So before the replace, Document::save calls abstract::Document::release_source (default: nothing). The ODF and OOXML files keep the zip::util::Archive they open and pass it to their documents. common::Document forwards the call to the archive's ReadSource, which moves onto a private temporary copy if it reads the destination (std::filesystem::equivalent, so hard links and symlinks count).

The shared path follows existing symlinks, refuses dangling ones, keeps the permissions of an existing destination and refuses one that its owner may not write, as truncation did. A new destination gets the default permissions of its directory; only temporary copies are private to the owner, because they can hold decrypted content. Stream saves keep their behaviour.

Limitations: only the saving document moves to a copy. A second document open on the same file is not protected, and a hard link to the destination keeps the old bytes after the rename.

Validation: 2397 tests pass. a_released_source_reads_from_its_own_copy truncates the source in place after the release and fails without the hook; the pooled read streams hide the problem from a plain save-over test on POSIX. The full library and CLI build passes clang-tidy 18 in an Ubuntu 24.04 container that matches the tidy job. No Windows runtime validation.

@andiwand
andiwand force-pushed the review/129-pdf-calculator-types branch from 759f921 to 17166d2 Compare October 6, 2026 14:53
Base automatically changed from review/129-pdf-calculator-types to main October 6, 2026 14:59
andiwand and others added 2 commits October 6, 2026 16:59
The owner-only mode applied to every file the temporary factory made, so
write_atomic gave a document saved to a new path mode 0600 where the old
ofstream gave 0666 minus the umask. Only temporary copies, which can hold
decrypted content, are private to their owner now. A new save gets the
permissions any new file in its directory gets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxyTMutqSUJRGfxA8CyzMc
@andiwand
andiwand force-pushed the review/130-document-path-save branch from fd99b7e to 16476bf Compare October 6, 2026 15:07
preserve_source sat on abstract::File and every filesystem, and the
document walked all of its entries to reach the one ZIP reader that can
act. The ODF and OOXML files now keep the zip::util::Archive they open
and hand it to their documents. abstract::Document::release_source does
nothing by default, and common::Document forwards it to the archive's
ReadSource, which moves onto a private copy if it reads the destination.

The reader compares the files with the error_code overload, so a source
deleted after open is no error. TemporaryDiskFile::persist renames the
written file over the destination, keeps the destination's permissions,
reports a failed rename as FileWriteError and stops owning the file, so
DiskFile::path stays protected. write_atomic refuses a destination its
owner may not write, as truncating it did before.

A new test truncates the source in place after the release, because the
pooled read streams keep replaced bytes readable on POSIX.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxyTMutqSUJRGfxA8CyzMc
@andiwand
andiwand merged commit ae9db7b into main Oct 6, 2026
23 of 25 checks passed
@andiwand
andiwand deleted the review/130-document-path-save branch October 6, 2026 16: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