Skip to content
- Removed
.bak two-rename pattern — the old atomicRename did path->.bak then tmp->path, opening a window where the target didn’t exist. A concurrent reader could see ENOENT, and a crash between the two renames left the target missing. The library named “atomic-write” was not atomic. Now a single os.Rename(tmp, path) is one atomic syscall on POSIX and atomically replaces on Windows via MoveFileEx.
- Concurrent writer corruption —
tmpPath was computed before the lock (path + ".tmp"), so two concurrent Write() calls to the same target both truncated and wrote the same staging file, interleaving bytes. Now each writer gets a unique temp file via crypto/rand suffix.
- Concurrency test couldn’t catch the bug —
TestConcurrentWriteRACE used identical content from all writers, so byte interleaving was undetectable. Rewritten with 10 writers using divergent content plus an integrity check.
- “Crash safety” doc claim was false — no
fsync was called anywhere. Now writeAndSync() opens the temp file, writes data, calls file.Sync(), and closes. On POSIX, the target directory is also fsync’d after rename.
fsync for crash durability — temp file is fsync’d before rename; target directory is fsync’d after rename (POSIX only)
- Unique temp file names —
crypto/rand 4-byte hex suffix prevents concurrent writer staging-file corruption
- Platform-specific rename — split into
rename_unix.go (single rename(2) + directory fsync) and rename_windows.go (retry loop on ERROR_ACCESS_DENIED/ERROR_SHARING_VIOLATION, 5 retries with exponential backoff)
writeAndSync() — opens temp file, writes data, fsyncs, closes; cleans up temp file on any error
randomSuffix() — generates random hex suffix for unique temp file names
syncDir() — opens and fsyncs the target directory after rename (POSIX only)
- Package doc: “atomic rename for crash safety” to “atomic rename, and fsync for crash durability”
Write() now stages to a unique temp file instead of a shared path + ".tmp"
atomicRename() no longer creates .bak files — single os.Rename replaces atomically
TestConcurrentWriteRACE rewritten: 10 writers, divergent content, integrity check
TestWriteCreatesBackup — tested the .bak pattern as a feature; the pattern was a bug
.bak file creation — no longer created alongside the target
- Windows rename retry loop and
FlushFileBuffers compile via build tags but are untested on real Windows hardware
- No POSIX-equivalent directory fsync on Windows
- No stale temp file cleanup — crashed writes leave
.tmp files
- LICENSE changed from PROPRIETARY to MIT — the README already stated MIT; the LICENSE file now matches
.golangci.yml now permits github.com/cespare/xxhash/v2 and github.com/gofrs/flock in the depguard allow list
- Tests use
//nolint:gosec annotations with rationale for os.ReadFile/os.WriteFile calls
TestConcurrentWriteRACE now calls t.Parallel()
- Benchmark helpers call
b.Helper() so failures are attributed to the caller
- Variable names lengthened for
varnamelen compliance
- Inline
if err blocks in tests refactored for noinlineerr compliance
errcheck: h.Write return value is now explicitly discarded in the streaming benchmark
- TOCTOU-safe file writes via xxhash64 fingerprint verification
- Cross-platform file locking (
flock on Unix, LockFileEx on Windows) via gofrs/flock
- Atomic rename with
.bak backup of previous content
Fingerprint type ([8]byte) with IsZero() and Matches() methods
FingerprintFile — computes fingerprint from file path (zero-value for nonexistent files)
FingerprintFromBytes — computes fingerprint from raw bytes
Write(path, data, fingerprint) — main API with TOCTOU protection
ErrConcurrentModification sentinel error for fingerprint mismatch detection
- File permission preservation from existing file (defaults to
0644 for new files)
- xxhash64 vs SHA-256 benchmarks demonstrating ~11x speed advantage
- Unit tests, permission preservation tests, and concurrent writer contention test