Skip to content

Commit

Permalink
add back sys module, move docs, reduce sys cloning, provide way to di…
Browse files Browse the repository at this point in the history
…sable PATHEXT caching
  • Loading branch information
dsherret committed Jan 2, 2025
1 parent 27116e0 commit e0e8332
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 107 deletions.
28 changes: 2 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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::<Vec<_>>();
```
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

Expand Down
158 changes: 81 additions & 77 deletions src/finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ impl<TSys: Sys + 'static> Finder<TSys> {
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")]
Expand All @@ -87,22 +86,27 @@ impl<TSys: Sys + 'static> Finder<TSys> {
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))
Expand Down Expand Up @@ -148,10 +152,10 @@ impl<TSys: Sys + 'static> Finder<TSys> {
}

fn cwd_search_candidates<C>(
sys: TSys,
sys: &TSys,
binary_name: PathBuf,
cwd: C,
) -> impl IntoIterator<Item = PathBuf>
) -> impl Iterator<Item = PathBuf>
where
C: AsRef<Path>,
{
Expand All @@ -161,94 +165,94 @@ impl<TSys: Sys + 'static> Finder<TSys> {
}

fn path_search_candidates<P>(
sys: TSys,
sys: &TSys,
binary_name: PathBuf,
paths: P,
) -> impl IntoIterator<Item = PathBuf>
) -> impl Iterator<Item = PathBuf>
where
P: IntoIterator<Item = PathBuf>,
P: Iterator<Item = PathBuf>,
{
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())
});

Self::append_extension(sys, new_paths)
}

fn append_extension<P>(sys: TSys, paths: P) -> impl IntoIterator<Item = PathBuf>
fn append_extension<P>(sys: &TSys, paths: P) -> impl Iterator<Item = PathBuf>
where
P: IntoIterator<Item = PathBuf>,
P: Iterator<Item = PathBuf>,
{
use std::sync::OnceLock;
struct PathsIter<P>
where
P: Iterator<Item = PathBuf>,
{
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<Vec<String>> = OnceLock::new();
impl<P> Iterator for PathsIter<P>
where
P: Iterator<Item = PathBuf>,
{
type Item = PathBuf;

paths
.into_iter()
.flat_map(move |p| -> Box<dyn Iterator<Item = _>> {
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<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));
}
// 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,
}
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub fn has_executable_extension<T: AsRef<Path>, S: AsRef<str>>(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,
}
}
Expand Down Expand Up @@ -37,4 +37,12 @@ mod test {
&[".COM", ".EXE", ".CMD"]
));
}

#[test]
fn test_invalid_exts() {
assert!(!has_executable_extension(
PathBuf::from("foo.bar"),
&["", "."]
));
}
}
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mod checker;
mod error;
mod finder;
mod helper;
mod sys;
pub mod sys;

use std::fmt;
use std::path;
Expand All @@ -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.
///
Expand Down Expand Up @@ -304,7 +304,7 @@ impl WhichConfig<sys::RealSys, Noop> {
}

impl<TSys: Sys> WhichConfig<TSys, Noop> {
/// 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.
Expand Down
61 changes: 61 additions & 0 deletions src/sys.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::env::VarError;
use std::ffi::OsStr;
use std::ffi::OsString;
Expand All @@ -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::<Vec<_>>();
/// ```
pub trait Sys: Clone {
type ReadDirEntry: SysReadDirEntry;
type Metadata: SysMetadata;
Expand All @@ -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<Vec<String>> = 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<Self::Metadata>;
/// Gets the metadata of the provided path, not following symlinks.
Expand Down

0 comments on commit e0e8332

Please sign in to comment.