diff --git a/src/checker.rs b/src/checker.rs index a1c924d..41490ef 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -1,4 +1,5 @@ use crate::finder::Checker; +use crate::{NonFatalError, NonFatalErrorHandler}; use std::fs; use std::path::Path; @@ -12,16 +13,32 @@ impl ExecutableChecker { impl Checker for ExecutableChecker { #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] - fn is_valid(&self, path: &Path) -> bool { + fn is_valid( + &self, + path: &Path, + nonfatal_error_handler: &mut F, + ) -> bool { + use std::io; + use rustix::fs as rfs; - let ret = rfs::access(path, rfs::Access::EXEC_OK).is_ok(); + let ret = rfs::access(path, rfs::Access::EXEC_OK) + .map_err(|e| { + nonfatal_error_handler.handle(NonFatalError::Io(io::Error::from_raw_os_error( + e.raw_os_error(), + ))) + }) + .is_ok(); #[cfg(feature = "tracing")] tracing::trace!("{} EXEC_OK = {ret}", path.display()); ret } #[cfg(windows)] - fn is_valid(&self, _path: &Path) -> bool { + fn is_valid( + &self, + _path: &Path, + _nonfatal_error_handler: &mut F, + ) -> bool { true } } @@ -36,7 +53,11 @@ impl ExistedChecker { impl Checker for ExistedChecker { #[cfg(target_os = "windows")] - fn is_valid(&self, path: &Path) -> bool { + fn is_valid( + &self, + path: &Path, + nonfatal_error_handler: &mut F, + ) -> bool { let ret = fs::symlink_metadata(path) .map(|metadata| { let file_type = metadata.file_type(); @@ -49,8 +70,11 @@ impl Checker for ExistedChecker { ); file_type.is_file() || file_type.is_symlink() }) + .map_err(|e| { + nonfatal_error_handler.handle(NonFatalError::Io(e)); + }) .unwrap_or(false) - && (path.extension().is_some() || matches_arch(path)); + && (path.extension().is_some() || matches_arch(path, nonfatal_error_handler)); #[cfg(feature = "tracing")] tracing::trace!( "{} has_extension = {}, ExistedChecker::is_valid() = {ret}", @@ -61,43 +85,63 @@ impl Checker for ExistedChecker { } #[cfg(not(target_os = "windows"))] - fn is_valid(&self, path: &Path) -> bool { - let ret = fs::metadata(path) - .map(|metadata| metadata.is_file()) - .unwrap_or(false); + fn is_valid( + &self, + path: &Path, + nonfatal_error_handler: &mut F, + ) -> bool { + let ret = fs::metadata(path).map(|metadata| metadata.is_file()); #[cfg(feature = "tracing")] - tracing::trace!("{} is_file() = {ret}", path.display()); - ret + tracing::trace!("{} is_file() = {ret:?}", path.display()); + match ret { + Ok(ret) => ret, + Err(e) => { + nonfatal_error_handler.handle(NonFatalError::Io(e)); + false + } + } } } #[cfg(target_os = "windows")] -fn matches_arch(path: &Path) -> bool { - let ret = winsafe::GetBinaryType(&path.display().to_string()).is_ok(); +fn matches_arch(path: &Path, nonfatal_error_handler: &mut F) -> bool { + use std::io; + + let ret = winsafe::GetBinaryType(&path.display().to_string()) + .map_err(|e| { + nonfatal_error_handler.handle(NonFatalError::Io(io::Error::from_raw_os_error( + e.raw() as i32 + ))) + }) + .is_ok(); #[cfg(feature = "tracing")] tracing::trace!("{} matches_arch() = {ret}", path.display()); ret } pub struct CompositeChecker { - checkers: Vec>, + existed_checker: ExistedChecker, + executable_checker: ExecutableChecker, } impl CompositeChecker { pub fn new() -> CompositeChecker { CompositeChecker { - checkers: Vec::new(), + executable_checker: ExecutableChecker::new(), + existed_checker: ExistedChecker::new(), } } - - pub fn add_checker(mut self, checker: Box) -> CompositeChecker { - self.checkers.push(checker); - self - } } impl Checker for CompositeChecker { - fn is_valid(&self, path: &Path) -> bool { - self.checkers.iter().all(|checker| checker.is_valid(path)) + fn is_valid( + &self, + path: &Path, + nonfatal_error_handler: &mut F, + ) -> bool { + self.existed_checker.is_valid(path, nonfatal_error_handler) + && self + .executable_checker + .is_valid(path, nonfatal_error_handler) } } diff --git a/src/error.rs b/src/error.rs index 6b6f285..9e3ed5f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{fmt, io}; pub type Result = std::result::Result; @@ -26,3 +26,19 @@ impl fmt::Display for Error { } } } + +#[derive(Debug)] +#[non_exhaustive] +pub enum NonFatalError { + Io(io::Error), +} + +impl std::error::Error for NonFatalError {} + +impl fmt::Display for NonFatalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(e) => write!(f, "{e}"), + } + } +} diff --git a/src/finder.rs b/src/finder.rs index 35f3153..c29ea1d 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -1,7 +1,7 @@ use crate::checker::CompositeChecker; -use crate::error::*; #[cfg(windows)] use crate::helper::has_executable_extension; +use crate::{error::*, NonFatalErrorHandler}; use either::Either; #[cfg(feature = "regex")] use regex::Regex; @@ -25,7 +25,11 @@ fn home_dir() -> Option { } pub trait Checker { - fn is_valid(&self, path: &Path) -> bool; + fn is_valid( + &self, + path: &Path, + nonfatal_error_handler: &mut F, + ) -> bool; } trait PathExt { @@ -62,17 +66,18 @@ impl Finder { Finder } - pub fn find( + pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>( &self, binary_name: T, paths: Option, cwd: Option, binary_checker: CompositeChecker, - ) -> Result> + mut nonfatal_error_handler: F, + ) -> Result + 'a> where T: AsRef, U: AsRef, - V: AsRef, + V: AsRef + 'a, { let path = PathBuf::from(&binary_name); @@ -92,39 +97,40 @@ impl Finder { path.display() ); // Search binary in cwd if the path have a path separator. - Either::Left(Self::cwd_search_candidates(path, cwd).into_iter()) + Either::Left(Self::cwd_search_candidates(path, cwd)) } _ => { #[cfg(feature = "tracing")] tracing::trace!("{} has no path seperators, so only paths in PATH environment variable will be searched.", path.display()); // Search binary in PATHs(defined in environment variable). - let paths = - env::split_paths(&paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?) - .collect::>(); + let paths = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?; + let paths = env::split_paths(&paths).collect::>(); if paths.is_empty() { return Err(Error::CannotGetCurrentDirAndPathListEmpty); } - Either::Right(Self::path_search_candidates(path, paths).into_iter()) + Either::Right(Self::path_search_candidates(path, paths)) } }; - let ret = binary_path_candidates - .filter(move |p| binary_checker.is_valid(p)) - .map(correct_casing); + let ret = binary_path_candidates.into_iter().filter_map(move |p| { + binary_checker + .is_valid(&p, &mut nonfatal_error_handler) + .then(|| correct_casing(p, &mut nonfatal_error_handler)) + }); #[cfg(feature = "tracing")] - let ret = ret.map(|p| { + let ret = ret.inspect(|p| { tracing::debug!("found path {}", p.display()); - p }); Ok(ret) } #[cfg(feature = "regex")] - pub fn find_re( + pub fn find_re( &self, binary_regex: impl Borrow, paths: Option, binary_checker: CompositeChecker, + mut nonfatal_error_handler: F, ) -> Result> where T: AsRef, @@ -148,7 +154,7 @@ impl Finder { false } }) - .filter(move |p| binary_checker.is_valid(p)); + .filter(move |p| binary_checker.is_valid(p, &mut nonfatal_error_handler)); Ok(matching_re) } @@ -277,14 +283,24 @@ fn tilde_expansion(p: &PathBuf) -> Cow<'_, PathBuf> { } #[cfg(target_os = "windows")] -fn correct_casing(mut p: PathBuf) -> PathBuf { +fn correct_casing( + mut p: PathBuf, + nonfatal_error_handler: &mut F, +) -> PathBuf { if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) { if let Ok(iter) = fs::read_dir(parent) { - for e in iter.filter_map(std::result::Result::ok) { - if e.file_name().eq_ignore_ascii_case(file_name) { - p.pop(); - p.push(e.file_name()); - break; + 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)); + } } } } @@ -293,6 +309,6 @@ fn correct_casing(mut p: PathBuf) -> PathBuf { } #[cfg(not(target_os = "windows"))] -fn correct_casing(p: PathBuf) -> PathBuf { +fn correct_casing(p: PathBuf, _nonfatal_error_handler: &mut F) -> PathBuf { p } diff --git a/src/lib.rs b/src/lib.rs index 8490cb6..78fd19a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ use std::path; use std::ffi::{OsStr, OsString}; -use crate::checker::{CompositeChecker, ExecutableChecker, ExistedChecker}; +use crate::checker::CompositeChecker; pub use crate::error::*; use crate::finder::Finder; @@ -87,26 +87,25 @@ pub fn which_global>(binary_name: T) -> Result { pub fn which_all>(binary_name: T) -> Result> { let cwd = env::current_dir().ok(); - let binary_checker = build_binary_checker(); - - let finder = Finder::new(); - - finder.find(binary_name, env::var_os("PATH"), cwd, binary_checker) + Finder::new().find( + binary_name, + env::var_os("PATH"), + cwd, + CompositeChecker::new(), + Noop, + ) } /// Find all binaries with `binary_name` ignoring `cwd`. pub fn which_all_global>( binary_name: T, ) -> Result> { - let binary_checker = build_binary_checker(); - - let finder = Finder::new(); - - finder.find( + Finder::new().find( binary_name, env::var_os("PATH"), Option::<&Path>::None, - binary_checker, + CompositeChecker::new(), + Noop, ) } @@ -189,29 +188,21 @@ pub fn which_re_in( where T: AsRef, { - let binary_checker = build_binary_checker(); - - let finder = Finder::new(); - - finder.find_re(regex, paths, binary_checker) + Finder::new().find_re(regex, paths, CompositeChecker::new(), Noop) } /// Find all binaries with `binary_name` in the path list `paths`, using `cwd` to resolve relative paths. -pub fn which_in_all( +pub fn which_in_all<'a, T, U, V>( binary_name: T, paths: Option, cwd: V, -) -> Result> +) -> Result + 'a> where T: AsRef, U: AsRef, - V: AsRef, + V: AsRef + 'a, { - let binary_checker = build_binary_checker(); - - let finder = Finder::new(); - - finder.find(binary_name, paths, Some(cwd), binary_checker) + Finder::new().find(binary_name, paths, Some(cwd), CompositeChecker::new(), Noop) } /// Find all binaries with `binary_name` in the path list `paths`, ignoring `cwd`. @@ -223,34 +214,60 @@ where T: AsRef, U: AsRef, { - let binary_checker = build_binary_checker(); - - let finder = Finder::new(); - - finder.find(binary_name, paths, Option::<&Path>::None, binary_checker) -} - -fn build_binary_checker() -> CompositeChecker { - CompositeChecker::new() - .add_checker(Box::new(ExistedChecker::new())) - .add_checker(Box::new(ExecutableChecker::new())) + Finder::new().find( + binary_name, + paths, + Option::<&Path>::None, + CompositeChecker::new(), + Noop, + ) } /// A wrapper containing all functionality in this crate. -pub struct WhichConfig { +pub struct WhichConfig { cwd: Option>, custom_path_list: Option, binary_name: Option, + nonfatal_error_handler: F, #[cfg(feature = "regex")] regex: Option, } -impl Default for WhichConfig { +/// A handler for non-fatal errors which does nothing with them. +#[derive(Default, Debug, Clone)] +pub struct Noop; + +/// Defines what should happen when a nonfatal error is encountered. A nonfatal error may represent a problem, +/// but it doesn't necessarily require `which` to stop its search. +/// +/// This trait is implemented for any closure or function that takes a single argument which is a [`NonFatalError`]. +/// You may also implement it for your own types. +pub trait NonFatalErrorHandler { + fn handle(&mut self, e: NonFatalError); +} + +impl NonFatalErrorHandler for Noop { + fn handle(&mut self, _: NonFatalError) { + // Do nothing + } +} + +impl NonFatalErrorHandler for T +where + T: FnMut(NonFatalError), +{ + fn handle(&mut self, e: NonFatalError) { + (self)(e); + } +} + +impl Default for WhichConfig { fn default() -> Self { Self { cwd: Some(either::Either::Left(true)), custom_path_list: None, binary_name: None, + nonfatal_error_handler: F::default(), #[cfg(feature = "regex")] regex: None, } @@ -263,11 +280,13 @@ type Regex = regex::Regex; #[cfg(not(feature = "regex"))] type Regex = (); -impl WhichConfig { +impl WhichConfig { pub fn new() -> Self { Self::default() } +} +impl<'a, F: NonFatalErrorHandler + 'a> WhichConfig { /// Whether or not to use the current working directory. `true` by default. /// /// # Panics @@ -352,6 +371,48 @@ impl WhichConfig { self } + /// Sets a closure that will receive non-fatal errors. You can also pass in other types + /// that implement [`NonFatalErrorHandler`]. + /// + /// # Example + /// ``` + /// # use which::WhichConfig; + /// let mut nonfatal_errors = Vec::new(); + /// + /// WhichConfig::new() + /// .binary_name("tar".into()) + /// .nonfatal_error_handler(|e| nonfatal_errors.push(e)) + /// .all_results() + /// .unwrap() + /// .collect::>(); + /// + /// if !nonfatal_errors.is_empty() { + /// println!("nonfatal errors encountered: {nonfatal_errors:?}"); + /// } + /// ``` + /// + /// You could also log it if you choose + /// + /// ``` + /// # use which::WhichConfig; + /// WhichConfig::new() + /// .binary_name("tar".into()) + /// .nonfatal_error_handler(|e| eprintln!("{e}")) + /// .all_results() + /// .unwrap() + /// .collect::>(); + /// ``` + pub fn nonfatal_error_handler(self, handler: NewF) -> WhichConfig { + WhichConfig { + custom_path_list: self.custom_path_list, + cwd: self.cwd, + binary_name: self.binary_name, + nonfatal_error_handler: handler, + #[cfg(feature = "regex")] + regex: self.regex, + } + } + /// Finishes configuring, runs the query and returns the first result. pub fn first_result(self) -> Result { self.all_results() @@ -359,18 +420,19 @@ impl WhichConfig { } /// Finishes configuring, runs the query and returns all results. - pub fn all_results(self) -> Result> { - let binary_checker = build_binary_checker(); - - let finder = Finder::new(); - + pub fn all_results(self) -> Result + 'a> { let paths = self.custom_path_list.or_else(|| env::var_os("PATH")); #[cfg(feature = "regex")] if let Some(regex) = self.regex { - return finder - .find_re(regex, paths, binary_checker) - .map(|i| Box::new(i) as Box>); + return Finder::new() + .find_re( + regex, + paths, + CompositeChecker::new(), + self.nonfatal_error_handler, + ) + .map(|i| Box::new(i) as Box + 'a>); } let cwd = match self.cwd { @@ -379,16 +441,17 @@ impl WhichConfig { None | Some(either::Either::Left(true)) => env::current_dir().ok(), }; - finder + Finder::new() .find( self.binary_name.expect( "binary_name not set! You must set binary_name or regex before searching!", ), paths, cwd, - binary_checker, + CompositeChecker::new(), + self.nonfatal_error_handler, ) - .map(|i| Box::new(i) as Box>) + .map(|i| Box::new(i) as Box + 'a>) } } @@ -439,15 +502,15 @@ impl Path { /// current working directory `cwd` to resolve relative paths. /// /// This calls `which_in_all` and maps the results into a `Path`. - pub fn all_in( + pub fn all_in<'a, T, U, V>( binary_name: T, paths: Option, cwd: V, - ) -> Result> + ) -> Result + 'a> where T: AsRef, U: AsRef, - V: AsRef, + V: AsRef + 'a, { which_in_all(binary_name, paths, cwd).map(|inner| inner.map(|inner| Path { inner })) } @@ -564,15 +627,15 @@ impl CanonicalPath { /// using the current working directory `cwd` to resolve relative paths. /// /// This calls `which_in_all` and `Path::canonicalize` and maps the result into a `CanonicalPath`. - pub fn all_in( + pub fn all_in<'a, T, U, V>( binary_name: T, paths: Option, cwd: V, - ) -> Result>> + ) -> Result> + 'a> where T: AsRef, U: AsRef, - V: AsRef, + V: AsRef + 'a, { which_in_all(binary_name, paths, cwd).map(|inner| { inner.map(|inner| { diff --git a/tests/basic.rs b/tests/basic.rs index 45a995f..120f878 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -128,7 +128,7 @@ fn _which>(f: &TestFixture, path: T) -> which::Result + 'a>( f: &'a TestFixture, path: T, -) -> which::Result> + '_> { +) -> which::Result> + 'a> { which::CanonicalPath::all_in(path, Some(f.paths.clone()), f.tempdir.path()) }