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 write_named(what: &str, name: &str, bytes: &[u8]) -> Result<PathBuf> {
56 use std::os::unix::fs::DirBuilderExt as _;
57 static COUNTER: AtomicUsize = AtomicUsize::new(0);
58 let name = Path::new(name)
60 .file_name()
61 .map(|n| n.to_string_lossy().into_owned())
62 .filter(|n| !n.is_empty())
63 .unwrap_or_else(|| "part".to_string());
64 for _ in 0..100 {
65 let nanos = std::time::SystemTime::now()
66 .duration_since(std::time::UNIX_EPOCH)
67 .map_or(0, |d| d.subsec_nanos());
68 let dir = std::env::temp_dir().join(format!(
69 "rmut-{what}-{}-{}-{nanos:08x}",
70 std::process::id(),
71 COUNTER.fetch_add(1, Ordering::Relaxed),
72 ));
73 match std::fs::DirBuilder::new().mode(0o700).create(&dir) {
74 Ok(()) => {}
75 Err(e) if e.kind() == ErrorKind::AlreadyExists => continue,
76 Err(e) => return Err(e).with_context(|| format!("creating {}", dir.display())),
77 }
78 let path = dir.join(&name);
79 let written = OpenOptions::new()
80 .write(true)
81 .create_new(true)
82 .mode(0o600)
83 .open(&path)
84 .and_then(|mut file| file.write_all(bytes));
85 if let Err(e) = written {
86 discard(&path);
87 return Err(e).with_context(|| format!("writing {}", path.display()));
88 }
89 return Ok(path);
90 }
91 bail!(
92 "no free temporary directory name in {}",
93 std::env::temp_dir().display()
94 )
95}
96
97pub fn discard(path: &Path) {
99 let _ = std::fs::remove_file(path);
100 if let Some(dir) = path.parent() {
101 let _ = std::fs::remove_dir(dir);
102 }
103}
104
105pub fn save_private(path: &Path, bytes: &[u8]) -> Result<()> {
110 static COUNTER: AtomicUsize = AtomicUsize::new(0);
111 let dir = path.parent().filter(|d| !d.as_os_str().is_empty());
112 if let Some(dir) = dir {
113 std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
114 }
115 let name = path
116 .file_name()
117 .with_context(|| format!("{} names no file", path.display()))?;
118 let tmp = path.with_file_name(format!(
119 ".{}.rmut-new-{}-{}",
120 name.to_string_lossy(),
121 std::process::id(),
122 COUNTER.fetch_add(1, Ordering::Relaxed)
123 ));
124 let written = (|| -> std::io::Result<()> {
125 let mut file = OpenOptions::new()
126 .write(true)
127 .create_new(true)
128 .mode(0o600)
129 .open(&tmp)?;
130 file.write_all(bytes)?;
131 file.sync_all()?;
132 std::fs::rename(&tmp, path)
133 })();
134 if let Err(err) = written {
135 let _ = std::fs::remove_file(&tmp);
136 return Err(err).with_context(|| format!("writing {}", path.display()));
137 }
138 Ok(())
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::os::unix::fs::PermissionsExt as _;
145
146 #[test]
147 fn private_and_fresh_each_time() {
148 let a = write("scratch-test", ".txt", b"one").unwrap();
149 let b = write("scratch-test", ".txt", b"two").unwrap();
150 assert_ne!(a, b);
151 assert!(a.to_str().unwrap().ends_with(".txt"));
152 assert_eq!(std::fs::read(&a).unwrap(), b"one");
153 let mode = std::fs::metadata(&a).unwrap().permissions().mode();
154 assert_eq!(mode & 0o777, 0o600);
155 let _ = std::fs::remove_file(a);
156 let _ = std::fs::remove_file(b);
157 }
158
159 #[test]
160 fn save_private_replaces_whole_and_private() {
161 let dir = tempfile::tempdir().unwrap();
162 let path = dir.path().join("sub/history");
163 save_private(&path, b"one").unwrap();
164 save_private(&path, b"two").unwrap();
165 assert_eq!(std::fs::read(&path).unwrap(), b"two");
166 let mode = std::fs::metadata(&path).unwrap().permissions().mode();
167 assert_eq!(mode & 0o777, 0o600);
168 let left: Vec<_> = std::fs::read_dir(dir.path().join("sub")).unwrap().collect();
169 assert_eq!(left.len(), 1, "no temporary left behind");
170 }
171
172 #[test]
173 fn named_parts_get_a_private_directory_of_their_own() {
174 let a = write_named("view", "report.pdf", b"%PDF").unwrap();
175 let b = write_named("view", "../../etc/report.pdf", b"%PDF").unwrap();
176 assert_eq!(a.file_name().unwrap(), "report.pdf");
177 assert_eq!(
178 b.file_name().unwrap(),
179 "report.pdf",
180 "only the last component"
181 );
182 assert_ne!(a.parent(), b.parent());
183 let dir_mode = std::fs::metadata(a.parent().unwrap())
184 .unwrap()
185 .permissions()
186 .mode();
187 assert_eq!(dir_mode & 0o777, 0o700);
188 let mode = std::fs::metadata(&a).unwrap().permissions().mode();
189 assert_eq!(mode & 0o777, 0o600);
190 let dir = a.parent().unwrap().to_path_buf();
191 discard(&a);
192 discard(&b);
193 assert!(!dir.exists());
194 }
195
196 #[test]
197 fn never_writes_through_a_planted_link() {
198 let dir = tempfile::tempdir().unwrap();
201 let target = dir.path().join("victim");
202 let link = dir.path().join("link");
203 std::os::unix::fs::symlink(&target, &link).unwrap();
204 let err = OpenOptions::new()
205 .write(true)
206 .create_new(true)
207 .mode(0o600)
208 .open(&link)
209 .unwrap_err();
210 assert_eq!(err.kind(), ErrorKind::AlreadyExists);
211 assert!(!target.exists());
212 }
213}