use std::{
io::ErrorKind,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use crate::error::{LmcppError, LmcppResult};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[repr(transparent)]
pub struct ValidDir(pub PathBuf);
impl ValidDir {
pub fn new<P: AsRef<Path>>(p: P) -> LmcppResult<Self> {
let path = p.as_ref();
let canonical = match path.canonicalize() {
Ok(abs) => abs,
Err(e) if e.kind() == ErrorKind::NotFound => {
std::fs::create_dir_all(path)
.map_err(|e| LmcppError::file_system("create dir", path, e))?;
path.canonicalize()
.map_err(|e| LmcppError::file_system("canonicalise dir", path, e))?
}
Err(e) => return Err(LmcppError::file_system("canonicalise dir", path, e)),
};
if !canonical.is_dir() {
return Err(LmcppError::file_system(
"ValidDir is_dir failed",
path,
std::io::Error::from(ErrorKind::NotADirectory),
));
}
Ok(Self(canonical))
}
pub fn reset(&self) -> LmcppResult<()> {
match std::fs::remove_dir_all(&self.0) {
Ok(_) => (), Err(e) if e.kind() == std::io::ErrorKind::NotFound => (), Err(e) => {
return Err(LmcppError::file_system("reset dir", &self.0, e));
}
}
std::fs::create_dir_all(&self.0)
.map_err(|e| LmcppError::file_system("recreate dir", &self.0, e))?;
Ok(())
}
pub fn remove(&self) -> LmcppResult<()> {
std::fs::remove_dir_all(self)
.map_err(|e| LmcppError::file_system("ValidDir remove dir", &self.0, e))?;
debug_assert!(!self.exists());
Ok(())
}
}
impl std::ops::Deref for ValidDir {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<Path> for ValidDir {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl TryFrom<PathBuf> for ValidDir {
type Error = LmcppError;
fn try_from(value: PathBuf) -> LmcppResult<Self> {
Self::new(value)
}
}
impl<'a> TryFrom<&'a Path> for ValidDir {
type Error = LmcppError;
fn try_from(value: &'a Path) -> LmcppResult<Self> {
Self::new(value)
}
}
impl<'a> TryFrom<&'a str> for ValidDir {
type Error = LmcppError;
fn try_from(value: &'a str) -> LmcppResult<Self> {
Self::new(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[repr(transparent)]
pub struct ValidFile(pub PathBuf);
impl ValidFile {
pub fn new<P: AsRef<Path>>(p: P) -> LmcppResult<Self> {
let mut path = p.as_ref().to_path_buf();
if !path.is_absolute() {
path = std::env::current_dir()
.map_err(|e| LmcppError::file_system("get current dir", &path, e))?
.join(path);
}
let meta = std::fs::symlink_metadata(&path)
.map_err(|e| LmcppError::file_system("fetch symlink metadata failed", &path, e))?;
if meta.file_type().is_dir() {
return Err(LmcppError::file_system(
"ValidFile is_dir failed",
&path,
std::io::Error::from(ErrorKind::NotADirectory),
));
}
if meta.file_type().is_symlink() {
let target_meta = std::fs::metadata(&path)
.map_err(|e| LmcppError::file_system("fetch path metadata", &path, e))?;
if target_meta.file_type().is_dir() {
return Err(LmcppError::file_system(
"ValidFile is_symlink failed",
&path,
std::io::Error::from(ErrorKind::InvalidInput),
));
}
}
Ok(Self(path))
}
pub fn make_executable(&self) -> LmcppResult<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&self, std::fs::Permissions::from_mode(0o755))
.map_err(|e| LmcppError::file_system("make executable", &self.0, e))?;
}
Ok(())
}
pub fn find_specific_file(root_dir: &ValidDir, target: &str) -> LmcppResult<ValidFile> {
fn walk(dir: &Path, target: &str) -> LmcppResult<ValidFile> {
for entry in
std::fs::read_dir(dir).map_err(|e| LmcppError::file_system("read dir", dir, e))?
{
let path = entry
.map_err(|e| LmcppError::file_system("read dir entry", dir, e))?
.path();
if path.is_file()
&& path
.file_name()
.and_then(|n| n.to_str())
.map_or(false, |n| n == target)
{
return ValidFile::new(path);
} else if path.is_dir() {
if let Ok(found) = walk(&path, target) {
return Ok(found);
}
}
}
Err(LmcppError::file_system(
"ValidDir find_specific_file failed",
dir,
std::io::Error::new(
ErrorKind::NotFound,
format!("Could not find `{}` in `{}`", target, dir.display()),
),
))
}
walk(root_dir.as_ref(), target)
}
}
impl std::ops::Deref for ValidFile {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<Path> for ValidFile {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl TryFrom<PathBuf> for ValidFile {
type Error = LmcppError;
fn try_from(value: PathBuf) -> LmcppResult<Self> {
Self::new(value)
}
}
impl<'a> TryFrom<&'a Path> for ValidFile {
type Error = LmcppError;
fn try_from(value: &'a Path) -> LmcppResult<Self> {
Self::new(value)
}
}
impl<'a> TryFrom<&'a str> for ValidFile {
type Error = LmcppError;
fn try_from(value: &'a str) -> LmcppResult<Self> {
Self::new(value)
}
}
#[cfg(test)]
mod tests {
use super::LmcppResult;
#[test]
fn valid_dir_new_creation_scenarios() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().to_path_buf();
for auto_create in [false, true] {
let target = if auto_create {
base.join("child")
} else {
base.clone()
};
if auto_create {
assert!(!target.exists(), "setup failure: dir should be absent");
}
let dir = super::ValidDir::new(&target)?;
assert!(dir.exists());
assert!(dir.is_absolute());
}
Ok(())
}
#[test]
fn valid_dir_rejects_file() -> LmcppResult<()> {
let tmp_file = tempfile::NamedTempFile::new().unwrap();
let err = super::ValidDir::new(tmp_file.path()).unwrap_err();
assert!(
err.to_string().contains("ValidDir is_dir failed"),
"err should contain 'ValidDir is_dir failed', but got: {}",
err
);
Ok(())
}
#[cfg(unix)]
#[test]
fn valid_dir_new_symlink_handling() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let dir_target = tmp.path().join("actual_dir");
std::fs::create_dir(&dir_target).unwrap();
let file_target = tmp.path().join("some_file");
std::fs::File::create(&file_target).unwrap();
let dir_link = tmp.path().join("dir_link");
let file_link = tmp.path().join("file_link");
std::os::unix::fs::symlink(&dir_target, &dir_link).unwrap();
std::os::unix::fs::symlink(&file_target, &file_link).unwrap();
for (link, expect_ok) in [(&dir_link, true), (&file_link, false)] {
let res = super::ValidDir::new(link);
assert_eq!(res.is_ok(), expect_ok, "link: {}", link.display());
if expect_ok {
let dir = res?;
assert_eq!(dir, super::ValidDir::new(&dir_target)?);
}
}
Ok(())
}
#[test]
fn valid_dir_reset_behaviour() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().to_path_buf();
for remove_before_reset in [false, true] {
let dir = super::ValidDir::new(&base)?;
let trash = dir.join("trash.txt");
std::fs::write(&trash, b"junk").unwrap();
assert!(trash.exists());
if remove_before_reset {
std::fs::remove_dir_all(&*dir).unwrap();
assert!(!dir.exists(), "directory should be gone before reset()");
}
dir.reset()?;
assert!(dir.exists());
assert!(
std::fs::read_dir(&*dir).unwrap().next().is_none(),
"directory not empty after reset()"
);
}
Ok(())
}
#[test]
fn valid_dir_remove_success_and_not_found() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let dir = super::ValidDir::new(tmp.path())?;
dir.remove()?;
assert!(!dir.exists());
let err = dir.remove().unwrap_err();
assert!(err.to_string().contains("ValidDir remove dir"));
Ok(())
}
#[test]
fn valid_file_new_absolute_and_relative() -> LmcppResult<()> {
let tmp_file = tempfile::NamedTempFile::new().unwrap();
let file = super::ValidFile::new(tmp_file.path())?;
assert!(file.exists());
assert!(file.is_absolute());
let cwd = std::env::current_dir().unwrap();
let tmp_dir = tempfile::tempdir().unwrap();
std::env::set_current_dir(tmp_dir.path()).unwrap();
let rel_path = std::path::Path::new("rel_file.txt");
std::fs::File::create(rel_path).unwrap();
let rel_file = super::ValidFile::new(rel_path)?;
assert!(rel_file.is_absolute());
std::env::set_current_dir(cwd).unwrap();
Ok(())
}
#[test]
fn valid_file_new_errors() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let no_exist = tmp.path().join("nope");
assert!(super::ValidFile::new(&no_exist).is_err());
let err = super::ValidFile::new(tmp.path()).unwrap_err();
assert!(
err.to_string().contains("ValidFile is_dir failed"),
"err should contain 'ValidFile is_dir failed', but got: {}",
err
);
Ok(())
}
#[cfg(unix)]
#[test]
fn valid_file_new_symlink_cases() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let tgt_file = tmp.path().join("target");
std::fs::File::create(&tgt_file).unwrap();
let ln_file = tmp.path().join("ln_file");
std::os::unix::fs::symlink(&tgt_file, &ln_file).unwrap();
super::ValidFile::new(&ln_file).unwrap();
let tgt_dir = tmp.path().join("dir");
std::fs::create_dir(&tgt_dir).unwrap();
let ln_dir = tmp.path().join("ln_dir");
std::os::unix::fs::symlink(&tgt_dir, &ln_dir).unwrap();
let err = super::ValidFile::new(&ln_dir).unwrap_err();
assert!(
err.to_string().contains("ValidFile is_symlink failed"),
"err should contain 'ValidFile is_symlink failed', but got: {}",
err
);
Ok(())
}
#[cfg(unix)]
#[test]
fn valid_file_make_executable() -> LmcppResult<()> {
use std::os::unix::fs::PermissionsExt;
let tmp_file = tempfile::NamedTempFile::new().unwrap();
let vf = super::ValidFile::new(tmp_file.path())?;
vf.make_executable()?;
let mode = std::fs::metadata(&*vf).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o755);
Ok(())
}
#[test]
fn find_specific_file_root_and_nested() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let root_dir = super::ValidDir::new(tmp.path())?;
let root_target = root_dir.join("hit.txt");
std::fs::File::create(&root_target).unwrap();
let found = super::ValidFile::find_specific_file(&root_dir, "hit.txt").unwrap();
assert_eq!(found.as_ref(), &root_target);
let sub = root_dir.join("a").join("b");
std::fs::create_dir_all(&sub).unwrap();
let nested_target = sub.join("deep.txt");
std::fs::File::create(&nested_target).unwrap();
let found_nested = super::ValidFile::find_specific_file(&root_dir, "deep.txt")?;
assert_eq!(found_nested.as_ref(), &nested_target);
Ok(())
}
#[test]
fn find_specific_file_not_found() -> LmcppResult<()> {
let tmp = tempfile::tempdir().unwrap();
let root_dir = super::ValidDir::new(tmp.path())?;
let err = super::ValidFile::find_specific_file(&root_dir, "ghost").unwrap_err();
assert!(
err.to_string()
.contains("ValidDir find_specific_file failed"),
"err should contain 'ValidDir find_specific_file failed', but got: {}",
err
);
Ok(())
}
#[test]
fn try_from_round_trip_variants() -> LmcppResult<()> {
let tmp_dir = tempfile::tempdir().unwrap();
let dir_path = tmp_dir.path();
let dir_str = dir_path.to_string_lossy();
let dir_variants: Vec<super::ValidDir> = vec![
std::convert::TryFrom::try_from(dir_str.as_ref())?,
std::convert::TryFrom::try_from(dir_path)?,
std::convert::TryFrom::try_from(dir_path.to_path_buf())?,
];
for a in &dir_variants {
for b in &dir_variants {
assert_eq!(a, b);
}
}
let tmp_file = tempfile::NamedTempFile::new().unwrap();
let file_path = tmp_file.path();
let file_str = file_path.to_string_lossy();
let file_variants: Vec<super::ValidFile> = vec![
std::convert::TryFrom::try_from(file_str.as_ref())?,
std::convert::TryFrom::try_from(file_path)?,
std::convert::TryFrom::try_from(file_path.to_path_buf())?,
];
for a in &file_variants {
for b in &file_variants {
assert_eq!(a, b);
}
}
Ok(())
}
}