Skip to main content

purple_ssh/
fs_util.rs

1use std::fs;
2use std::io;
3use std::path::Path;
4
5/// Atomic write: write content to a PID-suffixed temp file with chmod 600, then rename.
6/// Uses O_EXCL (create_new) to prevent symlink attacks on the temp file path.
7/// Cleans up the temp file on failure.
8pub fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
9    // Ensure parent directory exists
10    if let Some(parent) = path.parent() {
11        fs::create_dir_all(parent)?;
12    }
13
14    let mut tmp_name = path
15        .file_name()
16        .unwrap_or_default()
17        .to_os_string();
18    tmp_name.push(format!(".purple_tmp.{}", std::process::id()));
19    let tmp_path = path.with_file_name(tmp_name);
20
21    #[cfg(unix)]
22    {
23        use std::io::Write;
24        use std::os::unix::fs::OpenOptionsExt;
25        // Try O_EXCL first. If a stale tmp file exists from a crashed run, remove
26        // it and retry once. This avoids a TOCTOU gap from removing before creating.
27        let open = || {
28            fs::OpenOptions::new()
29                .write(true)
30                .create_new(true)
31                .mode(0o600)
32                .open(&tmp_path)
33        };
34        let mut file = match open() {
35            Ok(f) => f,
36            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
37                let _ = fs::remove_file(&tmp_path);
38                open().map_err(|e| {
39                    io::Error::new(
40                        e.kind(),
41                        format!("Failed to create temp file {}: {}", tmp_path.display(), e),
42                    )
43                })?
44            }
45            Err(e) => {
46                return Err(io::Error::new(
47                    e.kind(),
48                    format!("Failed to create temp file {}: {}", tmp_path.display(), e),
49                ));
50            }
51        };
52        file.write_all(content)?;
53    }
54
55    #[cfg(not(unix))]
56    fs::write(&tmp_path, content)?;
57
58    let result = fs::rename(&tmp_path, path);
59    if result.is_err() {
60        let _ = fs::remove_file(&tmp_path);
61    }
62    result
63}