From c1a94f87fc1b6ca37516323fecc1efb9191bd248 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 1 Jan 2025 18:52:47 -0500 Subject: [PATCH 1/9] feat: add Sys trait for swapping out system --- .github/workflows/rust.yml | 6 +- Cargo.toml | 8 +- clippy.toml | 48 +++++++++++ src/checker.rs | 170 ++++++++++++++++--------------------- src/finder.rs | 122 ++++++++++++-------------- src/lib.rs | 125 +++++++++++++++++++-------- src/sys.rs | 155 +++++++++++++++++++++++++++++++++ 7 files changed, 428 insertions(+), 206 deletions(-) create mode 100644 clippy.toml create mode 100644 src/sys.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index edd3334..389f171 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -61,7 +61,11 @@ jobs: target: ${{ matrix.target }} - name: Build | Check - run: cargo check --workspace --target ${{ matrix.target }} + run: | + cargo check --workspace --target ${{ matrix.target }} + cargo check --workspace --target ${{ matrix.target }} --features regex + cargo check --workspace --target ${{ matrix.target }} --no-default-features + cargo check --workspace --target ${{ matrix.target }} --no-default-features --features regex # Run tests on Linux, macOS, and Windows # On both Rust stable and Rust nightly diff --git a/Cargo.toml b/Cargo.toml index 54e2a72..9c526b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,10 @@ categories = ["os", "filesystem"] keywords = ["which", "which-rs", "unix", "command"] [features] +default = ["real-sys"] regex = ["dep:regex"] tracing = ["dep:tracing"] +real-sys = ["env_home", "rustix", "winsafe"] [dependencies] either = "1.9.0" @@ -22,13 +24,13 @@ regex = { version = "1.10.2", optional = true } tracing = { version = "0.1.40", default-features = false, optional = true } [target.'cfg(any(windows, unix, target_os = "redox"))'.dependencies] -env_home = "0.1.0" +env_home = { version = "0.1.0", optional = true } [target.'cfg(any(unix, target_os = "wasi", target_os = "redox"))'.dependencies] -rustix = { version = "0.38.30", default-features = false, features = ["fs", "std"] } +rustix = { version = "0.38.30", default-features = false, features = ["fs", "std"], optional = true } [target.'cfg(windows)'.dependencies] -winsafe = { version = "0.0.19", features = ["kernel"] } +winsafe = { version = "0.0.19", features = ["kernel"], optional = true } [dev-dependencies] tempfile = "3.9.0" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..5f08391 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,48 @@ +disallowed-methods = [ + { path = "std::env::current_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::canonicalize", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::is_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::is_file", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::is_symlink", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::metadata", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::read_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::read_link", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::symlink_metadata", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::try_exists", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::exists", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::canonicalize", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::is_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::is_file", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::is_symlink", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::metadata", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::read_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::read_link", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::symlink_metadata", reason = "System operations should be done using Sys trait" }, + { path = "std::path::PathBuf::try_exists", reason = "System operations should be done using Sys trait" }, + { path = "std::env::set_current_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::env::split_paths", reason = "System operations should be done using Sys trait" }, + { path = "std::env::temp_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::env::var", reason = "System operations should be done using Sys trait" }, + { path = "std::env::var_os", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::canonicalize", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::copy", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::create_dir_all", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::create_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::DirBuilder::new", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::hard_link", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::metadata", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::OpenOptions::new", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::read_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::read_link", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::read_to_string", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::read", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::remove_dir_all", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::remove_dir", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::remove_file", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::rename", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::set_permissions", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::symlink_metadata", reason = "System operations should be done using Sys trait" }, + { path = "std::fs::write", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::canonicalize", reason = "System operations should be done using Sys trait" }, + { path = "std::path::Path::exists", reason = "System operations should be done using Sys trait" }, +] diff --git a/src/checker.rs b/src/checker.rs index 41490ef..e356f3e 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -1,139 +1,111 @@ use crate::finder::Checker; +use crate::sys::Sys; +use crate::sys::SysMetadata; use crate::{NonFatalError, NonFatalErrorHandler}; -use std::fs; use std::path::Path; -pub struct ExecutableChecker; +pub struct ExecutableChecker { + sys: TSys, +} -impl ExecutableChecker { - pub fn new() -> ExecutableChecker { - ExecutableChecker +impl ExecutableChecker { + pub fn new(sys: TSys) -> Self { + Self { sys } } } -impl Checker for ExecutableChecker { - #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] +impl Checker for ExecutableChecker { 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) - .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, - _nonfatal_error_handler: &mut F, - ) -> bool { - true + if self.sys.is_windows() && path.extension().is_some() { + true + } else { + let ret = self + .sys + .is_valid_executable(path) + .map_err(|e| nonfatal_error_handler.handle(NonFatalError::Io(e))) + .unwrap_or(false); + #[cfg(feature = "tracing")] + tracing::trace!("{} EXEC_OK = {ret}", path.display()); + ret + } } } -pub struct ExistedChecker; - -impl ExistedChecker { - pub fn new() -> ExistedChecker { - ExistedChecker - } +pub struct ExistedChecker { + sys: TSys, } -impl Checker for ExistedChecker { - #[cfg(target_os = "windows")] - 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(); - #[cfg(feature = "tracing")] - tracing::trace!( - "{} is_file() = {}, is_symlink() = {}", - path.display(), - file_type.is_file(), - file_type.is_symlink() - ); - 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, nonfatal_error_handler)); - #[cfg(feature = "tracing")] - tracing::trace!( - "{} has_extension = {}, ExistedChecker::is_valid() = {ret}", - path.display(), - path.extension().is_some() - ); - ret +impl ExistedChecker { + pub fn new(sys: TSys) -> Self { + Self { sys } } +} - #[cfg(not(target_os = "windows"))] +impl Checker for ExistedChecker { 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()); - match ret { - Ok(ret) => ret, - Err(e) => { - nonfatal_error_handler.handle(NonFatalError::Io(e)); - false + if self.sys.is_windows() { + let ret = self + .sys + .symlink_metadata(path) + .map(|metadata| { + #[cfg(feature = "tracing")] + tracing::trace!( + "{} is_file() = {}, is_symlink() = {}", + path.display(), + metadata.is_file(), + metadata.is_symlink() + ); + metadata.is_file() || metadata.is_symlink() + }) + .map_err(|e| { + nonfatal_error_handler.handle(NonFatalError::Io(e)); + }) + .unwrap_or(false); + #[cfg(feature = "tracing")] + tracing::trace!( + "{} has_extension = {}, ExistedChecker::is_valid() = {ret}", + path.display(), + path.extension().is_some() + ); + ret + } else { + let ret = self.sys.metadata(path).map(|metadata| metadata.is_file()); + #[cfg(feature = "tracing")] + 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, 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 { - existed_checker: ExistedChecker, - executable_checker: ExecutableChecker, +pub struct CompositeChecker { + existed_checker: ExistedChecker, + executable_checker: ExecutableChecker, } -impl CompositeChecker { - pub fn new() -> CompositeChecker { +impl CompositeChecker { + pub fn new(sys: TSys) -> Self { CompositeChecker { - executable_checker: ExecutableChecker::new(), - existed_checker: ExistedChecker::new(), + executable_checker: ExecutableChecker::new(sys.clone()), + existed_checker: ExistedChecker::new(sys), } } } -impl Checker for CompositeChecker { +impl Checker for CompositeChecker { fn is_valid( &self, path: &Path, diff --git a/src/finder.rs b/src/finder.rs index 7c59a5e..4814fc2 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -1,29 +1,16 @@ use crate::checker::CompositeChecker; -#[cfg(windows)] use crate::helper::has_executable_extension; +use crate::sys::Sys; +use crate::sys::SysReadDirEntry; use crate::{error::*, NonFatalErrorHandler}; use either::Either; #[cfg(feature = "regex")] use regex::Regex; -#[cfg(feature = "regex")] -use std::borrow::Borrow; use std::borrow::Cow; -use std::env; use std::ffi::OsStr; -#[cfg(any(feature = "regex", target_os = "windows"))] -use std::fs; use std::iter; use std::path::{Component, Path, PathBuf}; -// Home dir shim, use env_home crate when possible. Otherwise, return None -#[cfg(any(windows, unix, target_os = "redox"))] -use env_home::env_home_dir; - -#[cfg(not(any(windows, unix, target_os = "redox")))] -fn env_home_dir() -> Option { - None -} - pub trait Checker { fn is_valid( &self, @@ -59,11 +46,13 @@ impl PathExt for PathBuf { } } -pub struct Finder; +pub struct Finder { + sys: TSys, +} -impl Finder { - pub fn new() -> Finder { - Finder +impl Finder { + pub fn new(sys: TSys) -> Self { + Finder { sys } } pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>( @@ -71,7 +60,7 @@ impl Finder { binary_name: T, paths: Option, cwd: Option, - binary_checker: CompositeChecker, + binary_checker: CompositeChecker, mut nonfatal_error_handler: F, ) -> Result + 'a> where @@ -89,6 +78,7 @@ impl Finder { cwd.as_ref().map(|p| p.as_ref().display()) ); + let sys = self.sys.clone(); let binary_path_candidates = match cwd { Some(cwd) if path.has_separator() => { #[cfg(feature = "tracing")] @@ -97,25 +87,25 @@ impl Finder { path.display() ); // Search binary in cwd if the path have a path separator. - Either::Left(Self::cwd_search_candidates(path, cwd)) + Either::Left(Self::cwd_search_candidates(sys.clone(), 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 = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?; - let paths = env::split_paths(&paths).collect::>(); + let paths = sys.env_split_paths(paths.as_ref()); if paths.is_empty() { return Err(Error::CannotGetCurrentDirAndPathListEmpty); } - Either::Right(Self::path_search_candidates(path, paths)) + Either::Right(Self::path_search_candidates(sys.clone(), path, paths)) } }; 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)) + .then(|| correct_casing(&sys, p, &mut nonfatal_error_handler)) }); #[cfg(feature = "tracing")] let ret = ret.inspect(|p| { @@ -127,9 +117,9 @@ impl Finder { #[cfg(feature = "regex")] pub fn find_re( &self, - binary_regex: impl Borrow, + binary_regex: impl std::borrow::Borrow, paths: Option, - binary_checker: CompositeChecker, + binary_checker: CompositeChecker, mut nonfatal_error_handler: F, ) -> Result> where @@ -141,9 +131,10 @@ impl Finder { #[allow(clippy::needless_collect)] let paths: Vec<_> = env::split_paths(&p).collect(); + let sys = self.sys.clone(); let matching_re = paths .into_iter() - .flat_map(fs::read_dir) + .flat_map(move |p| sys.read_dir(&p)) .flatten() .flatten() .map(|e| e.path()) @@ -159,39 +150,36 @@ impl Finder { Ok(matching_re) } - fn cwd_search_candidates(binary_name: PathBuf, cwd: C) -> impl IntoIterator + fn cwd_search_candidates( + sys: TSys, + binary_name: PathBuf, + cwd: C, + ) -> impl IntoIterator where C: AsRef, { let path = binary_name.to_absolute(cwd); - Self::append_extension(iter::once(path)) + Self::append_extension(sys, iter::once(path)) } fn path_search_candidates

