1use std::fs::OpenOptions;
7use std::io::{ErrorKind, Write as _};
8use std::os::unix::fs::OpenOptionsExt as _;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicUsize, Ordering};
11
12use anyhow::{Context, Result, bail};
13
14pub fn write(what: &str, suffix: &str, bytes: &[u8]) -> Result<PathBuf> {
17 static COUNTER: AtomicUsize = AtomicUsize::new(0);
18 for _ in 0..100 {
19 let nanos = std::time::SystemTime::now()
22 .duration_since(std::time::UNIX_EPOCH)
23 .map_or(0, |d| d.subsec_nanos());
24 let path = std::env::temp_dir().join(format!(
25 "rmut-{what}-{}-{}-{nanos:08x}{suffix}",
26 std::process::id(),
27 COUNTER.fetch_add(1, Ordering::Relaxed),
28 ));
29 let mut file = match OpenOptions::new()
30 .write(true)
31 .create_new(true)
32 .mode(0o600)
33 .open(&path)
34 {
35 Ok(file) => file,
36 Err(e) if e.kind() == ErrorKind::AlreadyExists => continue,
37 Err(e) => return Err(e).with_context(|| format!("creating {}", path.display())),
38 };
39 if let Err(e) = file.write_all(bytes) {
40 let _ = std::fs::remove_file(&path);
41 return Err(e).with_context(|| format!("writing {}", path.display()));
42 }
43 return Ok(path);
44 }
45 bail!(
46 "no free temporary file name in {}",
47 std::env::temp_dir().display()
48 )
49}
50
51pub fn save_private(path: &Path, bytes: &[u8]) -> Result<()> {
56 static COUNTER: AtomicUsize = AtomicUsize::new(0);
57 let dir = path.parent().filter(|d| !d.as_os_str().is_empty());
58 if let Some(dir) = dir {
59 std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
60 }
61 let name = path
62 .file_name()
63 .with_context(|| format!("{} names no file", path.display()))?;
64 let tmp = path.with_file_name(format!(
65 ".{}.rmut-new-{}-{}",
66 name.to_string_lossy(),
67 std::process::id(),
68 COUNTER.fetch_add(1, Ordering::Relaxed)
69 ));
70 let written = (|| -> std::io::Result<()> {
71 let mut file = OpenOptions::new()
72 .write(true)
73 .create_new(true)
74 .mode(0o600)
75 .open(&tmp)?;
76 file.write_all(bytes)?;
77 file.sync_all()?;
78 std::fs::rename(&tmp, path)
79 })();
80 if let Err(err) = written {
81 let _ = std::fs::remove_file(&tmp);
82 return Err(err).with_context(|| format!("writing {}", path.display()));
83 }
84 Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::os::unix::fs::PermissionsExt as _;
91
92 #[test]
93 fn private_and_fresh_each_time() {
94 let a = write("scratch-test", ".txt", b"one").unwrap();
95 let b = write("scratch-test", ".txt", b"two").unwrap();
96 assert_ne!(a, b);
97 assert!(a.to_str().unwrap().ends_with(".txt"));
98 assert_eq!(std::fs::read(&a).unwrap(), b"one");
99 let mode = std::fs::metadata(&a).unwrap().permissions().mode();
100 assert_eq!(mode & 0o777, 0o600);
101 let _ = std::fs::remove_file(a);
102 let _ = std::fs::remove_file(b);
103 }
104
105 #[test]
106 fn save_private_replaces_whole_and_private() {
107 let dir = tempfile::tempdir().unwrap();
108 let path = dir.path().join("sub/history");
109 save_private(&path, b"one").unwrap();
110 save_private(&path, b"two").unwrap();
111 assert_eq!(std::fs::read(&path).unwrap(), b"two");
112 let mode = std::fs::metadata(&path).unwrap().permissions().mode();
113 assert_eq!(mode & 0o777, 0o600);
114 let left: Vec<_> = std::fs::read_dir(dir.path().join("sub")).unwrap().collect();
115 assert_eq!(left.len(), 1, "no temporary left behind");
116 }
117
118 #[test]
119 fn never_writes_through_a_planted_link() {
120 let dir = tempfile::tempdir().unwrap();
123 let target = dir.path().join("victim");
124 let link = dir.path().join("link");
125 std::os::unix::fs::symlink(&target, &link).unwrap();
126 let err = OpenOptions::new()
127 .write(true)
128 .create_new(true)
129 .mode(0o600)
130 .open(&link)
131 .unwrap_err();
132 assert_eq!(err.kind(), ErrorKind::AlreadyExists);
133 assert!(!target.exists());
134 }
135}