use std::convert::TryFrom;
use std::fmt::Display;
use regex::Regex;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use thiserror::Error;
use wildmatch::WildMatch;
#[cfg(feature = "file_traversal")]
use crate::file_traversal::PathFilterRet;
#[cfg(all(feature = "async", feature = "serde"))]
use crate::path_buf::PathBuf;
#[cfg(feature = "async")]
use async_std::path::Path;
#[cfg(all(feature = "async", not(feature = "serde")))]
use async_std::path::PathBuf;
#[cfg(not(feature = "async"))]
use std::path::{Path, PathBuf};
#[derive(Error, Debug)]
pub enum Error {
#[error("Ignore path '{0:?}' not found: {1:?}")]
FailedToCanonicalize(PathBuf, std::io::Error),
#[error(
"Ignore path '{0:?}' is neither a dir nor a regular file; \
Do not know how to use i."
)]
UnknownPathType(PathBuf),
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum IgnorePath {
Whole(PathBuf),
Prefix(PathBuf),
Glob(WildMatch),
#[cfg_attr(feature = "serde", serde(with = "serde_regex"))]
Regex(Regex),
}
impl IgnorePath {
#[must_use]
pub fn matches(&self, abs_path: &Path) -> bool {
match self {
Self::Whole(path) => <PathBuf as AsRef<Path>>::as_ref(path) == abs_path,
Self::Prefix(path) => abs_path.starts_with(<PathBuf as AsRef<Path>>::as_ref(path)), Self::Glob(glob) => glob.matches(abs_path.to_string_lossy().as_ref()),
Self::Regex(regex) => regex
.captures(abs_path.to_string_lossy().as_ref())
.is_some(),
}
}
#[cfg(feature = "file_traversal")]
#[must_use]
pub fn create_filter(
ignore_paths: Vec<Self>,
) -> Box<dyn Fn(&Path) -> PathFilterRet + Send + Sync> {
Box::new(move |file: &Path| {
let abs_path = into_absolute(file)?;
if ignore_paths
.iter()
.any(|ignore_path| ignore_path.matches(abs_path.as_ref()))
{
#[cfg(feature = "logging")]
log::debug!(
"Ignoring file '{}', because it is in the ignore paths list.",
file.display()
);
return Ok(false);
}
Ok(true)
})
}
}
impl Display for IgnorePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Whole(path) | Self::Prefix(path) => path.display().fmt(f),
Self::Glob(glob) => glob.fmt(f),
Self::Regex(regex) => regex.fmt(f),
}
}
}
impl TryFrom<&Path> for IgnorePath {
type Error = Error;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
let can_path =
into_absolute(path).map_err(|err| Error::FailedToCanonicalize(path.into(), err))?;
#[cfg_attr(not(feature = "async"), allow(clippy::useless_conversion))]
if can_path.is_file() {
Ok(Self::Whole(can_path.into()))
} else if can_path.is_dir() {
Ok(Self::Prefix(can_path.into()))
} else {
Err(Error::UnknownPathType(can_path.into()))
}
}
}
impl TryFrom<&str> for IgnorePath {
type Error = Error;
fn try_from(path_str: &str) -> Result<Self, Self::Error> {
Self::try_from(Path::new(path_str))
}
}
pub fn into_absolute<P: AsRef<Path>>(path: P) -> std::io::Result<std::path::PathBuf> {
std::fs::canonicalize(path.as_ref())
}
pub fn into_absolute_async<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
#[cfg_attr(not(feature = "async"), allow(clippy::useless_conversion))]
into_absolute(path.as_ref()).map(PathBuf::from)
}
pub fn parse(path_str: &str) -> Result<IgnorePath, String> {
IgnorePath::try_from(path_str).map_err(|err| format!("{err:?}"))
}
pub fn is_valid<S: AsRef<str>>(path_str: S) -> Result<(), String> {
parse(path_str.as_ref()).map(|_| ())
}