功能说明
测试和文件处理工具经常需要临时目录:写入中间文件、解压内容或模拟用户目录。手动清理容易因为提前返回或 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(())
}