Documentation
¶
Overview ¶
Package safewrite is the single way this tool replaces a file on disk.
Every write -- the target's new content and the .bak holding its prior content alike -- is staged in a temp file beside the target, fsynced, optionally validated, and only then renamed into place, so a crash or an error partway through can never leave a truncated or half-written file. The containing directory is fsynced after the rename where the platform supports it, so the rename itself is durable and not just the bytes behind it. A target that already exists with different bytes is refused unless the caller passes Force -- a tool that authors files into a user's repository must not silently destroy the user's edits. Paths are confined to a caller-supplied root with symlinks resolved, so a symlink planted in a project can never redirect a write outside it.
CANARY: REQ=ENG-4330; FEATURE="SafeWrite"; ASPECT=Storage; STATUS=TESTED; TEST=TestWriteNewFile,TestWriteIdenticalNoop,TestWriteRefusesWithoutForce,TestWriteForceBacksUpAndPreservesMode,TestWriteFailureLeavesOriginal,TestConfineRejectsSymlinkEscape,TestConfineAcceptsPathInsideRoot; UPDATED=2026-08-30
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrWouldReplace reports that the target exists with different content // and Force was not set. Callers typically print a notice and skip. ErrWouldReplace = errors.New("target exists with different content; pass force") // ErrRootEscape reports that the target, with symlinks resolved, lies // outside the confinement root. ErrRootEscape = errors.New("path resolves outside root") )
Functions ¶
func Confine ¶
Confine resolves symlinks in path and its parents and returns the resolved path, or ErrRootEscape when it does not lie within the resolved root. Components that do not exist yet are appended verbatim: a file about to be created cannot be resolved, but every existing directory leading to it can.
Note the ordering: filepath.Abs cleans the path lexically -- collapsing "." and ".." textually -- before any symlink is resolved. So "root/link/../x" is evaluated as "root/x", not as "<link's target>/../x". That is strictly more restrictive than resolving ".." against the link's real parent: it can reject a path that a symlink-first walk would have allowed inside the root, but it can never let one out of it, which is the only direction that matters for confinement.
Types ¶
type Options ¶
type Options struct {
// Root confines the write. Required: an unset root is an error rather
// than an implicit "anywhere".
Root string
// Force allows replacing an existing file whose bytes differ.
Force bool
// Backup writes the prior bytes to <path>.bak before replacing.
Backup bool
// Validate, when set, inspects the staged bytes before the rename. A
// non-nil error aborts the write with the original file untouched.
Validate func([]byte) error
}
Options configures Write.
type Result ¶
type Result struct {
// Written is false when the target already held exactly these bytes.
Written bool
// Replaced is true when an existing file's content was replaced.
Replaced bool
// BackupPath is the .bak file written, when Options.Backup asked for one.
BackupPath string
// Diff summarizes old-to-new line changes. It is set whenever an
// existing file differs -- including on the ErrWouldReplace refusal, so
// a caller can show what it declined to overwrite.
Diff string
}
Result describes what Write did.