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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use log::{debug, error};
/// Advisory file lock using a `.lock` file.
/// The lock is released when the `FileLock` is dropped.
pub struct FileLock {
lock_path: PathBuf,
#[cfg(unix)]
_file: fs::File,
}
impl FileLock {
/// Acquire an advisory lock for the given path.
/// Creates a `.purple_lock` file alongside the target and holds an `flock` on it.
/// Blocks until the lock is acquired (or returns an error on failure).
pub fn acquire(path: &Path) -> io::Result<Self> {
let mut lock_name = path.file_name().unwrap_or_default().to_os_string();
lock_name.push(".purple_lock");
let lock_path = path.with_file_name(lock_name);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(&lock_path)?;
// SAFETY: flock() is safe to call on any valid file descriptor.
// The fd comes from a File we just opened and own. LOCK_EX
// requests an exclusive advisory lock, blocking until acquired.
let ret =
unsafe { libc::flock(std::os::unix::io::AsRawFd::as_raw_fd(&file), libc::LOCK_EX) };
if ret != 0 {
return Err(io::Error::last_os_error());
}
Ok(FileLock {
lock_path,
_file: file,
})
}
#[cfg(not(unix))]
{
// On non-Unix, use a simple lock file (best-effort)
let file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
.or_else(|_| {
// If it already exists, wait briefly and retry
std::thread::sleep(std::time::Duration::from_millis(100));
fs::remove_file(&lock_path).ok();
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
})?;
Ok(FileLock {
lock_path,
_file: file,
})
}
}
}
impl Drop for FileLock {
fn drop(&mut self) {
// On Unix, flock is released when the file descriptor is closed (automatic).
// Clean up the lock file.
let _ = fs::remove_file(&self.lock_path);
}
}
/// Atomic write: write content to a PID-suffixed temp file with chmod 600, then rename.
/// Uses O_EXCL (create_new) to prevent symlink attacks on the temp file path.
/// Cleans up the temp file on failure.
pub fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
debug!("Atomic write: {}", path.display());
// Ensure parent directory exists
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut tmp_name = path.file_name().unwrap_or_default().to_os_string();
tmp_name.push(format!(".purple_tmp.{}", std::process::id()));
let tmp_path = path.with_file_name(tmp_name);
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
// Try O_EXCL first. If a stale tmp file exists from a crashed run, remove
// it and retry once. This avoids a TOCTOU gap from removing before creating.
let open = || {
fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&tmp_path)
};
let mut file = match open() {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
let _ = fs::remove_file(&tmp_path);
open().map_err(|e| {
io::Error::new(
e.kind(),
format!("Failed to create temp file {}: {}", tmp_path.display(), e),
)
})?
}
Err(e) => {
return Err(io::Error::new(
e.kind(),
format!("Failed to create temp file {}: {}", tmp_path.display(), e),
));
}
};
if let Err(e) = file.write_all(content) {
drop(file);
let _ = fs::remove_file(&tmp_path);
return Err(e);
}
if let Err(e) = file.sync_all() {
drop(file);
let _ = fs::remove_file(&tmp_path);
return Err(e);
}
}
#[cfg(not(unix))]
{
if let Err(e) = fs::write(&tmp_path, content) {
let _ = fs::remove_file(&tmp_path);
return Err(e);
}
// sync_all via reopen since fs::write doesn't return a File handle
match fs::File::open(&tmp_path) {
Ok(f) => {
if let Err(e) = f.sync_all() {
let _ = fs::remove_file(&tmp_path);
return Err(e);
}
}
Err(e) => {
let _ = fs::remove_file(&tmp_path);
return Err(e);
}
}
}
let result = fs::rename(&tmp_path, path);
if let Err(ref err) = result {
let _ = fs::remove_file(&tmp_path);
error!("[purple] Atomic write failed: {}: {err}", path.display());
}
result
}