use log::{debug, warn};
use std::error::Error as StdError;
use std::{
env, io,
path::{Path, PathBuf},
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PushdError {
#[error("Could not get current directory: {source}")]
GetCurrentDir {
#[from]
source: io::Error,
},
#[error("Could not set current directory to {path}: {source}")]
SetCurrentDir { path: PathBuf, source: io::Error },
}
pub struct Pushd {
orig: PathBuf,
panic_on_err: bool,
popped: bool,
}
impl Pushd {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Pushd, PushdError> {
let cwd = env::current_dir()?;
env::set_current_dir(path.as_ref()).map_err(|e| PushdError::SetCurrentDir {
path: path.as_ref().to_owned(),
source: e,
})?;
debug!(
"Set current dir to {} from {}.",
path.as_ref().display(),
cwd.display(),
);
Ok(Pushd {
orig: cwd,
panic_on_err: true,
popped: false,
})
}
pub fn new_no_panic<P: AsRef<Path>>(path: P) -> Result<Pushd, PushdError> {
let mut pd = Self::new(path)?;
pd.panic_on_err = false;
Ok(pd)
}
pub fn pop(&mut self) -> Result<(), PushdError> {
if self.popped {
return Ok(());
}
debug!("Setting current dir back to {}.", self.orig.display());
env::set_current_dir(&self.orig).map_err(|e| PushdError::SetCurrentDir {
path: self.orig.clone(),
source: e,
})?;
self.popped = true;
Ok(())
}
}
impl Drop for Pushd {
fn drop(&mut self) {
if let Err(e) = self.pop() {
if !self.panic_on_err {
warn!(
"Could not return to original dir {}: {e}",
self.orig.display(),
);
return;
}
if let Some(s) = e.source() {
if let Some(i) = s.downcast_ref::<io::Error>() {
if i.kind() == io::ErrorKind::NotFound {
return;
}
}
}
panic!("Could not return to original dir: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use serial_test::serial;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::PermissionsExt;
#[cfg(not(target_os = "windows"))]
use std::panic;
use tempfile::tempdir;
#[test]
#[serial]
fn no_errors() -> Result<(), Box<dyn StdError>> {
env::set_current_dir(env::var("CARGO_MANIFEST_DIR")?)?;
let cwd = fs::canonicalize(env::current_dir()?)?;
{
let td = tempdir()?;
let _pd = Pushd::new(td.path());
assert_eq!(
fs::canonicalize(env::current_dir()?)?,
fs::canonicalize(td.path())?,
);
}
assert_eq!(fs::canonicalize(env::current_dir()?)?, cwd);
Ok(())
}
#[test]
#[serial]
fn no_errors_explicit_pop() -> Result<(), Box<dyn StdError>> {
env::set_current_dir(env::var("CARGO_MANIFEST_DIR")?)?;
let cwd = fs::canonicalize(env::current_dir()?)?;
{
let td = tempdir()?;
let mut pd = Pushd::new(td.path())?;
assert_eq!(
fs::canonicalize(env::current_dir()?)?,
fs::canonicalize(td.path())?,
);
pd.pop()?;
assert_eq!(fs::canonicalize(env::current_dir()?)?, cwd);
}
assert_eq!(fs::canonicalize(env::current_dir()?)?, cwd);
Ok(())
}
#[cfg(not(target_os = "windows"))]
#[test]
#[serial]
fn not_found_error_on_drop() -> Result<(), Box<dyn StdError>> {
env::set_current_dir(env::var("CARGO_MANIFEST_DIR")?)?;
let td1 = tempdir()?;
env::set_current_dir(td1.path())?;
{
let td2 = tempdir()?;
let _pd = Pushd::new(td2.path());
assert_eq!(
fs::canonicalize(env::current_dir()?)?,
fs::canonicalize(td2.path())?,
);
td1.close()?;
}
let cwd = env::current_dir();
assert!(cwd.is_err());
assert_eq!(cwd.unwrap_err().kind(), io::ErrorKind::NotFound);
Ok(())
}
#[cfg(not(target_os = "windows"))]
#[test]
#[serial]
fn permissions_error_panic_on_drop() -> Result<(), Box<dyn StdError>> {
let result = panic::catch_unwind(|| {
let man_dir = match env::var("CARGO_MANIFEST_DIR") {
Ok(md) => md,
Err(e) => {
println!("Error getting CARGO_MANIFEST_DIR: {e}");
return;
}
};
match env::set_current_dir(man_dir) {
Ok(()) => (),
Err(e) => {
println!("Error setting current dir to CARGO_MANIFEST_DIR: {e}");
return;
}
}
let td1 = match tempdir() {
Ok(td) => td,
Err(e) => {
println!("Error creating tempdir: {e}");
return;
}
};
match env::set_current_dir(td1.path()) {
Ok(()) => (),
Err(e) => {
println!("Error setting current dir to tempdir: {e}");
return;
}
}
{
let td2 = match tempdir() {
Ok(td) => td,
Err(e) => {
println!("Error creating tempdir: {e}");
return;
}
};
let _pd = Pushd::new(td2.path());
let md = match fs::metadata(td1.path()) {
Ok(md) => md,
Err(e) => {
println!("Error getting metadata for tempdir: {e}");
return;
}
};
let mut perms = md.permissions();
perms.set_mode(0o0400);
match fs::set_permissions(td1.path(), perms) {
Ok(()) => (),
Err(e) => {
println!("Error setting permissions for tempdir: {e}");
return;
}
}
}
println!("We should never get here");
});
assert!(result.is_err());
assert!(result
.unwrap_err()
.downcast_ref::<String>()
.unwrap()
.contains("Permission denied"));
Ok(())
}
#[cfg(not(target_os = "windows"))]
#[test]
#[serial]
fn permissions_error_no_panic_on_drop() -> Result<(), Box<dyn StdError>> {
env::set_current_dir(env::var("CARGO_MANIFEST_DIR")?)?;
let td1 = tempdir()?;
env::set_current_dir(td1.path())?;
{
let td2 = tempdir()?;
let _pd = Pushd::new_no_panic(td2.path());
assert_eq!(
fs::canonicalize(env::current_dir()?)?,
fs::canonicalize(td2.path())?,
);
let mut perms = fs::metadata(td1.path())?.permissions();
perms.set_mode(0o0400);
fs::set_permissions(td1.path(), perms)?;
}
let cwd = env::current_dir();
assert!(cwd.is_err());
assert_eq!(cwd.unwrap_err().kind(), io::ErrorKind::NotFound);
Ok(())
}
}