Skip to content

Changelog

  • 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 corruptiontmpPath 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 bugTestConcurrentWriteRACE 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 namescrypto/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