use crate::checker::is_valid;
use crate::helper::has_executable_extension;
use crate::sys::Sys;
use crate::sys::SysReadDirEntry;
use crate::{error::*, NonFatalErrorHandler};
#[cfg(feature = "regex")]
use regex::Regex;
#[cfg(feature = "regex")]
use std::borrow::Borrow;
use std::borrow::Cow;
use std::ffi::OsStr;
#[cfg(feature = "regex")]
use std::io;
use std::path::{Component, Path, PathBuf};
use std::vec;
trait PathExt {
fn has_separator(&self) -> bool;
fn to_absolute<P>(self, cwd: P) -> PathBuf
where
P: AsRef<Path>;
}
impl PathExt for PathBuf {
fn has_separator(&self) -> bool {
self.components().count() > 1
}
fn to_absolute<P>(self, cwd: P) -> PathBuf
where
P: AsRef<Path>,
{
if self.is_absolute() {
self
} else {
let mut new_path = PathBuf::from(cwd.as_ref());
new_path.extend(
self.components()
.skip_while(|c| matches!(c, Component::CurDir)),
);
new_path
}
}
}
pub struct Finder<TSys: Sys> {
sys: TSys,
}
impl<TSys: Sys> Finder<TSys> {
pub fn new(sys: TSys) -> Self {
Finder { sys }
}
pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>(
self,
binary_name: T,
paths: Option<U>,
cwd: Option<V>,
nonfatal_error_handler: F,
) -> Result<impl Iterator<Item = PathBuf> + 'a>
where
TSys: 'a,
T: AsRef<OsStr>,
U: AsRef<OsStr>,
V: AsRef<Path> + 'a,
{
let path = PathBuf::from(&binary_name);
#[cfg(feature = "tracing")]
tracing::debug!(
"query binary_name = {:?}, paths = {:?}, cwd = {:?}",
binary_name.as_ref().to_string_lossy(),
paths.as_ref().map(|p| p.as_ref().to_string_lossy()),
cwd.as_ref().map(|p| p.as_ref().display())
);
let ret = match cwd {
Some(cwd) if path.has_separator() => {
WhichFindIterator::new_cwd(path, cwd.as_ref(), self.sys, nonfatal_error_handler)
}
_ => {
#[cfg(feature = "tracing")]
tracing::trace!("{} has no path seperators, so only paths in PATH environment variable will be searched.", path.display());
let paths = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
let paths = self.sys.env_split_paths(paths.as_ref());
if paths.is_empty() {
return Err(Error::CannotGetCurrentDirAndPathListEmpty);
}
WhichFindIterator::new_paths(path, paths, self.sys, nonfatal_error_handler)
}
};
#[cfg(feature = "tracing")]
let ret = ret.inspect(|p| {
tracing::debug!("found path {}", p.display());
});
Ok(ret)
}
#[cfg(feature = "regex")]
pub fn find_re<T, F: NonFatalErrorHandler>(
self,
binary_regex: impl std::borrow::Borrow<Regex>,
paths: Option<T>,
nonfatal_error_handler: F,
) -> Result<impl Iterator<Item = PathBuf>>
where
T: AsRef<OsStr>,
{
WhichFindRegexIter::new(self.sys, paths, binary_regex, nonfatal_error_handler)
}
}
struct WhichFindIterator<TSys: Sys, F: NonFatalErrorHandler> {
sys: TSys,
paths: PathsIter<vec::IntoIter<PathBuf>>,
nonfatal_error_handler: F,
}
impl<TSys: Sys, F: NonFatalErrorHandler> WhichFindIterator<TSys, F> {
pub fn new_cwd(binary_name: PathBuf, cwd: &Path, sys: TSys, nonfatal_error_handler: F) -> Self {
let path_extensions = if sys.is_windows() {
sys.env_windows_path_ext()
} else {
Cow::Borrowed(Default::default())
};
Self {
sys,
paths: PathsIter {
paths: vec![binary_name.to_absolute(cwd)].into_iter(),
current_path_with_index: None,
path_extensions,
},
nonfatal_error_handler,
}
}
pub fn new_paths(
binary_name: PathBuf,
paths: Vec<PathBuf>,
sys: TSys,
nonfatal_error_handler: F,
) -> Self {
let path_extensions = if sys.is_windows() {
sys.env_windows_path_ext()
} else {
Cow::Borrowed(Default::default())
};
let paths = paths
.iter()
.map(|p| tilde_expansion(&sys, p).join(&binary_name))
.collect::<Vec<_>>();
Self {
sys,
paths: PathsIter {
paths: paths.into_iter(),
current_path_with_index: None,
path_extensions,
},
nonfatal_error_handler,
}
}
}
impl<TSys: Sys, F: NonFatalErrorHandler> Iterator for WhichFindIterator<TSys, F> {
type Item = PathBuf;
fn next(&mut self) -> Option<Self::Item> {
for path in &mut self.paths {
if is_valid(&self.sys, &path, &mut self.nonfatal_error_handler) {
return Some(correct_casing(
&self.sys,
path,
&mut self.nonfatal_error_handler,
));
}
}
None
}
}
struct PathsIter<P>
where
P: Iterator<Item = PathBuf>,
{
paths: P,
current_path_with_index: Option<(PathBuf, usize)>,
path_extensions: Cow<'static, [String]>,
}
impl<P> Iterator for PathsIter<P>
where
P: Iterator<Item = PathBuf>,
{
type Item = PathBuf;
fn next(&mut self) -> Option<Self::Item> {
if self.path_extensions.is_empty() {
self.paths.next()
} else if let Some((p, index)) = self.current_path_with_index.take() {
let next_index = index + 1;
if next_index < self.path_extensions.len() {
self.current_path_with_index = Some((p.clone(), next_index));
}
let mut p = p.into_os_string();
p.push(&self.path_extensions[index]);
let ret = PathBuf::from(p);
#[cfg(feature = "tracing")]
tracing::trace!("possible extension: {}", ret.display());
Some(ret)
} else {
let p = self.paths.next()?;
if has_executable_extension(&p, &self.path_extensions) {
#[cfg(feature = "tracing")]
tracing::trace!(
"{} already has an executable extension, not modifying it further",
p.display()
);
} else {
#[cfg(feature = "tracing")]
tracing::trace!(
"{} has no extension, using PATHEXT environment variable to infer one",
p.display()
);
self.current_path_with_index = Some((p.clone(), 0));
}
Some(p)
}
}
}
fn tilde_expansion<TSys: Sys>(sys: TSys, p: &Path) -> Cow<'_, Path> {
let mut component_iter = p.components();
if let Some(Component::Normal(o)) = component_iter.next() {
if o == "~" {
let new_path = sys.home_dir();
if let Some(mut new_path) = new_path {
new_path.extend(component_iter);
#[cfg(feature = "tracing")]
tracing::trace!(
"found tilde, substituting in user's home directory to get {}",
new_path.display()
);
return Cow::Owned(new_path);
} else {
#[cfg(feature = "tracing")]
tracing::trace!("found tilde in path, but user's home directory couldn't be found");
}
}
}
Cow::Borrowed(p)
}
fn correct_casing<TSys: Sys, F: NonFatalErrorHandler>(
sys: TSys,
mut p: PathBuf,
nonfatal_error_handler: &mut F,
) -> PathBuf {
if sys.is_windows() {
if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) {
if let Ok(iter) = sys.read_dir(parent) {
for e in iter {
match e {
Ok(e) => {
if e.file_name().eq_ignore_ascii_case(file_name) {
p.pop();
p.push(e.file_name());
break;
}
}
Err(e) => {
nonfatal_error_handler.handle(NonFatalError::Io(e));
}
}
}
}
}
}
p
}
#[cfg(feature = "regex")]
struct WhichFindRegexIter<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> {
sys: TSys,
re: B,
paths: vec::IntoIter<PathBuf>,
nonfatal_error_handler: F,
current_read_dir_iter: Option<Box<dyn Iterator<Item = io::Result<TSys::ReadDirEntry>>>>,
}
#[cfg(feature = "regex")]
impl<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> WhichFindRegexIter<TSys, B, F> {
pub fn new<T: AsRef<OsStr>>(
sys: TSys,
paths: Option<T>,
re: B,
nonfatal_error_handler: F,
) -> Result<Self> {
let p = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
let paths = sys.env_split_paths(p.as_ref());
Ok(WhichFindRegexIter {
sys,
re,
paths: paths.into_iter(),
nonfatal_error_handler,
current_read_dir_iter: None,
})
}
}
#[cfg(feature = "regex")]
impl<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> Iterator
for WhichFindRegexIter<TSys, B, F>
{
type Item = PathBuf;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(iter) = &mut self.current_read_dir_iter {
match iter.next() {
Some(Ok(path)) => {
if let Some(unicode_file_name) = path.file_name().to_str() {
if self.re.borrow().is_match(unicode_file_name) {
return Some(path.path());
} else {
#[cfg(feature = "tracing")]
tracing::debug!("regex filtered out {}", unicode_file_name);
}
} else {
#[cfg(feature = "tracing")]
tracing::debug!("regex unable to evaluate filename as it's not valid unicode. Lossy filename conversion: {}", path.file_name().to_string_lossy());
}
}
Some(Err(e)) => {
self.nonfatal_error_handler.handle(NonFatalError::Io(e));
}
None => {
self.current_read_dir_iter = None;
}
}
} else {
let path = self.paths.next();
if let Some(path) = path {
match self.sys.read_dir(&path) {
Ok(new_read_dir_iter) => {
self.current_read_dir_iter = Some(new_read_dir_iter);
}
Err(e) => {
self.nonfatal_error_handler.handle(NonFatalError::Io(e));
}
}
} else {
return None;
}
}
}
}
}