Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for user provided closure to receive nonfatal errors #101

Merged
merged 4 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## 7.0.0

- Add support to `WhichConfig` for a user provided closure that will be called whenever a nonfatal error occurs.
This technically breaks a few APIs due to the need to add more generics and lifetimes. Most code will compile
without changes.

## 6.0.3

- Enhance `tracing` feature with some `debug` level logs for higher level logic.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "which"
version = "6.0.3"
version = "7.0.0"
edition = "2021"
rust-version = "1.70"
authors = ["Harry Fei <tiziyuanfang@gmail.com>"]
authors = ["Harry Fei <tiziyuanfang@gmail.com>, Jacob Kiesel <jake@bitcrafters.co>"]
repository = "https://github.com/harryfei/which-rs.git"
documentation = "https://docs.rs/which/"
license = "MIT"
Expand Down
88 changes: 66 additions & 22 deletions src/checker.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::finder::Checker;
use crate::{NonFatalError, NonFatalErrorHandler};
use std::fs;
use std::path::Path;

Expand All @@ -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<F: NonFatalErrorHandler>(
&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<F: NonFatalErrorHandler>(
&self,
_path: &Path,
_nonfatal_error_handler: &mut F,
) -> bool {
true
}
}
Expand All @@ -36,7 +53,11 @@ impl ExistedChecker {

impl Checker for ExistedChecker {
#[cfg(target_os = "windows")]
fn is_valid(&self, path: &Path) -> bool {
fn is_valid<F: NonFatalErrorHandler>(
&self,
path: &Path,
nonfatal_error_handler: &mut F,
) -> bool {
let ret = fs::symlink_metadata(path)
.map(|metadata| {
let file_type = metadata.file_type();
Expand All @@ -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}",
Expand All @@ -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<F: NonFatalErrorHandler>(
&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<F: NonFatalErrorHandler>(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<Box<dyn Checker>>,
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<dyn Checker>) -> 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<F: NonFatalErrorHandler>(
&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)
}
}
18 changes: 17 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::fmt;
use std::{fmt, io};

pub type Result<T> = std::result::Result<T, Error>;

Expand Down Expand Up @@ -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}"),
}
}
}
64 changes: 40 additions & 24 deletions src/finder.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,7 +25,11 @@ fn home_dir() -> Option<std::path::PathBuf> {
}

pub trait Checker {
fn is_valid(&self, path: &Path) -> bool;
fn is_valid<F: NonFatalErrorHandler>(
&self,
path: &Path,
nonfatal_error_handler: &mut F,
) -> bool;
}

trait PathExt {
Expand Down Expand Up @@ -62,17 +66,18 @@ impl Finder {
Finder
}

pub fn find<T, U, V>(
pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>(
&self,
binary_name: T,
paths: Option<U>,
cwd: Option<V>,
binary_checker: CompositeChecker,
) -> Result<impl Iterator<Item = PathBuf>>
mut nonfatal_error_handler: F,
) -> Result<impl Iterator<Item = PathBuf> + 'a>
where
T: AsRef<OsStr>,
U: AsRef<OsStr>,
V: AsRef<Path>,
V: AsRef<Path> + 'a,
{
let path = PathBuf::from(&binary_name);

Expand All @@ -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::<Vec<_>>();
let paths = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
let paths = env::split_paths(&paths).collect::<Vec<_>>();
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<T>(
pub fn find_re<T, F: NonFatalErrorHandler>(
&self,
binary_regex: impl Borrow<Regex>,
paths: Option<T>,
binary_checker: CompositeChecker,
mut nonfatal_error_handler: F,
) -> Result<impl Iterator<Item = PathBuf>>
where
T: AsRef<OsStr>,
Expand All @@ -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)
}
Expand Down Expand Up @@ -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<F: NonFatalErrorHandler>(
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));
}
}
}
}
Expand All @@ -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<F: NonFatalErrorHandler>(p: PathBuf, _nonfatal_error_handler: &mut F) -> PathBuf {
p
}
Loading