Expand description
Atomic on-disk commit for a staged save token.
crate::save::resolve_commit_with is filesystem-free; this module supplies the real source
re-read, the file-identity comparison that decides whether that re-read applies, and a
per-destination lock. The replacement itself is not done here: it is
rightkit_fs::AtomicWriter (temp sibling in the destination’s directory, flush, rename,
directory sync), which this crate does not reimplement. Path authorization is the caller’s job.
§What this crate owns, and what it does not
Owned here: the destination lock, the source re-validation, resolving a symlink destination
once, invalidating the other staged tokens, and the check described under “What a replacement
keeps” below. Owned by rightkit-fs: the temp file, the flush, the rename (on Windows the
POSIX-semantics std::fs::rename, never ReplaceFileW), the directory sync, and cleanup. A
write that fails before the rename leaves the original file as it was and removes the temp
file; there is no state in which neither version exists.
§Concurrency and aliases
A commit is “check the source, replace the destination, invalidate the siblings”. Two commits
staged against the same source must not interleave those steps - each would read the original
bytes, both would pass the check, and the later rename would silently discard the earlier edit.
So every commit takes a lock on its destination’s identity first and holds it across all three
steps; the second commit then finds its token invalidated (or the source changed) and fails with
PdfEditError::WriteConflict, the same error a source changed by someone else gives.
Identity is not the path string. /tmp/./memo.pdf, a hard link, a symlink, and (on a
case-insensitive volume) MEMO.pdf are the same file, and the source recheck must run for all of
them. [FileIdentity] is the volume and file id read from the file itself (dev/ino on Unix,
FILE_ID_INFO from an opened handle on Windows) together with the canonical path; a destination
that does not exist yet is its canonical parent directory plus its file name. Where the platform
cannot give an existing file an id, the commit is refused (fail closed) rather than compared by
path alone.
A symlink destination is resolved once: the lock, the source recheck and the replacement all
use the resolved path, so the rename replaces the file the link points at and never the link
itself. The destination is resolved again after the lock is obtained; if it now resolves
elsewhere (the link was retargeted while this commit waited) the commit fails with
PdfEditError::WriteConflict.
The lock is in-process. Another program replacing the file between the recheck and the rename is narrowed by the recheck, not excluded.
§What a replacement keeps
A replacement is a new file in the same directory, so it carries what a new file there would
carry, not what the old file had. Before any byte is written this module compares the new temp
file with the destination and refuses the save (CommitError::Refused) when a difference
it can see would weaken or change the file’s protection; the original is untouched. It sees only
what std and rightkit-fs already expose, no platform call of its own:
| Platform | Refused when | Not examined, and not kept |
|---|---|---|
| Unix | owner, group or permission bits (0o7777) of the new file differ from the destination’s | POSIX ACLs, SELinux labels, extended attributes, file flags, hard links (siblings keep the old bytes), creation time |
| macOS | as Unix; and a destination whose mode is owner-only (& 0o077 == 0) when it or the new file is not provably owner-only, which is how rightkit-fs reports an extended ACL | the ACL of a destination whose mode is wider than owner-only; extended attributes; file flags |
| Windows | the read-only attribute or the EFS-encrypted attribute is set | the destination’s DACL (a protected or explicit ACL, its owner), alternate data streams, the hidden/system/compressed attributes, creation time: the new file takes the parent directory’s inherited ACL |
“Not examined” means exactly that: such a save succeeds and the property is lost.
Enums§
- Commit
Error - Unpreserved
- What a replacement file would not keep of the file it replaces, found before anything was written. See “What a replacement keeps” in the module docs for what is, and is not, examined on each platform.
Functions§
- commit_
pdf_ save - Consumes
token, re-checks the on-disk source for same-file saves, and atomically writes the verified candidate totarget(or back to the source path whenNone). - write_
atomic - Writes
bytestotargetatomically throughrightkit_fs::AtomicWriter: a temp file in the same directory is flushed and then renamed over the target, and the directory is synced, so readers observe either the old or the new file, never a mix, and a failure before the rename leaves the old file in place.