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        // Remove stale tmp file from a previous crashed run (O_EXCL would fail otherwise)
21        let _ = fs::remove_file(&tmp_path);
22        let mut file = fs::OpenOptions::new()
23            .write(true)
24            .create_new(true)
25            .mode(0o600)
26            .open(&tmp_path)
27            .map_err(|e| {
28                io::Error::new(
29                    e.kind(),
30                    format!("Failed to create temp file {}: {}", tmp_path.display(), e),
31                )
32            })?;
33        file.write_all(content)?;
34    }
35
36    #[cfg(not(unix))]
37    fs::write(&tmp_path, content)?;
38
39    let result = fs::rename(&tmp_path, path);
40    if result.is_err() {
41        let _ = fs::remove_file(&tmp_path);
42    }
43    result
44}