( + sys: TSys, binary_name: PathBuf, paths: P, ) -> impl IntoIterator where P: IntoIterator, { - let new_paths = paths - .into_iter() - .map(move |p| tilde_expansion(&p).join(binary_name.clone())); - - Self::append_extension(new_paths) - } + let new_paths = paths.into_iter().map({ + let sys = sys.clone(); + move |p| tilde_expansion(&sys, &p).join(binary_name.clone()) + }); - #[cfg(not(windows))] - fn append_extension

(paths: P) -> impl IntoIterator - where - P: IntoIterator, - { - paths + Self::append_extension(sys, new_paths) } - #[cfg(windows)] - fn append_extension

(paths: P) -> impl IntoIterator + fn append_extension

(sys: TSys, paths: P) -> impl IntoIterator where P: IntoIterator, { @@ -206,8 +194,13 @@ impl Finder { paths .into_iter() .flat_map(move |p| -> Box> { - let path_extensions = PATH_EXTENSIONS.get_or_init(|| { - env::var("PATHEXT") + if !sys.is_windows() { + return Box::new(iter::once(p)); + } + + let sys = sys.clone(); + let path_extensions = PATH_EXTENSIONS.get_or_init(move || { + sys.env_var("PATHEXT") .map(|pathext| { pathext .split(';') @@ -262,11 +255,11 @@ impl Finder { } } -fn tilde_expansion(p: &PathBuf) -> Cow<'_, PathBuf> { +fn tilde_expansion<'a, TSys: Sys>(sys: &TSys, p: &'a PathBuf) -> Cow<'a, PathBuf> { let mut component_iter = p.components(); if let Some(Component::Normal(o)) = component_iter.next() { if o == "~" { - let new_path = env_home_dir(); + let new_path = sys.home_dir(); if let Some(mut new_path) = new_path { new_path.extend(component_iter); #[cfg(feature = "tracing")] @@ -284,24 +277,26 @@ fn tilde_expansion(p: &PathBuf) -> Cow<'_, PathBuf> { Cow::Borrowed(p) } -#[cfg(target_os = "windows")] -fn correct_casing( +fn correct_casing( + sys: &TSys, 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 { - match e { - Ok(e) => { - if e.file_name().eq_ignore_ascii_case(file_name) { - p.pop(); - p.push(e.file_name()); - break; + 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)); } - } - Err(e) => { - nonfatal_error_handler.handle(NonFatalError::Io(e)); } } } @@ -309,8 +304,3 @@ fn correct_casing( } p } - -#[cfg(not(target_os = "windows"))] -fn correct_casing(p: PathBuf, _nonfatal_error_handler: &mut F) -> PathBuf { - p -} diff --git a/src/lib.rs b/src/lib.rs index 78fd19a..39d9ca8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,17 +19,16 @@ mod checker; mod error; mod finder; -#[cfg(windows)] mod helper; +pub mod sys; -#[cfg(feature = "regex")] -use std::borrow::Borrow; -use std::env; use std::fmt; use std::path; use std::ffi::{OsStr, OsString}; +use sys::Sys; + use crate::checker::CompositeChecker; pub use crate::error::*; use crate::finder::Finder; @@ -55,6 +54,7 @@ use crate::finder::Finder; /// assert_eq!(result, PathBuf::from("/usr/bin/rustc")); /// /// ``` +#[cfg(feature = "real-sys")] pub fn which>(binary_name: T) -> Result { which_all(binary_name).and_then(|mut i| i.next().ok_or(Error::CannotFindBinaryPath)) } @@ -79,32 +79,35 @@ pub fn which>(binary_name: T) -> Result { /// assert_eq!(result, PathBuf::from("/usr/bin/rustc")); /// /// ``` +#[cfg(feature = "real-sys")] pub fn which_global>(binary_name: T) -> Result { which_all_global(binary_name).and_then(|mut i| i.next().ok_or(Error::CannotFindBinaryPath)) } /// Find all binaries with `binary_name` using `cwd` to resolve relative paths. +#[cfg(feature = "real-sys")] pub fn which_all>(binary_name: T) -> Result> { - let cwd = env::current_dir().ok(); + let cwd = sys::RealSys.current_dir().ok(); - Finder::new().find( + Finder::new(sys::RealSys).find( binary_name, - env::var_os("PATH"), + sys::RealSys.env_var_os("PATH"), cwd, - CompositeChecker::new(), + CompositeChecker::new(sys::RealSys), Noop, ) } /// Find all binaries with `binary_name` ignoring `cwd`. +#[cfg(feature = "real-sys")] pub fn which_all_global>( binary_name: T, ) -> Result> { - Finder::new().find( + Finder::new(sys::RealSys).find( binary_name, - env::var_os("PATH"), + sys::RealSys.env_var_os("PATH"), Option::<&Path>::None, - CompositeChecker::new(), + CompositeChecker::new(sys::RealSys), Noop, ) } @@ -141,12 +144,15 @@ pub fn which_all_global>( /// which_re(Regex::new("^cargo-.*").unwrap()).unwrap() /// .for_each(|pth| println!("{}", pth.to_string_lossy())); /// ``` -#[cfg(feature = "regex")] -pub fn which_re(regex: impl Borrow) -> Result> { +#[cfg(all(feature = "regex", feature = "real-sys"))] +pub fn which_re( + regex: impl std::borrow::Borrow, +) -> Result> { which_re_in(regex, env::var_os("PATH")) } /// Find `binary_name` in the path list `paths`, using `cwd` to resolve relative paths. +#[cfg(feature = "real-sys")] pub fn which_in(binary_name: T, paths: Option, cwd: V) -> Result where T: AsRef, @@ -180,18 +186,19 @@ where /// let python_paths = vec![PathBuf::from("/usr/bin/python2"), PathBuf::from("/usr/bin/python3")]; /// assert_eq!(binaries, python_paths); /// ``` -#[cfg(feature = "regex")] +#[cfg(all(feature = "regex", feature = "real-sys"))] pub fn which_re_in( - regex: impl Borrow, + regex: impl std::borrow::Borrow, paths: Option, ) -> Result> where T: AsRef, { - Finder::new().find_re(regex, paths, CompositeChecker::new(), Noop) + Finder::new(sys::RealSys).find_re(regex, paths, CompositeChecker::new(sys::RealSys), Noop) } /// Find all binaries with `binary_name` in the path list `paths`, using `cwd` to resolve relative paths. +#[cfg(feature = "real-sys")] pub fn which_in_all<'a, T, U, V>( binary_name: T, paths: Option, @@ -202,10 +209,17 @@ where U: AsRef, V: AsRef + 'a, { - Finder::new().find(binary_name, paths, Some(cwd), CompositeChecker::new(), Noop) + Finder::new(sys::RealSys).find( + binary_name, + paths, + Some(cwd), + CompositeChecker::new(sys::RealSys), + Noop, + ) } /// Find all binaries with `binary_name` in the path list `paths`, ignoring `cwd`. +#[cfg(feature = "real-sys")] pub fn which_in_global( binary_name: T, paths: Option, @@ -214,23 +228,24 @@ where T: AsRef, U: AsRef, { - Finder::new().find( + Finder::new(sys::RealSys).find( binary_name, paths, Option::<&Path>::None, - CompositeChecker::new(), + CompositeChecker::new(sys::RealSys), 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, + sys: TSys, } /// A handler for non-fatal errors which does nothing with them. @@ -261,7 +276,8 @@ where } } -impl Default for WhichConfig { +#[cfg(feature = "real-sys")] +impl Default for WhichConfig { fn default() -> Self { Self { cwd: Some(either::Either::Left(true)), @@ -270,6 +286,7 @@ impl Default for WhichConfig { nonfatal_error_handler: F::default(), #[cfg(feature = "regex")] regex: None, + sys: sys::RealSys, } } } @@ -280,13 +297,28 @@ type Regex = regex::Regex; #[cfg(not(feature = "regex"))] type Regex = (); -impl WhichConfig { +#[cfg(feature = "real-sys")] +impl WhichConfig { pub fn new() -> Self { - Self::default() + Self::new_with_sys(sys::RealSys) } } -impl<'a, F: NonFatalErrorHandler + 'a> WhichConfig { +impl WhichConfig { + pub fn new_with_sys(sys: TSys) -> Self { + Self { + cwd: Some(either::Either::Left(true)), + custom_path_list: None, + binary_name: None, + nonfatal_error_handler: Noop, + #[cfg(feature = "regex")] + regex: None, + sys, + } + } +} + +impl<'a, TSys: Sys, F: NonFatalErrorHandler + 'a> WhichConfig { /// Whether or not to use the current working directory. `true` by default. /// /// # Panics @@ -402,7 +434,7 @@ impl<'a, F: NonFatalErrorHandler + 'a> WhichConfig { /// .unwrap() /// .collect::>(); /// ``` - pub fn nonfatal_error_handler(self, handler: NewF) -> WhichConfig { + pub fn nonfatal_error_handler(self, handler: NewF) -> WhichConfig { WhichConfig { custom_path_list: self.custom_path_list, cwd: self.cwd, @@ -410,6 +442,7 @@ impl<'a, F: NonFatalErrorHandler + 'a> WhichConfig { nonfatal_error_handler: handler, #[cfg(feature = "regex")] regex: self.regex, + sys: self.sys, } } @@ -421,15 +454,17 @@ impl<'a, F: NonFatalErrorHandler + 'a> WhichConfig { /// Finishes configuring, runs the query and returns all results. pub fn all_results(self) -> Result + 'a> { - let paths = self.custom_path_list.or_else(|| env::var_os("PATH")); + let paths = self + .custom_path_list + .or_else(|| self.sys.env_var_os("PATH")); #[cfg(feature = "regex")] if let Some(regex) = self.regex { - return Finder::new() + return Finder::new(self.sys.clone()) .find_re( regex, paths, - CompositeChecker::new(), + CompositeChecker::new(self.sys), self.nonfatal_error_handler, ) .map(|i| Box::new(i) as Box + 'a>); @@ -438,17 +473,17 @@ impl<'a, F: NonFatalErrorHandler + 'a> WhichConfig { let cwd = match self.cwd { Some(either::Either::Left(false)) => None, Some(either::Either::Right(custom)) => Some(custom), - None | Some(either::Either::Left(true)) => env::current_dir().ok(), + None | Some(either::Either::Left(true)) => self.sys.current_dir().ok(), }; - Finder::new() + Finder::new(self.sys.clone()) .find( self.binary_name.expect( "binary_name not set! You must set binary_name or regex before searching!", ), paths, cwd, - CompositeChecker::new(), + CompositeChecker::new(self.sys), self.nonfatal_error_handler, ) .map(|i| Box::new(i) as Box + 'a>) @@ -474,6 +509,7 @@ impl Path { /// Returns the path of an executable binary by name. /// /// This calls `which` and maps the result into a `Path`. + #[cfg(feature = "real-sys")] pub fn new>(binary_name: T) -> Result { which(binary_name).map(|inner| Path { inner }) } @@ -481,6 +517,7 @@ impl Path { /// Returns the paths of all executable binaries by a name. /// /// this calls `which_all` and maps the results into `Path`s. + #[cfg(feature = "real-sys")] pub fn all>(binary_name: T) -> Result> { which_all(binary_name).map(|inner| inner.map(|inner| Path { inner })) } @@ -489,6 +526,7 @@ impl Path { /// current working directory `cwd` to resolve relative paths. /// /// This calls `which_in` and maps the result into a `Path`. + #[cfg(feature = "real-sys")] pub fn new_in(binary_name: T, paths: Option, cwd: V) -> Result where T: AsRef, @@ -502,6 +540,7 @@ impl Path { /// current working directory `cwd` to resolve relative paths. /// /// This calls `which_in_all` and maps the results into a `Path`. + #[cfg(feature = "real-sys")] pub fn all_in<'a, T, U, V>( binary_name: T, paths: Option, @@ -586,22 +625,28 @@ impl CanonicalPath { /// Returns the canonical path of an executable binary by name. /// /// This calls `which` and `Path::canonicalize` and maps the result into a `CanonicalPath`. + #[cfg(feature = "real-sys")] pub fn new>(binary_name: T) -> Result { which(binary_name) - .and_then(|p| p.canonicalize().map_err(|_| Error::CannotCanonicalize)) + .and_then(|p| { + sys::RealSys + .canonicalize(&p) + .map_err(|_| Error::CannotCanonicalize) + }) .map(|inner| CanonicalPath { inner }) } /// Returns the canonical paths of an executable binary by name. /// /// This calls `which_all` and `Path::canonicalize` and maps the results into `CanonicalPath`s. + #[cfg(feature = "real-sys")] pub fn all>( binary_name: T, ) -> Result>> { which_all(binary_name).map(|inner| { inner.map(|inner| { - inner - .canonicalize() + sys::RealSys + .canonicalize(&inner) .map_err(|_| Error::CannotCanonicalize) .map(|inner| CanonicalPath { inner }) }) @@ -612,6 +657,7 @@ impl CanonicalPath { /// using the current working directory `cwd` to resolve relative paths. /// /// This calls `which_in` and `Path::canonicalize` and maps the result into a `CanonicalPath`. + #[cfg(feature = "real-sys")] pub fn new_in(binary_name: T, paths: Option, cwd: V) -> Result where T: AsRef, @@ -619,7 +665,11 @@ impl CanonicalPath { V: AsRef, { which_in(binary_name, paths, cwd) - .and_then(|p| p.canonicalize().map_err(|_| Error::CannotCanonicalize)) + .and_then(|p| { + sys::RealSys + .canonicalize(&p) + .map_err(|_| Error::CannotCanonicalize) + }) .map(|inner| CanonicalPath { inner }) } @@ -627,6 +677,7 @@ 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`. + #[cfg(feature = "real-sys")] pub fn all_in<'a, T, U, V>( binary_name: T, paths: Option, @@ -639,8 +690,8 @@ impl CanonicalPath { { which_in_all(binary_name, paths, cwd).map(|inner| { inner.map(|inner| { - inner - .canonicalize() + sys::RealSys + .canonicalize(&inner) .map_err(|_| Error::CannotCanonicalize) .map(|inner| CanonicalPath { inner }) }) diff --git a/src/sys.rs b/src/sys.rs new file mode 100644 index 0000000..7f084a8 --- /dev/null +++ b/src/sys.rs @@ -0,0 +1,155 @@ +use std::env::VarError; +use std::ffi::OsStr; +use std::ffi::OsString; +use std::io; +use std::path::Path; +use std::path::PathBuf; + +pub trait SysReadDirEntry { + fn file_name(&self) -> OsString; + fn path(&self) -> PathBuf; +} + +pub trait SysMetadata { + fn is_symlink(&self) -> bool; + fn is_file(&self) -> bool; +} + +pub trait Sys: Clone { + type ReadDirEntry: SysReadDirEntry; + type Metadata: SysMetadata; + + fn is_windows(&self) -> bool; + fn current_dir(&self) -> io::Result; + fn home_dir(&self) -> Option; + fn env_split_paths(&self, paths: &OsStr) -> Vec; + fn env_var_os(&self, name: &str) -> Option; + fn env_var(&self, key: &str) -> Result { + match self.env_var_os(key) { + Some(val) => val.into_string().map_err(VarError::NotUnicode), + None => Err(VarError::NotPresent), + } + } + fn metadata(&self, path: &Path) -> io::Result; + fn symlink_metadata(&self, path: &Path) -> io::Result; + fn read_dir( + &self, + path: &Path, + ) -> io::Result>>>; + fn is_valid_executable(&self, path: &Path) -> io::Result; +} + +#[cfg(feature = "real-sys")] +impl SysReadDirEntry for std::fs::DirEntry { + fn file_name(&self) -> OsString { + self.file_name() + } + + fn path(&self) -> PathBuf { + self.path() + } +} + +#[cfg(feature = "real-sys")] +impl SysMetadata for std::fs::Metadata { + fn is_symlink(&self) -> bool { + self.file_type().is_symlink() + } + + fn is_file(&self) -> bool { + self.file_type().is_file() + } +} + +#[cfg(feature = "real-sys")] +#[derive(Default, Clone)] +pub struct RealSys; + +#[cfg(feature = "real-sys")] +impl RealSys { + #[inline] + pub(crate) fn canonicalize(&self, path: &Path) -> io::Result { + #[allow(clippy::disallowed_methods)] // ok, sys implementation + std::fs::canonicalize(path) + } +} + +#[cfg(feature = "real-sys")] +impl Sys for RealSys { + type ReadDirEntry = std::fs::DirEntry; + type Metadata = std::fs::Metadata; + + #[inline] + fn is_windows(&self) -> bool { + cfg!(windows) + } + + #[inline] + fn current_dir(&self) -> io::Result { + #[allow(clippy::disallowed_methods)] // ok, sys implementation + std::env::current_dir() + } + + #[inline] + fn home_dir(&self) -> Option { + // Home dir shim, use env_home crate when possible. Otherwise, return None + #[cfg(any(windows, unix, target_os = "redox"))] + { + env_home::env_home_dir() + } + #[cfg(not(any(windows, unix, target_os = "redox")))] + { + None + } + } + + #[inline] + fn env_split_paths(&self, paths: &OsStr) -> Vec { + #[allow(clippy::disallowed_methods)] // ok, sys implementation + std::env::split_paths(paths).collect() + } + + #[inline] + fn env_var_os(&self, name: &str) -> Option { + #[allow(clippy::disallowed_methods)] // ok, sys implementation + std::env::var_os(name) + } + + #[inline] + fn read_dir( + &self, + path: &Path, + ) -> io::Result>>> { + #[allow(clippy::disallowed_methods)] // ok, sys implementation + let iter = std::fs::read_dir(path)?; + Ok(Box::new(iter)) + } + + #[inline] + fn metadata(&self, path: &Path) -> io::Result { + #[allow(clippy::disallowed_methods)] // ok, sys implementation + std::fs::metadata(path) + } + + #[inline] + fn symlink_metadata(&self, path: &Path) -> io::Result { + #[allow(clippy::disallowed_methods)] // ok, sys implementation + std::fs::symlink_metadata(path) + } + + #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] + fn is_valid_executable(&self, path: &Path) -> io::Result { + use std::io; + + use rustix::fs as rfs; + rfs::access(path, rfs::Access::EXEC_OK) + .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error())) + } + + #[cfg(windows)] + fn is_valid_executable(&self, path: &Path) -> io::Result { + winsafe::GetBinaryType(&path.display().to_string()) + .map(|_| true) + .map_err(|e| io::Error::from_raw_os_error(e.raw() as i32)) + } +} From aaae1543fcbf7709d237da8b9897c08a853c712f Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 1 Jan 2025 19:02:08 -0500 Subject: [PATCH 2/9] some cleanup --- src/sys.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/sys.rs b/src/sys.rs index 7f084a8..2b31e49 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -6,12 +6,16 @@ use std::path::Path; use std::path::PathBuf; pub trait SysReadDirEntry { + /// Gets the file name of the directory entry, not the full path. fn file_name(&self) -> OsString; + /// Gets the full path of the directory entry. fn path(&self) -> PathBuf; } pub trait SysMetadata { + /// Gets if the path is a symlink. fn is_symlink(&self) -> bool; + /// Gets if the path is a file. fn is_file(&self) -> bool; } @@ -19,10 +23,18 @@ pub trait Sys: Clone { type ReadDirEntry: SysReadDirEntry; type Metadata: SysMetadata; + /// Check if the current platform is Windows. + /// + /// This can be set to true in wasm32-unknown-unknown targets that + /// are running on Windows systems. fn is_windows(&self) -> bool; + /// Gets the current working directory. fn current_dir(&self) -> io::Result; + /// Gets the home directory of the current user. fn home_dir(&self) -> Option; + /// Splits a platform-specific PATH variable into a list of paths. fn env_split_paths(&self, paths: &OsStr) -> Vec; + /// Gets the value of an environment variable. fn env_var_os(&self, name: &str) -> Option; fn env_var(&self, key: &str) -> Result { match self.env_var_os(key) { @@ -30,16 +42,19 @@ pub trait Sys: Clone { None => Err(VarError::NotPresent), } } + /// Gets the metadata of the provided path, following symlinks. fn metadata(&self, path: &Path) -> io::Result; + /// Gets the metadata of the provided path, not following symlinks. fn symlink_metadata(&self, path: &Path) -> io::Result; + /// Reads the directory entries of the provided path. fn read_dir( &self, path: &Path, ) -> io::Result>>>; + /// Checks if the provided path is a valid executable. fn is_valid_executable(&self, path: &Path) -> io::Result; } -#[cfg(feature = "real-sys")] impl SysReadDirEntry for std::fs::DirEntry { fn file_name(&self) -> OsString { self.file_name() @@ -50,7 +65,6 @@ impl SysReadDirEntry for std::fs::DirEntry { } } -#[cfg(feature = "real-sys")] impl SysMetadata for std::fs::Metadata { fn is_symlink(&self) -> bool { self.file_type().is_symlink() From 534b6cca5f4b97f183af9bc77f37a342ec019aee Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 1 Jan 2025 19:14:20 -0500 Subject: [PATCH 3/9] Update docs and add some tests for compiling with wasm32-unknown-unknown --- .github/workflows/rust.yml | 9 ++++++++- README.md | 32 ++++++++++++++++++++++++++++---- src/lib.rs | 4 ++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 389f171..b4fa6ea 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -48,7 +48,7 @@ jobs: name: Compile strategy: matrix: - target: [x86_64-unknown-linux-musl, wasm32-wasi] + target: [x86_64-unknown-linux-musl, wasm32-wasi, wasm32-unknown-unknown] runs-on: ubuntu-latest steps: - name: Setup | Checkout @@ -61,12 +61,19 @@ jobs: target: ${{ matrix.target }} - name: Build | Check + if: ${{ matrix.target != 'wasm32-unknown-unknown' }} run: | cargo check --workspace --target ${{ matrix.target }} cargo check --workspace --target ${{ matrix.target }} --features regex cargo check --workspace --target ${{ matrix.target }} --no-default-features cargo check --workspace --target ${{ matrix.target }} --no-default-features --features regex + - name: Check wasm32-unknown-unknown + if: ${{ matrix.target == 'wasm32-unknown-unknown' }} + run: | + cargo check --workspace --target ${{ matrix.target }} --no-default-features + cargo check --workspace --target ${{ matrix.target }} --no-default-features --features regex + # Run tests on Linux, macOS, and Windows # On both Rust stable and Rust nightly test: diff --git a/README.md b/README.md index 0597e0a..0c76f3e 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,9 @@ A Rust equivalent of Unix command "which". Locate installed executable in cross ### A note on WebAssembly -This project aims to support WebAssembly with the [wasi](https://wasi.dev/) extension. This extension is a requirement. `which` is a library for exploring a filesystem, and -WebAssembly without wasi does not have a filesystem. `which` cannot do anything useful without this extension. Issues and PRs relating to -`wasm32-unknown-unknown` and `wasm64-unknown-unknown` will not be resolved or merged. All `wasm32-wasi*` targets are officially supported. +This project aims to support WebAssembly with the [wasi](https://wasi.dev/) extension. All `wasm32-wasi*` targets are officially supported. -If you need to add a conditional dependency on `which` for this reason please refer to [the relevant cargo documentation for platform specific dependencies.](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) +If you need to add a conditional dependency on `which` please refer to [the relevant cargo documentation for platform specific dependencies.](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) Here's an example of how to conditionally add `which`. You should tweak this to your needs. @@ -26,6 +24,32 @@ Here's an example of how to conditionally add `which`. You should tweak this to which = "7.0.0" ``` +### How to use in `wasm32-unknown-unknown` + +WebAssembly without wasi does not have a filesystem, but using this crate is possible in `wasm32-unknown-unknown` targets by disabling default features: + +```toml +which = { version = "...", default-features = false } +``` + +Then providing your own implementation of the `which::sys::Sys` trait: + +```rs +use which::WhichConfig; + +struct WasmSys; + +impl which::sys::Sys for WasmSys { + // it is up to you to implement this trait based on the + // environment you are running WebAssembly in +} + +let paths = WhichConfig::new_with_sys(WasmSys) + .all_results() + .unwrap() + .collect::>(); +``` + ## Examples 1) To find which rustc executable binary is using. diff --git a/src/lib.rs b/src/lib.rs index 39d9ca8..89cc66f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -305,6 +305,10 @@ impl WhichConfig { } impl WhichConfig { + /// Creates a new `WhichConfig` with the given system. + /// + /// This is useful for providing all the system related + /// functionality to this crate. pub fn new_with_sys(sys: TSys) -> Self { Self { cwd: Some(either::Either::Left(true)), From ef6f192d53a258ebb5a5aa20200265c3089d1bd9 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 1 Jan 2025 19:16:37 -0500 Subject: [PATCH 4/9] Flatten sys mod --- README.md | 4 ++-- src/lib.rs | 5 ++--- src/sys.rs | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0c76f3e..bbcbf0a 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,14 @@ WebAssembly without wasi does not have a filesystem, but using this crate is pos which = { version = "...", default-features = false } ``` -Then providing your own implementation of the `which::sys::Sys` trait: +Then providing your own implementation of the `which::Sys` trait: ```rs use which::WhichConfig; struct WasmSys; -impl which::sys::Sys for WasmSys { +impl which::Sys for WasmSys { // it is up to you to implement this trait based on the // environment you are running WebAssembly in } diff --git a/src/lib.rs b/src/lib.rs index 89cc66f..3b7078b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,18 +20,17 @@ mod checker; mod error; mod finder; mod helper; -pub mod sys; +mod sys; use std::fmt; use std::path; use std::ffi::{OsStr, OsString}; -use sys::Sys; - use crate::checker::CompositeChecker; pub use crate::error::*; use crate::finder::Finder; +pub use sys::*; /// Find an executable binary's path by name. /// diff --git a/src/sys.rs b/src/sys.rs index 2b31e49..5351a9a 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -157,6 +157,7 @@ impl Sys for RealSys { use rustix::fs as rfs; rfs::access(path, rfs::Access::EXEC_OK) + .map(|_| true) .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error())) } From a6168bca3d85d010a179c785f273df819bd5c93c Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 1 Jan 2025 19:19:40 -0500 Subject: [PATCH 5/9] fixes --- src/finder.rs | 5 +---- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/finder.rs b/src/finder.rs index 4814fc2..191b92a 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -126,10 +126,7 @@ impl Finder { T: AsRef, { let p = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?; - // Collect needs to happen in order to not have to - // change the API to borrow on `paths`. - #[allow(clippy::needless_collect)] - let paths: Vec<_> = env::split_paths(&p).collect(); + let paths = self.sys.env_split_paths(p.as_ref()); let sys = self.sys.clone(); let matching_re = paths diff --git a/src/lib.rs b/src/lib.rs index 3b7078b..4e92226 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,7 +147,7 @@ pub fn which_all_global>( pub fn which_re( regex: impl std::borrow::Borrow, ) -> Result> { - which_re_in(regex, env::var_os("PATH")) + which_re_in(regex, sys::RealSys.env_var_os("PATH")) } /// Find `binary_name` in the path list `paths`, using `cwd` to resolve relative paths. From 8195e32bb66443486abc66ab8710038e951665c7 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 1 Jan 2025 19:21:54 -0500 Subject: [PATCH 6/9] clippy --- tests/basic.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/basic.rs b/tests/basic.rs index 120f878..81772c0 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_methods)] + extern crate which; #[cfg(all(unix, feature = "regex"))] From 27116e0dfb68f319d0fc40eb78dd7234b08aadf9 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 1 Jan 2025 19:22:17 -0500 Subject: [PATCH 7/9] fix error --- src/sys.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sys.rs b/src/sys.rs index 5351a9a..10d683e 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -153,8 +153,6 @@ impl Sys for RealSys { #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] fn is_valid_executable(&self, path: &Path) -> io::Result { - use std::io; - use rustix::fs as rfs; rfs::access(path, rfs::Access::EXEC_OK) .map(|_| true) From e0e83322c76e955809ec0065a447f1f1c702aa79 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Thu, 2 Jan 2025 11:14:53 -0500 Subject: [PATCH 8/9] add back sys module, move docs, reduce sys cloning, provide way to disable PATHEXT caching --- README.md | 28 +-------- src/finder.rs | 158 ++++++++++++++++++++++++++------------------------ src/helper.rs | 10 +++- src/lib.rs | 6 +- src/sys.rs | 61 +++++++++++++++++++ 5 files changed, 156 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index bbcbf0a..a72ea39 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A Rust equivalent of Unix command "which". Locate installed executable in cross ### A note on WebAssembly -This project aims to support WebAssembly with the [wasi](https://wasi.dev/) extension. All `wasm32-wasi*` targets are officially supported. +This project aims to support WebAssembly with the [WASI](https://wasi.dev/) extension. All `wasm32-wasi*` targets are officially supported. If you need to add a conditional dependency on `which` please refer to [the relevant cargo documentation for platform specific dependencies.](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) @@ -24,31 +24,7 @@ Here's an example of how to conditionally add `which`. You should tweak this to which = "7.0.0" ``` -### How to use in `wasm32-unknown-unknown` - -WebAssembly without wasi does not have a filesystem, but using this crate is possible in `wasm32-unknown-unknown` targets by disabling default features: - -```toml -which = { version = "...", default-features = false } -``` - -Then providing your own implementation of the `which::Sys` trait: - -```rs -use which::WhichConfig; - -struct WasmSys; - -impl which::Sys for WasmSys { - // it is up to you to implement this trait based on the - // environment you are running WebAssembly in -} - -let paths = WhichConfig::new_with_sys(WasmSys) - .all_results() - .unwrap() - .collect::>(); -``` +Note that you can disable the default features of this crate and provide a custom `which::sys::Sys` implementation to `which::WhichConfig` for use in Wasm environments without WASI. ## Examples diff --git a/src/finder.rs b/src/finder.rs index 191b92a..ae3a7ae 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -78,7 +78,6 @@ impl Finder { cwd.as_ref().map(|p| p.as_ref().display()) ); - let sys = self.sys.clone(); let binary_path_candidates = match cwd { Some(cwd) if path.has_separator() => { #[cfg(feature = "tracing")] @@ -87,22 +86,27 @@ impl Finder { path.display() ); // Search binary in cwd if the path have a path separator. - Either::Left(Self::cwd_search_candidates(sys.clone(), path, cwd)) + Either::Left(Self::cwd_search_candidates(&self.sys, 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 = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?; - let paths = sys.env_split_paths(paths.as_ref()); + let paths = self.sys.env_split_paths(paths.as_ref()); if paths.is_empty() { return Err(Error::CannotGetCurrentDirAndPathListEmpty); } - Either::Right(Self::path_search_candidates(sys.clone(), path, paths)) + Either::Right(Self::path_search_candidates( + &self.sys, + path, + paths.into_iter(), + )) } }; - let ret = binary_path_candidates.into_iter().filter_map(move |p| { + let sys = self.sys.clone(); + let ret = binary_path_candidates.filter_map(move |p| { binary_checker .is_valid(&p, &mut nonfatal_error_handler) .then(|| correct_casing(&sys, p, &mut nonfatal_error_handler)) @@ -148,10 +152,10 @@ impl Finder { } fn cwd_search_candidates( - sys: TSys, + sys: &TSys, binary_name: PathBuf, cwd: C, - ) -> impl IntoIterator + ) -> impl Iterator where C: AsRef, { @@ -161,14 +165,14 @@ impl Finder { } fn path_search_candidates

( - sys: TSys, + sys: &TSys, binary_name: PathBuf, paths: P, - ) -> impl IntoIterator + ) -> impl Iterator where - P: IntoIterator, + P: Iterator, { - let new_paths = paths.into_iter().map({ + let new_paths = paths.map({ let sys = sys.clone(); move |p| tilde_expansion(&sys, &p).join(binary_name.clone()) }); @@ -176,79 +180,79 @@ impl Finder { Self::append_extension(sys, new_paths) } - fn append_extension

(sys: TSys, paths: P) -> impl IntoIterator + fn append_extension

(sys: &TSys, paths: P) -> impl Iterator where - P: IntoIterator, + P: Iterator, { - use std::sync::OnceLock; + struct PathsIter

+ where + P: Iterator, + { + paths: P, + current_path_with_index: Option<(PathBuf, usize)>, + path_extensions: Cow<'static, [String]>, + } - // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC - // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …]. - // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it; - // hence its retention.) - static PATH_EXTENSIONS: OnceLock> = OnceLock::new(); + impl

Iterator for PathsIter

+ where + P: Iterator, + { + type Item = PathBuf; - paths - .into_iter() - .flat_map(move |p| -> Box> { - if !sys.is_windows() { - return Box::new(iter::once(p)); - } - - let sys = sys.clone(); - let path_extensions = PATH_EXTENSIONS.get_or_init(move || { - sys.env_var("PATHEXT") - .map(|pathext| { - pathext - .split(';') - .filter_map(|s| { - if s.as_bytes().first() == Some(&b'.') { - Some(s.to_owned()) - } else { - // Invalid segment; just ignore it. - None - } - }) - .collect() - }) - // PATHEXT not being set or not being a proper Unicode string is exceedingly - // improbable and would probably break Windows badly. Still, don't crash: - .unwrap_or_default() - }); - // Check if path already have executable extension - if has_executable_extension(&p, path_extensions) { + fn next(&mut self) -> Option { + 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)); + } + // Append the extension. + let mut p = p.into_os_string(); + p.push(&self.path_extensions[index]); + let ret = PathBuf::from(p); #[cfg(feature = "tracing")] - tracing::trace!( - "{} already has an executable extension, not modifying it further", - p.display() - ); - Box::new(iter::once(p)) + tracing::trace!("possible extension: {}", ret.display()); + Some(ret) } else { - #[cfg(feature = "tracing")] - tracing::trace!( - "{} has no extension, using PATHEXT environment variable to infer one", - p.display() - ); - // Appended paths with windows executable extensions. - // e.g. path `c:/windows/bin[.ext]` will expand to: - // [c:/windows/bin.ext] - // c:/windows/bin[.ext].COM - // c:/windows/bin[.ext].EXE - // c:/windows/bin[.ext].CMD - // ... - Box::new( - iter::once(p.clone()).chain(path_extensions.iter().map(move |e| { - // Append the extension. - let mut p = p.clone().into_os_string(); - p.push(e); - let ret = PathBuf::from(p); - #[cfg(feature = "tracing")] - tracing::trace!("possible extension: {}", ret.display()); - ret - })), - ) + 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() + ); + // Appended paths with windows executable extensions. + // e.g. path `c:/windows/bin[.ext]` will expand to: + // [c:/windows/bin.ext] + // c:/windows/bin[.ext].COM + // c:/windows/bin[.ext].EXE + // c:/windows/bin[.ext].CMD + // ... + self.current_path_with_index = Some((p.clone(), 0)); + } + Some(p) } - }) + } + } + + let path_extensions = if sys.is_windows() { + sys.env_windows_path_ext() + } else { + Cow::Borrowed(Default::default()) + }; + + PathsIter { + paths, + current_path_with_index: None, + path_extensions, + } } } diff --git a/src/helper.rs b/src/helper.rs index eb96891..ad2a9db 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -6,7 +6,7 @@ pub fn has_executable_extension, S: AsRef>(path: T, pathext: match ext { Some(ext) => pathext .iter() - .any(|e| ext.eq_ignore_ascii_case(&e.as_ref()[1..])), + .any(|e| !e.as_ref().is_empty() && ext.eq_ignore_ascii_case(&e.as_ref()[1..])), _ => false, } } @@ -37,4 +37,12 @@ mod test { &[".COM", ".EXE", ".CMD"] )); } + + #[test] + fn test_invalid_exts() { + assert!(!has_executable_extension( + PathBuf::from("foo.bar"), + &["", "."] + )); + } } diff --git a/src/lib.rs b/src/lib.rs index 4e92226..355ca12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ mod checker; mod error; mod finder; mod helper; -mod sys; +pub mod sys; use std::fmt; use std::path; @@ -30,7 +30,7 @@ use std::ffi::{OsStr, OsString}; use crate::checker::CompositeChecker; pub use crate::error::*; use crate::finder::Finder; -pub use sys::*; +use crate::sys::Sys; /// Find an executable binary's path by name. /// @@ -304,7 +304,7 @@ impl WhichConfig { } impl WhichConfig { - /// Creates a new `WhichConfig` with the given system. + /// Creates a new `WhichConfig` with the given `sys::Sys`. /// /// This is useful for providing all the system related /// functionality to this crate. diff --git a/src/sys.rs b/src/sys.rs index 10d683e..6f7350d 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::env::VarError; use std::ffi::OsStr; use std::ffi::OsString; @@ -19,6 +20,34 @@ pub trait SysMetadata { fn is_file(&self) -> bool; } +/// Represents the system that `which` interacts with to get information +/// about the environment and file system. +/// +/// ### How to use in Wasm without WASI +/// +/// WebAssembly without WASI does not have a filesystem, but using this crate is possible in `wasm32-unknown-unknown` targets by disabling default features: +/// +/// ```toml +/// which = { version = "...", default-features = false } +/// ``` +/// +// Then providing your own implementation of the `which::sys::Sys` trait: +/// +/// ```rs +/// use which::WhichConfig; +/// +/// struct WasmSys; +/// +/// impl which::sys::Sys for WasmSys { +/// // it is up to you to implement this trait based on the +/// // environment you are running WebAssembly in +/// } +/// +/// let paths = WhichConfig::new_with_sys(WasmSys) +/// .all_results() +/// .unwrap() +/// .collect::>(); +/// ``` pub trait Sys: Clone { type ReadDirEntry: SysReadDirEntry; type Metadata: SysMetadata; @@ -42,6 +71,38 @@ pub trait Sys: Clone { None => Err(VarError::NotPresent), } } + /// Gets and parses the PATHEXT environment variable on Windows. + /// + /// Override this to disable globally caching the parsed PATHEXT. + fn env_windows_path_ext(&self) -> Cow<'static, [String]> { + use std::sync::OnceLock; + + // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC + // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …]. + // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it; + // hence its retention.) + static PATH_EXTENSIONS: OnceLock> = OnceLock::new(); + let path_extensions = PATH_EXTENSIONS.get_or_init(|| { + self.env_var("PATHEXT") + .map(|pathext| { + pathext + .split(';') + .filter_map(|s| { + if s.as_bytes().first() == Some(&b'.') { + Some(s.to_owned()) + } else { + // Invalid segment; just ignore it. + None + } + }) + .collect() + }) + // PATHEXT not being set or not being a proper Unicode string is exceedingly + // improbable and would probably break Windows badly. Still, don't crash: + .unwrap_or_default() + }); + Cow::Borrowed(path_extensions) + } /// Gets the metadata of the provided path, following symlinks. fn metadata(&self, path: &Path) -> io::Result; /// Gets the metadata of the provided path, not following symlinks. From 226aaef40d69f7a7dfa43074f32fcd580c65d740 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Thu, 2 Jan 2025 11:26:27 -0500 Subject: [PATCH 9/9] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a72ea39..6e332cd 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Here's an example of how to conditionally add `which`. You should tweak this to which = "7.0.0" ``` -Note that you can disable the default features of this crate and provide a custom `which::sys::Sys` implementation to `which::WhichConfig` for use in Wasm environments without WASI. +Note that non-WASI environments have no access to the system. Using this in that situation requires disabling the default features of this crate and providing a custom `which::sys::Sys` implementation to `which::WhichConfig`. ## Examples