uira_core/protocol/primitives/
atomic_write.rs1use std::fs::{self, File};
7use std::io::{self, Write};
8use std::path::Path;
9
10use tempfile::NamedTempFile;
11
12#[cfg(unix)]
13use std::os::unix::fs::PermissionsExt;
14
15pub fn atomic_write(path: &Path, content: &[u8], mode: Option<u32>) -> io::Result<()> {
34 let parent = path.parent().ok_or_else(|| {
35 io::Error::new(io::ErrorKind::InvalidInput, "Path has no parent directory")
36 })?;
37
38 fs::create_dir_all(parent)?;
40
41 let mut temp_file = NamedTempFile::new_in(parent)?;
44
45 temp_file.write_all(content)?;
47
48 temp_file.as_file().sync_all()?;
50
51 #[cfg(unix)]
53 {
54 let m = mode.unwrap_or(0o644);
55 let perms = std::fs::Permissions::from_mode(m);
56 temp_file.as_file().set_permissions(perms)?;
57 }
58
59 #[cfg(not(unix))]
61 let _ = mode;
62
63 temp_file.persist(path).map_err(|e| e.error)?;
66
67 #[cfg(unix)]
70 {
71 if let Ok(dir) = File::open(parent) {
72 let _ = dir.sync_all();
73 }
74 }
75
76 Ok(())
77}
78
79pub fn atomic_write_secure(path: &Path, content: &[u8]) -> io::Result<()> {
85 atomic_write(path, content, Some(0o600))
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use std::fs;
92 use tempfile::tempdir;
93
94 #[test]
95 fn test_atomic_write_creates_file() {
96 let dir = tempdir().unwrap();
97 let path = dir.path().join("test.txt");
98
99 atomic_write(&path, b"hello world", None).unwrap();
100
101 assert_eq!(fs::read_to_string(&path).unwrap(), "hello world");
102 }
103
104 #[test]
105 fn test_atomic_write_overwrites_existing() {
106 let dir = tempdir().unwrap();
107 let path = dir.path().join("test.txt");
108
109 atomic_write(&path, b"first", None).unwrap();
110 atomic_write(&path, b"second", None).unwrap();
111
112 assert_eq!(fs::read_to_string(&path).unwrap(), "second");
113 }
114
115 #[test]
116 fn test_atomic_write_creates_parent_dirs() {
117 let dir = tempdir().unwrap();
118 let path = dir.path().join("nested/dir/test.txt");
119
120 atomic_write(&path, b"nested content", None).unwrap();
121
122 assert_eq!(fs::read_to_string(&path).unwrap(), "nested content");
123 }
124
125 #[cfg(unix)]
126 #[test]
127 fn test_atomic_write_secure_permissions() {
128 use std::os::unix::fs::PermissionsExt;
129
130 let dir = tempdir().unwrap();
131 let path = dir.path().join("secret.txt");
132
133 atomic_write_secure(&path, b"secret").unwrap();
134
135 let perms = fs::metadata(&path).unwrap().permissions();
136 assert_eq!(perms.mode() & 0o777, 0o600);
137 }
138
139 #[test]
140 fn test_no_temp_file_left_on_success() {
141 let dir = tempdir().unwrap();
142 let path = dir.path().join("test.txt");
143
144 atomic_write(&path, b"content", None).unwrap();
145
146 let entries: Vec<_> = fs::read_dir(dir.path()).unwrap().collect();
148 assert_eq!(entries.len(), 1);
149 assert_eq!(entries[0].as_ref().unwrap().file_name(), "test.txt");
150 }
151
152 #[test]
153 fn test_no_temp_file_left_on_write_error() {
154 let dir = tempdir().unwrap();
155 let temp = NamedTempFile::new_in(dir.path()).unwrap();
160 let temp_path = temp.path().to_path_buf();
161 assert!(temp_path.exists());
162
163 drop(temp);
165 assert!(!temp_path.exists());
166 }
167
168 #[test]
169 fn test_concurrent_writes_no_collision() {
170 use std::sync::Arc;
171 use std::thread;
172
173 let dir = tempdir().unwrap();
174 let dir_path = Arc::new(dir.path().to_path_buf());
175
176 let handles: Vec<_> = (0..10)
178 .map(|i| {
179 let dir = Arc::clone(&dir_path);
180 thread::spawn(move || {
181 let path = dir.join(format!("file_{}.txt", i));
182 atomic_write(&path, format!("content_{}", i).as_bytes(), None).unwrap();
183 })
184 })
185 .collect();
186
187 for handle in handles {
188 handle.join().unwrap();
189 }
190
191 for i in 0..10 {
193 let path = dir.path().join(format!("file_{}.txt", i));
194 assert_eq!(fs::read_to_string(&path).unwrap(), format!("content_{}", i));
195 }
196
197 let entries: Vec<_> = fs::read_dir(dir.path())
199 .unwrap()
200 .filter_map(|e| e.ok())
201 .filter(|e| e.file_name().to_string_lossy().starts_with('.'))
202 .collect();
203 assert!(entries.is_empty(), "Temp files left behind: {:?}", entries);
204 }
205}