1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! 1.2.15+ Phase S.4 — atomic file write helpers.
//!
//! Standalone module for "write to disk such that a
//! crash mid-write leaves either the old contents or
//! the new, never a half-written file". Implemented
//! as the well-known temp + fsync + rename + parent-
//! dir fsync idiom.
//!
//! Used by the panic-hook rescue flush in
//! [`crate::crash`] (where the same atomicity is what
//! makes rescue buffers reliable) and by every
//! paragraph save / sidecar save in the editor.
//!
//! Before 1.2.15 those callers used `std::fs::write`
//! which truncates the target THEN writes — a power
//! loss or `kill -9` between the truncate and the
//! write would leave the user with an empty file.
//! The atomic flow writes to a side-by-side temp
//! and only swaps it in once the bytes are durably
//! on disk.
//!
//! POSIX details:
//!
//! 1. `OpenOptions::write|create|truncate` opens the
//! temp file.
//! 2. `write_all` lands the bytes in the kernel page
//! cache.
//! 3. `sync_all` flushes the file (data + metadata)
//! to the device.
//! 4. `rename` does the atomic swap.
//! 5. On Unix, `open(parent) + sync_all` durably
//! commits the directory entry pointing at the
//! new inode — without this the swap can roll
//! back on power loss. Windows: skipped (you
//! can't open a directory as a file).
//!
//! Failure at any step bubbles to the caller as a
//! `std::io::Error`. Partial state cleanup is
//! best-effort: the temp file may be left behind if
//! a step before rename fails. Doctor scan picks
//! these up as `*.tmp` orphans.
use std::io::Write;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
/// Per-process counter so concurrent writes — including two writes to
/// the same target — never share a temp path (M8 / cross-cutting).
static TMP_NONCE: AtomicU64 = AtomicU64::new(0);
/// Atomic write. See module docs for the durability
/// guarantee.
pub fn write(target: &Path, body: &[u8]) -> std::io::Result<()> {
let parent = target.parent().unwrap_or(Path::new("."));
let tmp_name = match target.file_name() {
Some(name) => {
// Unique temp name (`<name>.<pid>.<nonce>.tmp`) so two
// processes — or two concurrent in-process writes — to the
// same target can't interleave into one shared `.tmp` and
// corrupt the result. Kept ending in `.tmp` so the doctor
// scan still recognises a crash-orphaned temp.
let nonce = TMP_NONCE.fetch_add(1, Ordering::Relaxed);
let mut s = name.to_os_string();
s.push(format!(".{}.{}.tmp", std::process::id(), nonce));
s
}
None => return Err(std::io::Error::other("io_atomic: target has no file_name")),
};
let tmp = parent.join(tmp_name);
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp)?;
f.write_all(body)?;
f.sync_all()?;
drop(f);
std::fs::rename(&tmp, target)?;
#[cfg(unix)]
{
if let Ok(d) = std::fs::File::open(parent) {
let _ = d.sync_all();
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_replaces_existing_atomically() {
let dir = std::env::temp_dir().join(format!(
"io-atomic-test-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let target = dir.join("doc.txt");
// First write — creates the file.
write(&target, b"first").unwrap();
assert_eq!(std::fs::read_to_string(&target).unwrap(), "first");
// Second write — overwrites atomically.
write(&target, b"second").unwrap();
assert_eq!(std::fs::read_to_string(&target).unwrap(), "second");
// No tmp orphan.
let tmp = dir.join("doc.txt.tmp");
assert!(!tmp.exists(), "tmp file should have been renamed away");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn write_to_missing_parent_returns_error_no_panic() {
let target = std::env::temp_dir()
.join(format!("io-atomic-missing-{}", std::process::id()))
.join("subdir-that-does-not-exist")
.join("doc.txt");
let err = write(&target, b"hi").unwrap_err();
// io::ErrorKind::NotFound or PermissionDenied
// are the expected shapes; we don't pin a
// specific kind because POSIX vs Windows
// differ. What matters: no panic, error
// returned.
assert!(matches!(
err.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied
), "got {err:?}");
}
}