agent_mesh_protocol/
atomic.rs1use std::fs::{self, File, OpenOptions};
10use std::io::{self, Write};
11use std::path::{Path, PathBuf};
12
13pub fn create_tmp(dir: &Path, dest: &Path) -> io::Result<(PathBuf, File)> {
19 let name = dest
20 .file_name()
21 .map_or_else(String::new, |n| n.to_string_lossy().into_owned());
22 for _ in 0..8 {
23 let tmp = dir.join(format!(".{name}.{:032x}.tmp", rand::random::<u128>()));
24 let mut o = OpenOptions::new();
25 o.write(true).create_new(true);
26 #[cfg(unix)]
27 std::os::unix::fs::OpenOptionsExt::mode(&mut o, 0o600);
28 match o.open(&tmp) {
29 Ok(f) => return Ok((tmp, f)),
30 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
31 Err(e) => return Err(e),
32 }
33 }
34 Err(io::Error::new(
35 io::ErrorKind::AlreadyExists,
36 format!("could not create a temp file in {}", dir.display()),
37 ))
38}
39
40pub fn atomic_write(dest: &Path, bytes: &[u8]) -> io::Result<()> {
47 let dir = match dest.parent() {
48 Some(d) if !d.as_os_str().is_empty() => d,
49 _ => Path::new("."),
50 };
51 let (tmp, mut f) = create_tmp(dir, dest)?;
52 let written = f
53 .write_all(bytes)
54 .and_then(|()| f.sync_all())
55 .and_then(|()| fs::rename(&tmp, dest));
56 if let Err(e) = written {
57 let _ = fs::remove_file(&tmp);
58 return Err(e);
59 }
60 #[cfg(unix)]
61 File::open(dir)?.sync_all()?;
62 Ok(())
63}
64
65#[cfg(all(test, unix))]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn replaces_the_destination_and_leaves_no_temp_file() {
71 let dir = tempfile::tempdir().unwrap();
72 let dest = dir.path().join("f.json");
73 atomic_write(&dest, b"one").unwrap();
74 atomic_write(&dest, b"two").unwrap();
75 assert_eq!(fs::read(&dest).unwrap(), b"two");
76 assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1);
77 }
78
79 #[test]
80 fn a_symlink_at_a_guessed_temp_name_is_not_followed() {
81 let dir = tempfile::tempdir().unwrap();
82 let victim = dir.path().join("victim");
83 fs::write(&victim, "precious").unwrap();
84 let dest = dir.path().join("f.json");
85 for guess in ["f.tmp", "f.json.tmp", "f.json.tmp.1"] {
86 let _ = std::os::unix::fs::symlink(&victim, dir.path().join(guess));
87 }
88 atomic_write(&dest, b"data").unwrap();
89 assert_eq!(fs::read_to_string(&victim).unwrap(), "precious");
90 }
91
92 #[test]
93 fn a_failed_rename_removes_the_temp_file() {
94 let dir = tempfile::tempdir().unwrap();
95 let dest = dir.path().join("is-a-dir");
96 fs::create_dir(&dest).unwrap();
97 fs::write(dest.join("x"), "x").unwrap();
98 assert!(atomic_write(&dest, b"data").is_err());
99 let leftovers: Vec<_> = fs::read_dir(dir.path()).unwrap().collect();
100 assert_eq!(leftovers.len(), 1, "only the directory remains");
101 }
102}