File System Operations

Temporary Files - Standard-Library Limitations

The Rust standard library provides std::env::temp_dir() -> PathBuf to query the operating system's temporary directory (/tmp or $TMPDIR on Unix, %TEMP% on Windows), but intentionally does not include a built-in temporary-file generator or auto-deleting TempFile / TempDir type in std.

Why Naive Temporary File Creation Is Unsafe

In a multi-user or multi-process system, constructing a predictable path like env::temp_dir().join("my_app_temp.txt") and calling File::create(&path) has three severe flaws:

  1. Symlink / TOCTOU Attack (File::create): If an attacker creates a symbolic link /tmp/my_app_temp.txt -> /home/user/.ssh/authorized_keys before your program runs, File::create follows the symlink and overwrites the target file!
  2. Name Collisions Across Concurrent Processes or Tests: Two threads or test binaries running simultaneously overwrite each other's /tmp/my_app_temp.txt.
  3. Leaked Disk Files on Early Return or Panic: Without an RAII guard whose Drop implementation deletes the file, any early ? error or panic leaves stale files in /tmp.
Unsafe Naive Pattern:
  let p = env::temp_dir().join("fixed_name.tmp");
  File::create(&p)?;   // ❌ Follows symlinks, collides across processes, leaks on panic!

Safe Standard-Library Pattern (`File::create_new` + RAII Drop Guard):
  1. Generate unique name (`PID` + atomic counter + timestamp)
  2. Open with `File::create_new(&p)` (`O_CREAT | O_EXCL` -> NEVER follows symlinks,
     retries if `ErrorKind::AlreadyExists`!)
  3. Wrap in a `ScopedTempFile` struct whose `Drop` calls `let _ = fs::remove_file(&self.path);`!

Building a Secure, Self-Deleting ScopedTempFile in Pure std

By combining File::create_new (which uses OS O_CREAT | O_EXCL to guarantee the path did not previously exist as a file or symlink), an atomic counter + process ID, and a Drop implementation that removes the file when the guard leaves scope, you get collision-free, auto-cleaning temporary files in pure std (or use the ecosystem tempfile crate in production Cargo projects):

use std::fs::{self, File};
use std::io::{self, ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};

static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

pub struct ScopedTempFile {
    file: Option<File>,
    path: PathBuf,
}

impl ScopedTempFile {
    pub fn new(prefix: &str) -> io::Result<Self> {
        let base_dir = std::env::temp_dir();
        let pid = process::id();

        loop {
            let seq = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
            let candidate = base_dir.join(format!("{prefix}_{pid}_{seq}.tmp"));

            // `File::create_new` uses `O_CREAT | O_EXCL`: it NEVER follows symlinks
            // and returns `AlreadyExists` if another process took the name:
            match File::create_new(&candidate) {
                Ok(file) => {
                    return Ok(Self {
                        file: Some(file),
                        path: candidate,
                    });
                }
                Err(e) if e.kind() == ErrorKind::AlreadyExists => continue,
                Err(e) => return Err(e),
            }
        }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn file_mut(&mut self) -> &mut File {
        self.file.as_mut().unwrap()
    }
}

impl Drop for ScopedTempFile {
    fn drop(&mut self) {
        // Close the `File` handle first (required on Windows before deleting!),
        // then remove the temporary file from disk:
        drop(self.file.take());
        let _ = fs::remove_file(&self.path);
    }
}

fn main() -> io::Result<()> {
    let saved_path: PathBuf;
    {
        let mut tmp = ScopedTempFile::new("rust_ch50")?;
        saved_path = tmp.path().to_path_buf();
        tmp.file_mut().write_all(b"auto-cleaned scratch data\n")?;
        println!(
            "inside scope -> exists: {}, content: {:?}",
            fs::exists(&saved_path)?,
            fs::read_to_string(&saved_path)?.trim()
        );
    } // `tmp` drops here, closing and deleting `saved_path` automatically!

    println!("after scope  -> exists: {}", fs::exists(&saved_path)?);
    Ok(())
}
inside scope -> exists: true, content: "auto-cleaned scratch data"
after scope  -> exists: false