← 中文资源

简体中文版本

RAII 临时目录守卫与自动清理

创建唯一临时目录并用 Drop 自动清理,同时拒绝绝对路径和父目录逃逸。

阅读英文原文

功能说明

测试和文件处理工具经常需要临时目录:写入中间文件、解压内容或模拟用户目录。手动清理容易因为提前返回或 panic 而遗漏。这段代码用 Rust 的 RAII 机制,把清理逻辑放在 Drop 中,离开作用域时自动删除整个目录。

TempDir 还提供受限的 write 方法,拒绝绝对路径、父目录组件和 Windows 盘符前缀,避免调用方把文件写到临时目录之外。它适合作为测试夹具或 CLI 的临时工作区。

关键代码

TempDir::new 把进程 ID 和原子计数器拼进目录名,调用 fs::create_dir 创建目录;如果同名目录已存在会直接失败,而不是复用可能被占用的路径。

write 遍历路径组件,遇到 ParentDir、Prefix 或 RootDir 就返回 InvalidInput;随后为嵌套文件创建父目录并写入内容。Drop 调用 remove_dir_all,错误被忽略,因为析构函数不能返回错误。

使用注意

进程被强制终止、abort 或机器断电时 Drop 不会执行,临时目录可能残留;启动时应有清理策略。共享临时目录中,其他进程可能预创建同名路径造成符号链接攻击;高安全场景应使用操作系统提供的安全临时目录 API。

Windows 上如果文件仍被打开,删除可能失败,代码会静默忽略。目录权限继承系统默认值,不要在其中存放长期密钥。write 只防止路径逃逸,不限制文件大小和数量。

验证方式

示例写入 nested/config.txt 并读回 ok,随后尝试写入 ../escape.txt,断言返回错误。保存路径后显式 drop(dir),再断言目录已不存在,证明清理逻辑生效。最后再创建一个临时目录并确认存在,验证计数器避免了重名。

代码示例

use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

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

struct TempDir {
    path: PathBuf,
}

impl TempDir {
    fn new(prefix: &str) -> io::Result<Self> {
        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!("{prefix}-{}-{id}", std::process::id()));
        fs::create_dir(&path)?;
        Ok(Self { path })
    }

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

    fn write(&self, name: &str, contents: &str) -> io::Result<PathBuf> {
        let relative = Path::new(name);
        let escapes = relative.components().any(|component| {
            matches!(
                component,
                Component::ParentDir | Component::Prefix(_) | Component::RootDir
            )
        });
        if relative.is_absolute() || escapes {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "path escapes the temporary directory",
            ));
        }

        let target = self.path.join(relative);
        if let Some(parent) = target.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(&target, contents)?;
        Ok(target)
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}

fn main() -> io::Result<()> {
    let dir = TempDir::new("devexamples")?;
    let file = dir.write("nested/config.txt", "ok")?;
    println!("wrote {}", file.display());
    assert_eq!(fs::read_to_string(&file)?, "ok");
    assert!(dir.write("../escape.txt", "no").is_err());

    let path = dir.path().to_path_buf();
    drop(dir);
    assert!(!path.exists(), "temporary directory should be removed");

    let second = TempDir::new("devexamples")?;
    assert!(second.path().exists());
    Ok(())
}