use crate::{error::*, file::*};
use camino::{Utf8Path, Utf8PathBuf};
use std::fs::{File, create_dir_all, remove_dir_all};
use std::path::Component;
fn reject_traversal(subpath: &Utf8Path) -> Result<(), Err> {
if subpath.as_std_path().is_absolute()
|| subpath
.as_std_path()
.components()
.any(|c| c == Component::ParentDir)
{
return Result::Err(Err::PathTraversal(subpath.to_string()));
}
Ok(())
}
pub struct TmpDir {
path: Utf8PathBuf,
retain_on_close: bool,
}
pub struct TmpFile(File);
impl TmpDir {
pub fn new(bpt_conf: &BptConf) -> Result<Self, Err> {
Ok(Self {
path: bpt_conf
.build
.tmp
.join(format!("bpt-{}", std::process::id())),
retain_on_close: false,
})
}
pub fn retain_on_close(&mut self) {
self.retain_on_close = true;
}
pub fn as_path(&self) -> &Utf8Path {
&self.path
}
pub fn as_str(&self) -> &str {
self.path.as_str()
}
pub fn subdir(&self, subpath: &Utf8Path) -> Result<TmpDir, Err> {
reject_traversal(subpath)?;
let path = self.as_path().join(subpath);
create_dir_all(&path).map_err(|e| Err::CreateDir(path.to_string(), e))?;
Ok(TmpDir {
path,
retain_on_close: true,
})
}
pub fn subfile(&self, subpath: &Utf8Path) -> Result<TmpFile, Err> {
reject_traversal(subpath)?;
let path = self.as_path().join(subpath);
if let Some(parent) = path.parent() {
create_dir_all(parent).map_err(|e| Err::CreateDir(parent.to_string(), e))?;
}
let file = File::create(&path).map_err(|e| Err::CreateFile(path.to_string(), e))?;
Ok(TmpFile(file))
}
}
impl TmpFile {
pub fn into_file(self) -> File {
self.0
}
}
impl Drop for TmpDir {
fn drop(&mut self) {
if !self.retain_on_close {
let _ = remove_dir_all(&self.path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::unit_test_tmp_dir;
fn make_tmpdir(name: &str) -> TmpDir {
let path = unit_test_tmp_dir("tmpdir", name);
TmpDir {
path,
retain_on_close: true, }
}
#[test]
fn reject_traversal_allows_simple_relative() {
assert!(reject_traversal(Utf8Path::new("foo/bar")).is_ok());
}
#[test]
fn reject_traversal_rejects_parent_dir() {
assert!(reject_traversal(Utf8Path::new("foo/../bar")).is_err());
}
#[test]
fn reject_traversal_rejects_leading_parent_dir() {
assert!(reject_traversal(Utf8Path::new("../escape")).is_err());
}
#[test]
fn reject_traversal_rejects_absolute_path() {
assert!(reject_traversal(Utf8Path::new("/etc/passwd")).is_err());
}
#[test]
fn subdir_creates_directory() {
let tmp = make_tmpdir("subdir_creates");
let sub = tmp.subdir(Utf8Path::new("child")).unwrap();
assert!(sub.as_path().is_dir());
}
#[test]
fn subdir_rejects_absolute_path() {
let tmp = make_tmpdir("subdir_rejects_abs");
assert!(tmp.subdir(Utf8Path::new("/etc")).is_err());
}
#[test]
fn subdir_rejects_parent_traversal() {
let tmp = make_tmpdir("subdir_rejects_parent");
assert!(tmp.subdir(Utf8Path::new("a/../../escape")).is_err());
}
#[test]
fn subfile_creates_file_and_parents() {
let tmp = make_tmpdir("subfile_creates");
let _f = tmp.subfile(Utf8Path::new("a/b/c.txt")).unwrap();
assert!(tmp.as_path().join("a/b/c.txt").exists());
}
#[test]
fn subfile_rejects_absolute_path() {
let tmp = make_tmpdir("subfile_rejects_abs");
assert!(tmp.subfile(Utf8Path::new("/tmp/evil")).is_err());
}
#[test]
fn subfile_rejects_parent_traversal() {
let tmp = make_tmpdir("subfile_rejects_parent");
assert!(tmp.subfile(Utf8Path::new("../escape.txt")).is_err());
}
#[test]
fn drop_removes_directory_when_not_retained() {
let path;
{
let tmp = TmpDir {
path: unit_test_tmp_dir("tmpdir", "drop_removes"),
retain_on_close: false,
};
let _f = tmp.subfile(Utf8Path::new("file.txt")).unwrap();
path = tmp.as_path().to_owned();
assert!(path.exists());
}
assert!(!path.exists());
}
#[test]
fn drop_retains_directory_when_retained() {
let path;
{
let mut tmp = TmpDir {
path: unit_test_tmp_dir("tmpdir", "drop_retains"),
retain_on_close: false,
};
let _f = tmp.subfile(Utf8Path::new("file.txt")).unwrap();
tmp.retain_on_close();
path = tmp.as_path().to_owned();
}
assert!(path.exists());
let _ = remove_dir_all(&path);
}
}