Skip to main content

rmut_core/
scratch.rs

1//! Private temporary files: a part for a mailcap `%s` command, a
2//! signature for gpg, a draft for the editor. They live in the shared
3//! temp dir, so each is created fresh (O_EXCL: never through a link or
4//! over a file someone else put there) and readable by us alone.
5
6use std::fs::OpenOptions;
7use std::io::{ErrorKind, Write as _};
8use std::os::unix::fs::OpenOptionsExt as _;
9use std::path::PathBuf;
10use std::sync::atomic::{AtomicUsize, Ordering};
11
12use anyhow::{Context, Result, bail};
13
14/// `bytes` in a new file `rmut-<what>-<unique><suffix>` under the temp
15/// dir, mode 0600. The caller removes it.
16pub fn write(what: &str, suffix: &str, bytes: &[u8]) -> Result<PathBuf> {
17    static COUNTER: AtomicUsize = AtomicUsize::new(0);
18    for _ in 0..100 {
19        // The clock makes the name hard to guess ahead of time; a taken
20        // name is skipped rather than reused either way.
21        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
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use std::os::unix::fs::PermissionsExt as _;
55
56    #[test]
57    fn private_and_fresh_each_time() {
58        let a = write("scratch-test", ".txt", b"one").unwrap();
59        let b = write("scratch-test", ".txt", b"two").unwrap();
60        assert_ne!(a, b);
61        assert!(a.to_str().unwrap().ends_with(".txt"));
62        assert_eq!(std::fs::read(&a).unwrap(), b"one");
63        let mode = std::fs::metadata(&a).unwrap().permissions().mode();
64        assert_eq!(mode & 0o777, 0o600);
65        let _ = std::fs::remove_file(a);
66        let _ = std::fs::remove_file(b);
67    }
68
69    #[test]
70    fn never_writes_through_a_planted_link() {
71        // create_new refuses a name that exists, a dangling symlink
72        // included, so a link at the path cannot redirect the write.
73        let dir = tempfile::tempdir().unwrap();
74        let target = dir.path().join("victim");
75        let link = dir.path().join("link");
76        std::os::unix::fs::symlink(&target, &link).unwrap();
77        let err = OpenOptions::new()
78            .write(true)
79            .create_new(true)
80            .mode(0o600)
81            .open(&link)
82            .unwrap_err();
83        assert_eq!(err.kind(), ErrorKind::AlreadyExists);
84        assert!(!target.exists());
85    }
86}