use std::{env, fs,
fs::File,
io::{ErrorKind, Write},
path::{Path, PathBuf}};
use miette::Diagnostic;
use thiserror::Error;
use crate::ok;
#[macro_export]
macro_rules! fs_paths {
(with_root: $path:expr=> $($x:expr)=>*) => {{
let mut it: std::path::PathBuf = $path.to_path_buf();
$(
it = it.join($x);
)*
it
}};
(with_empty_root=> $($x:expr)=>*) => {{
use std::path::{PathBuf};
let mut it = PathBuf::new();
$(
it = it.join($x);
)*
it
}}
}
#[macro_export]
macro_rules! fs_paths_exist {
($($x:expr),*) => {'block: {
$(
if !std::fs::metadata($x).is_ok() {
break 'block false;
};
)*
true
}};
}
#[derive(Debug, Error, Diagnostic)]
pub enum FsOpError {
#[error("File does not exist: {0}")]
FileDoesNotExist(String),
#[error("Directory does not exist: {0}")]
DirectoryDoesNotExist(String),
#[error("File already exists: {0}")]
FileAlreadyExists(String),
#[error("Directory already exists: {0}")]
DirectoryAlreadyExists(String),
#[error("Insufficient permissions: {0}")]
PermissionDenied(String),
#[error("Invalid name: {0}")]
InvalidName(String),
#[error("Failed to perform fs operation directory: {0}")]
IoError(#[from] std::io::Error),
}
pub type FsOpResult<T> = miette::Result<T, FsOpError>;
pub fn directory_exists(directory: impl AsRef<Path>) -> bool {
fs::metadata(directory).is_ok_and(|metadata| metadata.is_dir())
}
pub fn file_exists(file: impl AsRef<Path>) -> bool {
fs::metadata(file).is_ok_and(|metadata| metadata.is_file())
}
pub fn try_directory_exists(directory_path: impl AsRef<Path>) -> FsOpResult<bool> {
match fs::metadata(directory_path) {
Ok(metadata) => {
ok!(metadata.is_dir())
}
Err(err) => match err.kind() {
ErrorKind::NotFound => {
FsOpResult::Err(FsOpError::DirectoryDoesNotExist(err.to_string()))
}
ErrorKind::InvalidInput => {
FsOpResult::Err(FsOpError::InvalidName(err.to_string()))
}
_ => FsOpResult::Err(FsOpError::IoError(err)),
},
}
}
pub fn try_file_exists(file_path: impl AsRef<Path>) -> FsOpResult<bool> {
match fs::metadata(file_path) {
Ok(metadata) => ok!(metadata.is_file()),
Err(err) => match err.kind() {
ErrorKind::NotFound => {
FsOpResult::Err(FsOpError::FileDoesNotExist(err.to_string()))
}
ErrorKind::InvalidInput => {
FsOpResult::Err(FsOpError::InvalidName(err.to_string()))
}
_ => FsOpResult::Err(FsOpError::IoError(err)),
},
}
}
pub fn try_pwd() -> FsOpResult<PathBuf> {
match env::current_dir() {
Ok(pwd) => FsOpResult::Ok(pwd),
Err(err) => match err.kind() {
ErrorKind::NotFound => {
FsOpResult::Err(FsOpError::DirectoryDoesNotExist(err.to_string()))
}
_ => FsOpResult::Err(FsOpError::IoError(err)),
},
}
}
#[must_use]
pub fn path_as_string(path: &Path) -> String { path.display().to_string() }
pub fn try_write_file(
folder: impl AsRef<Path>,
file_name: impl AsRef<str>,
content: impl AsRef<str>,
) -> FsOpResult<()> {
let file_path = folder.as_ref().join(file_name.as_ref());
match File::create(file_path) {
Ok(mut file) => {
if let Err(err) = file.write_all(content.as_ref().as_bytes()) {
return FsOpResult::Err(FsOpError::IoError(err));
}
ok!()
}
Err(err) => match err.kind() {
ErrorKind::NotFound => {
FsOpResult::Err(FsOpError::DirectoryDoesNotExist(err.to_string()))
}
ErrorKind::PermissionDenied => {
FsOpResult::Err(FsOpError::PermissionDenied(err.to_string()))
}
ErrorKind::InvalidInput => {
FsOpResult::Err(FsOpError::InvalidName(err.to_string()))
}
_ => FsOpResult::Err(FsOpError::IoError(err)),
},
}
}
#[cfg(test)]
mod tests {
use std::os::unix::fs::PermissionsExt;
use super::*;
use crate::{fs_path, try_create_temp_dir, with_saved_pwd};
fn test_try_directory_exists_not_found_error() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let new_dir = root.join("test_dir_exists_not_found_error");
let result = fs_path::try_directory_exists(&new_dir);
assert!(result.is_err());
assert!(matches!(result, Err(FsOpError::DirectoryDoesNotExist(_))));
});
}
fn test_try_directory_exists_permissions_errors() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let no_permissions_dir = root.join("no_permissions_dir");
fs::create_dir_all(&no_permissions_dir).unwrap();
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o000);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
assert!(no_permissions_dir.exists());
let result = fs_path::try_directory_exists(&no_permissions_dir);
assert!(result.is_ok());
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o777);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
});
}
fn test_try_file_exists() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let new_dir = root.join("test_file_exists_dir");
fs::create_dir_all(&new_dir).unwrap();
let new_file = new_dir.join("test_file_exists_file.txt");
fs::write(&new_file, "test").unwrap();
assert!(fs_path::try_file_exists(&new_file).unwrap());
assert!(!fs_path::try_file_exists(&new_dir).unwrap());
fs::remove_dir_all(&new_dir).unwrap();
assert!(fs_path::try_file_exists(&new_file).is_err()); assert!(fs_path::try_file_exists(&new_dir).is_err()); });
}
fn test_try_file_exists_invalid_name_error() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let new_dir = root.join("test_file_exists_invalid_name_error\0");
let result = fs_path::try_file_exists(&new_dir);
assert!(result.is_err());
assert!(matches!(result, Err(FsOpError::InvalidName(_))));
});
}
fn test_try_file_exists_permissions_errors() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let no_permissions_dir = root.join("no_permissions_dir");
fs::create_dir_all(&no_permissions_dir).unwrap();
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o000);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
assert!(no_permissions_dir.exists());
let result = fs_path::try_file_exists(&no_permissions_dir);
assert!(result.is_ok());
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o777);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
});
}
fn test_try_pwd() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let new_dir = root.join("test_pwd");
fs::create_dir_all(&new_dir).unwrap();
env::set_current_dir(&new_dir).unwrap();
let pwd = try_pwd().unwrap();
assert!(pwd.exists());
assert_eq!(pwd, new_dir);
});
}
fn test_try_pwd_errors() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let no_permissions_dir = root.join("no_permissions_dir");
fs::create_dir_all(&no_permissions_dir).unwrap();
env::set_current_dir(&no_permissions_dir).unwrap();
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o000);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
assert!(no_permissions_dir.exists());
let result = try_pwd();
assert!(result.is_ok());
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o777);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
fs::remove_dir_all(&no_permissions_dir).unwrap();
let result = try_pwd();
assert!(result.is_err());
assert!(matches!(result, Err(FsOpError::DirectoryDoesNotExist(_))));
});
}
fn test_try_write() {
with_saved_pwd!({
let root = try_create_temp_dir().unwrap();
let content = "Hello, world!";
let file_name = "test_file.txt";
try_write_file(&root, file_name, content).unwrap();
let file_path = root.join(file_name);
assert!(file_path.exists());
assert!(file_path.is_file());
let read_content = fs::read_to_string(&file_path).unwrap();
assert_eq!(read_content, content);
});
}
fn test_try_mkdir() {
with_saved_pwd!({
use crate::{directory_create::{try_mkdir, MkdirOptions::*},
fs_paths};
let root = try_create_temp_dir().unwrap();
let tmp_root_dir = fs_paths!(with_root: root => "test_create_clean_new_dir");
try_mkdir(&tmp_root_dir, CreateIntermediateDirectories).unwrap();
let new_dir = fs_paths!(with_root: tmp_root_dir => "new_dir");
try_mkdir(&new_dir, CreateIntermediateDirectories).unwrap();
assert!(new_dir.exists());
let result =
try_mkdir(&new_dir, CreateIntermediateDirectoriesOnlyIfNotExists);
assert!(result.is_err());
assert!(matches!(result, Err(FsOpError::DirectoryAlreadyExists(_))));
let file_path = new_dir.join("test_file.txt");
fs::write(&file_path, "test").unwrap();
assert!(file_path.exists());
try_mkdir(&new_dir, CreateIntermediateDirectoriesAndPurgeExisting).unwrap();
assert!(new_dir.exists());
assert!(!file_path.exists());
});
}
fn test_try_change_directory_permissions_errors() {
with_saved_pwd!({
use crate::directory_change::try_cd;
let root = try_create_temp_dir().unwrap();
let new_tmp_dir =
fs_paths!(with_root: root => "test_change_dir_permissions_errors");
fs::create_dir_all(&new_tmp_dir).unwrap();
assert!(new_tmp_dir.exists());
let no_permissions_dir =
fs_paths!(with_root: new_tmp_dir => "no_permissions_dir");
fs::create_dir_all(&no_permissions_dir).unwrap();
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o000);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
assert!(no_permissions_dir.exists());
let result = try_cd(&no_permissions_dir);
assert!(result.is_err());
assert!(matches!(result, Err(FsOpError::PermissionDenied(_))));
let mut permissions =
fs::metadata(&no_permissions_dir).unwrap().permissions();
permissions.set_mode(0o777);
fs::set_permissions(&no_permissions_dir, permissions).unwrap();
});
}
fn test_try_change_directory_happy_path() {
with_saved_pwd!({
use crate::directory_change::try_cd;
let root = try_create_temp_dir().unwrap();
let new_tmp_dir = fs_paths!(with_root: root => "test_change_dir_happy_path");
fs::create_dir_all(&new_tmp_dir).unwrap();
assert!(new_tmp_dir.exists());
try_cd(&new_tmp_dir).unwrap();
assert_eq!(env::current_dir().unwrap(), new_tmp_dir);
try_cd(&root).unwrap();
assert_eq!(env::current_dir().unwrap(), *root);
});
}
fn test_try_change_directory_non_existent() {
with_saved_pwd!({
use crate::directory_change::try_cd;
let root = try_create_temp_dir().unwrap();
let new_tmp_dir =
fs_paths!(with_root: root => "test_change_dir_non_existent");
fs::create_dir_all(&new_tmp_dir).unwrap();
assert!(new_tmp_dir.exists());
let non_existent_dir =
fs_paths!(with_root: new_tmp_dir => "non_existent_dir");
let result = try_cd(&non_existent_dir);
assert!(result.is_err());
assert!(matches!(result, Err(FsOpError::DirectoryDoesNotExist(_))));
try_cd(&root).unwrap();
assert_eq!(env::current_dir().unwrap(), *root);
});
}
fn test_try_change_directory_invalid_name() {
with_saved_pwd!({
use crate::directory_change::try_cd;
let root = try_create_temp_dir().unwrap();
let new_tmp_dir =
fs_paths!(with_root: root => "test_change_dir_invalid_name");
fs::create_dir_all(&new_tmp_dir).unwrap();
assert!(new_tmp_dir.exists());
let invalid_name_dir =
fs_paths!(with_root: new_tmp_dir => "invalid_name_dir\0");
let result = try_cd(&invalid_name_dir);
assert!(result.is_err());
assert!(matches!(result, Err(FsOpError::InvalidName(_))));
try_cd(&root).unwrap();
assert_eq!(env::current_dir().unwrap(), *root);
});
}
#[allow(clippy::missing_errors_doc)]
fn run_all_fs_path_functions_sequentially_impl() {
test_try_directory_exists_not_found_error();
test_try_directory_exists_permissions_errors();
test_try_file_exists();
test_try_file_exists_invalid_name_error();
test_try_file_exists_permissions_errors();
test_try_pwd();
test_try_pwd_errors();
test_try_write();
test_try_mkdir();
test_try_change_directory_permissions_errors();
test_try_change_directory_happy_path();
test_try_change_directory_non_existent();
test_try_change_directory_invalid_name();
}
#[test]
fn test_all_fs_path_functions_in_isolated_process() {
if std::env::var("ISOLATED_TEST_RUNNER").is_ok() {
run_all_fs_path_functions_sequentially_impl();
std::process::exit(0);
}
let current_exe = std::env::current_exe().unwrap();
let mut cmd = std::process::Command::new(¤t_exe);
cmd.env("ISOLATED_TEST_RUNNER", "1")
.env("RUST_BACKTRACE", "1") .args([
"--test-threads",
"1",
"test_all_fs_path_functions_in_isolated_process",
]);
let output = cmd.output().expect("Failed to run isolated test");
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success()
|| stderr.contains("panicked at")
|| stderr.contains("Test failed with error")
{
eprintln!("Exit status: {:?}", output.status);
eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
eprintln!("Stderr: {stderr}");
panic!(
"Isolated test failed with status code {:?}: {}",
output.status.code(),
stderr
);
}
}
}