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 tmp_path = path.with_extension(format!("purple_tmp.{}", std::process::id()));
15
16    #[cfg(unix)]
17    {
18        use std::io::Write;
19        use std::os::unix::fs::OpenOptionsExt;
20        // Try O_EXCL first. If a stale tmp file exists from a crashed run, remove
21        // it and retry once. This avoids a TOCTOU gap from removing before creating.
22        let open = || {
23            fs::OpenOptions::new()
24                .write(true)
25                .create_new(true)
26                .mode(0o600)
27                .open(&tmp_path)
28        };
29        let mut file = match open() {
30            Ok(f) => f,
31            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
32                let _ = fs::remove_file(&tmp_path);
33                open().map_err(|e| {
34                    io::Error::new(
35                        e.kind(),
36                        format!("Failed to create temp file {}: {}", tmp_path.display(), e),
37                    )
38                })?
39            }
40            Err(e) => {
41                return Err(io::Error::new(
42                    e.kind(),
43                    format!("Failed to create temp file {}: {}", tmp_path.display(), e),
44                ));
45            }
46        };
47        file.write_all(content)?;
48    }
49
50    #[cfg(not(unix))]
51    fs::write(&tmp_path, content)?;
52
53    let result = fs::rename(&tmp_path, path);
54    if result.is_err() {
55        let _ = fs::remove_file(&tmp_path);
56    }
57    result
58}