use std::fs::{File, self};
use std::io::{Write, BufWriter};
use std::path::{Path, PathBuf};
use std::fmt::Display;
use std::ffi::{OsStr, OsString};
use std::collections::HashMap;
use tempfile::TempDir;
use parking_lot::Mutex;
use crate::error::Result;
#[cfg_attr(nightly, doc(cfg(feature = "test")))]
pub struct Jail {
_directory: TempDir,
canonical_dir: PathBuf,
saved_env_vars: HashMap<OsString, Option<OsString>>,
saved_cwd: PathBuf,
}
fn as_string<S: Display>(s: S) -> String { s.to_string() }
fn dedot(path: &Path) -> PathBuf {
use std::path::Component::*;
let mut comps = vec![];
for component in path.components() {
match component {
p@Prefix(_) => comps = vec![p],
r@RootDir if comps.iter().all(|c| matches!(c, Prefix(_))) => comps.push(r),
r@RootDir => comps = vec![r],
CurDir => { },
ParentDir if comps.iter().all(|c| matches!(c, Prefix(_) | RootDir)) => { },
ParentDir => { comps.pop(); },
c@Normal(_) => comps.push(c),
}
}
comps.iter().map(|c| c.as_os_str()).collect()
}
static LOCK: Mutex<()> = parking_lot::const_mutex(());
impl Jail {
#[track_caller]
pub fn expect_with<F: FnOnce(&mut Jail) -> Result<()>>(f: F) {
if let Err(e) = Jail::try_with(f) {
panic!("jail failed: {}", e)
}
}
#[track_caller]
pub fn try_with<F: FnOnce(&mut Jail) -> Result<()>>(f: F) -> Result<()> {
let _lock = LOCK.lock();
let directory = TempDir::new().map_err(as_string)?;
let mut jail = Jail {
canonical_dir: directory.path().canonicalize().map_err(as_string)?,
_directory: directory,
saved_cwd: std::env::current_dir().map_err(as_string)?,
saved_env_vars: HashMap::new(),
};
std::env::set_current_dir(jail.directory()).map_err(as_string)?;
f(&mut jail)
}
pub fn directory(&self) -> &Path {
&self.canonical_dir
}
fn safe_jailed_path(&self, path: &Path) -> Result<PathBuf> {
let path = dedot(path);
if path.is_absolute() && path.starts_with(self.directory()) {
return Ok(path);
}
if !path.is_relative() {
return Err("Jail: input path is outside of jail directory".to_string().into());
}
Ok(path)
}
pub fn create_file<P: AsRef<Path>>(&self, path: P, contents: &str) -> Result<File> {
self.create_binary(path.as_ref(), contents.as_bytes())
}
pub fn create_binary<P: AsRef<Path>>(&self, path: P, bytes: &[u8]) -> Result<File> {
let path = self.safe_jailed_path(path.as_ref())?;
let file = File::create(path).map_err(as_string)?;
let mut writer = BufWriter::new(file);
writer.write_all(bytes).map_err(as_string)?;
Ok(writer.into_inner().map_err(as_string)?)
}
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
let path = self.safe_jailed_path(path.as_ref())?;
fs::create_dir_all(&path).map_err(as_string)?;
Ok(path)
}
pub fn change_dir<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
let path = self.safe_jailed_path(path.as_ref())?;
std::env::set_current_dir(&path).map_err(as_string)?;
Ok(path)
}
pub fn clear_env(&mut self) {
for (key, val) in std::env::vars_os() {
std::env::remove_var(&key);
if !self.saved_env_vars.contains_key(&key) {
self.saved_env_vars.insert(key, Some(val));
}
}
}
pub fn set_env<K: AsRef<str>, V: Display>(&mut self, k: K, v: V) {
let key = k.as_ref();
if !self.saved_env_vars.contains_key(OsStr::new(key)) {
self.saved_env_vars.insert(key.into(), std::env::var_os(key));
}
std::env::set_var(key, v.to_string());
}
}
impl Drop for Jail {
fn drop(&mut self) {
for (key, value) in self.saved_env_vars.iter() {
match value {
Some(val) => std::env::set_var(key, val),
None => std::env::remove_var(key)
}
}
let _ = std::env::set_current_dir(&self.saved_cwd);
}
}