Skip to content

Commit a0a6daf

Browse files
authored
feat: add Sys trait for swapping out system (#109)
* feat: add Sys trait for swapping out system * some cleanup * Update docs and add some tests for compiling with wasm32-unknown-unknown * Flatten sys mod * fixes * clippy * fix error * add back sys module, move docs, reduce sys cloning, provide way to disable PATHEXT caching * update readme * Make have lifetime as self * revert last commit * clippy * add some more docs * format * keep env_var_os param as OsStr * self review: actually, it is better to force someone to implement this * Caching should be on the real sys impl and not the trait. * add in-memory tests * make work on Rust 1.70 * update * update to io::Error and HashSet * format
1 parent eef1998 commit a0a6daf

File tree

10 files changed

+1411
-694
lines changed

10 files changed

+1411
-694
lines changed

.github/workflows/rust.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
name: Compile
4949
strategy:
5050
matrix:
51-
target: [x86_64-unknown-linux-musl, wasm32-wasip1]
51+
target: [x86_64-unknown-linux-musl, wasm32-wasip1, wasm32-unknown-unknown]
5252
runs-on: ubuntu-latest
5353
steps:
5454
- name: Setup | Checkout
@@ -61,7 +61,15 @@ jobs:
6161
target: ${{ matrix.target }}
6262

6363
- name: Build | Check
64-
run: cargo check --workspace --target ${{ matrix.target }}
64+
if: ${{ matrix.target != 'wasm32-unknown-unknown' }}
65+
run: |
66+
cargo check --workspace --target ${{ matrix.target }}
67+
cargo check --workspace --target ${{ matrix.target }} --features regex
68+
69+
- name: Build | Check (no default features)
70+
run: |
71+
cargo check --workspace --target ${{ matrix.target }} --no-default-features
72+
cargo check --workspace --target ${{ matrix.target }} --no-default-features --features regex
6573
6674
# Run tests on Linux, macOS, and Windows
6775
# On both Rust stable and Rust nightly
@@ -86,6 +94,9 @@ jobs:
8694
# Run the ignored tests that expect the above setup
8795
- name: Build | Test
8896
run: cargo test --workspace --all-features -- --include-ignored
97+
98+
- name: Build | Test (no default features)
99+
run: cargo test --workspace --no-default-features
89100
cargo-deny:
90101
runs-on: ubuntu-22.04
91102
steps:

Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,24 @@ categories = ["os", "filesystem"]
1313
keywords = ["which", "which-rs", "unix", "command"]
1414

1515
[features]
16+
default = ["real-sys"]
1617
regex = ["dep:regex"]
1718
tracing = ["dep:tracing"]
19+
real-sys = ["env_home", "rustix", "winsafe"]
1820

1921
[dependencies]
2022
either = "1.9.0"
2123
regex = { version = "1.10.2", optional = true }
2224
tracing = { version = "0.1.40", default-features = false, optional = true }
2325

2426
[target.'cfg(any(windows, unix, target_os = "redox"))'.dependencies]
25-
env_home = "0.1.0"
27+
env_home = { version = "0.1.0", optional = true }
2628

2729
[target.'cfg(any(unix, target_os = "wasi", target_os = "redox"))'.dependencies]
28-
rustix = { version = "1.0.5", default-features = false, features = ["fs", "std"] }
30+
rustix = { version = "1.0.5", default-features = false, features = ["fs", "std"], optional = true }
2931

3032
[target.'cfg(windows)'.dependencies]
31-
winsafe = { version = "0.0.19", features = ["kernel"] }
33+
winsafe = { version = "0.0.19", features = ["kernel"], optional = true }
3234

3335
[dev-dependencies]
3436
tempfile = "3.9.0"

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@ A Rust equivalent of Unix command "which". Locate installed executable in cross
1414

1515
### A note on WebAssembly
1616

17-
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
18-
WebAssembly without wasi does not have a filesystem. `which` cannot do anything useful without this extension. Issues and PRs relating to
19-
`wasm32-unknown-unknown` and `wasm64-unknown-unknown` will not be resolved or merged. All `wasm32-wasi*` targets are officially supported.
17+
This project aims to support WebAssembly with the [WASI](https://wasi.dev/) extension. All `wasm32-wasi*` targets are officially supported.
2018

21-
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)
19+
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)
2220

2321
Here's an example of how to conditionally add `which`. You should tweak this to your needs.
2422

@@ -27,6 +25,8 @@ Here's an example of how to conditionally add `which`. You should tweak this to
2725
which = "7.0.0"
2826
```
2927

28+
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`.
29+
3030
## Examples
3131

3232
1) To find which rustc executable binary is using.

clippy.toml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
disallowed-methods = [
2+
{ path = "std::env::current_dir", reason = "System operations should be done using Sys trait" },
3+
{ path = "std::path::Path::canonicalize", reason = "System operations should be done using Sys trait" },
4+
{ path = "std::path::Path::is_dir", reason = "System operations should be done using Sys trait" },
5+
{ path = "std::path::Path::is_file", reason = "System operations should be done using Sys trait" },
6+
{ path = "std::path::Path::is_symlink", reason = "System operations should be done using Sys trait" },
7+
{ path = "std::path::Path::metadata", reason = "System operations should be done using Sys trait" },
8+
{ path = "std::path::Path::read_dir", reason = "System operations should be done using Sys trait" },
9+
{ path = "std::path::Path::read_link", reason = "System operations should be done using Sys trait" },
10+
{ path = "std::path::Path::symlink_metadata", reason = "System operations should be done using Sys trait" },
11+
{ path = "std::path::Path::try_exists", reason = "System operations should be done using Sys trait" },
12+
{ path = "std::path::PathBuf::exists", reason = "System operations should be done using Sys trait" },
13+
{ path = "std::path::PathBuf::canonicalize", reason = "System operations should be done using Sys trait" },
14+
{ path = "std::path::PathBuf::is_dir", reason = "System operations should be done using Sys trait" },
15+
{ path = "std::path::PathBuf::is_file", reason = "System operations should be done using Sys trait" },
16+
{ path = "std::path::PathBuf::is_symlink", reason = "System operations should be done using Sys trait" },
17+
{ path = "std::path::PathBuf::metadata", reason = "System operations should be done using Sys trait" },
18+
{ path = "std::path::PathBuf::read_dir", reason = "System operations should be done using Sys trait" },
19+
{ path = "std::path::PathBuf::read_link", reason = "System operations should be done using Sys trait" },
20+
{ path = "std::path::PathBuf::symlink_metadata", reason = "System operations should be done using Sys trait" },
21+
{ path = "std::path::PathBuf::try_exists", reason = "System operations should be done using Sys trait" },
22+
{ path = "std::env::set_current_dir", reason = "System operations should be done using Sys trait" },
23+
{ path = "std::env::split_paths", reason = "System operations should be done using Sys trait" },
24+
{ path = "std::env::temp_dir", reason = "System operations should be done using Sys trait" },
25+
{ path = "std::env::var", reason = "System operations should be done using Sys trait" },
26+
{ path = "std::env::var_os", reason = "System operations should be done using Sys trait" },
27+
{ path = "std::fs::canonicalize", reason = "System operations should be done using Sys trait" },
28+
{ path = "std::fs::copy", reason = "System operations should be done using Sys trait" },
29+
{ path = "std::fs::create_dir_all", reason = "System operations should be done using Sys trait" },
30+
{ path = "std::fs::create_dir", reason = "System operations should be done using Sys trait" },
31+
{ path = "std::fs::DirBuilder::new", reason = "System operations should be done using Sys trait" },
32+
{ path = "std::fs::hard_link", reason = "System operations should be done using Sys trait" },
33+
{ path = "std::fs::metadata", reason = "System operations should be done using Sys trait" },
34+
{ path = "std::fs::OpenOptions::new", reason = "System operations should be done using Sys trait" },
35+
{ path = "std::fs::read_dir", reason = "System operations should be done using Sys trait" },
36+
{ path = "std::fs::read_link", reason = "System operations should be done using Sys trait" },
37+
{ path = "std::fs::read_to_string", reason = "System operations should be done using Sys trait" },
38+
{ path = "std::fs::read", reason = "System operations should be done using Sys trait" },
39+
{ path = "std::fs::remove_dir_all", reason = "System operations should be done using Sys trait" },
40+
{ path = "std::fs::remove_dir", reason = "System operations should be done using Sys trait" },
41+
{ path = "std::fs::remove_file", reason = "System operations should be done using Sys trait" },
42+
{ path = "std::fs::rename", reason = "System operations should be done using Sys trait" },
43+
{ path = "std::fs::set_permissions", reason = "System operations should be done using Sys trait" },
44+
{ path = "std::fs::symlink_metadata", reason = "System operations should be done using Sys trait" },
45+
{ path = "std::fs::write", reason = "System operations should be done using Sys trait" },
46+
{ path = "std::path::Path::canonicalize", reason = "System operations should be done using Sys trait" },
47+
{ path = "std::path::Path::exists", reason = "System operations should be done using Sys trait" },
48+
]

src/checker.rs

Lines changed: 71 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,111 @@
11
use crate::finder::Checker;
2+
use crate::sys::Sys;
3+
use crate::sys::SysMetadata;
24
use crate::{NonFatalError, NonFatalErrorHandler};
3-
use std::fs;
45
use std::path::Path;
56

6-
pub struct ExecutableChecker;
7+
pub struct ExecutableChecker<TSys: Sys> {
8+
sys: TSys,
9+
}
710

8-
impl ExecutableChecker {
9-
pub fn new() -> ExecutableChecker {
10-
ExecutableChecker
11+
impl<TSys: Sys> ExecutableChecker<TSys> {
12+
pub fn new(sys: TSys) -> Self {
13+
Self { sys }
1114
}
1215
}
1316

14-
impl Checker for ExecutableChecker {
15-
#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
17+
impl<TSys: Sys> Checker for ExecutableChecker<TSys> {
1618
fn is_valid<F: NonFatalErrorHandler>(
1719
&self,
1820
path: &Path,
1921
nonfatal_error_handler: &mut F,
2022
) -> bool {
21-
use std::io;
22-
23-
use rustix::fs as rfs;
24-
let ret = rfs::access(path, rfs::Access::EXEC_OK)
25-
.map_err(|e| {
26-
nonfatal_error_handler.handle(NonFatalError::Io(io::Error::from_raw_os_error(
27-
e.raw_os_error(),
28-
)))
29-
})
30-
.is_ok();
31-
#[cfg(feature = "tracing")]
32-
tracing::trace!("{} EXEC_OK = {ret}", path.display());
33-
ret
34-
}
35-
36-
#[cfg(windows)]
37-
fn is_valid<F: NonFatalErrorHandler>(
38-
&self,
39-
_path: &Path,
40-
_nonfatal_error_handler: &mut F,
41-
) -> bool {
42-
true
23+
if self.sys.is_windows() && path.extension().is_some() {
24+
true
25+
} else {
26+
let ret = self
27+
.sys
28+
.is_valid_executable(path)
29+
.map_err(|e| nonfatal_error_handler.handle(NonFatalError::Io(e)))
30+
.unwrap_or(false);
31+
#[cfg(feature = "tracing")]
32+
tracing::trace!("{} EXEC_OK = {ret}", path.display());
33+
ret
34+
}
4335
}
4436
}
4537

46-
pub struct ExistedChecker;
47-
48-
impl ExistedChecker {
49-
pub fn new() -> ExistedChecker {
50-
ExistedChecker
51-
}
38+
pub struct ExistedChecker<TSys: Sys> {
39+
sys: TSys,
5240
}
5341

54-
impl Checker for ExistedChecker {
55-
#[cfg(target_os = "windows")]
56-
fn is_valid<F: NonFatalErrorHandler>(
57-
&self,
58-
path: &Path,
59-
nonfatal_error_handler: &mut F,
60-
) -> bool {
61-
let ret = fs::symlink_metadata(path)
62-
.map(|metadata| {
63-
let file_type = metadata.file_type();
64-
#[cfg(feature = "tracing")]
65-
tracing::trace!(
66-
"{} is_file() = {}, is_symlink() = {}",
67-
path.display(),
68-
file_type.is_file(),
69-
file_type.is_symlink()
70-
);
71-
file_type.is_file() || file_type.is_symlink()
72-
})
73-
.map_err(|e| {
74-
nonfatal_error_handler.handle(NonFatalError::Io(e));
75-
})
76-
.unwrap_or(false)
77-
&& (path.extension().is_some() || matches_arch(path, nonfatal_error_handler));
78-
#[cfg(feature = "tracing")]
79-
tracing::trace!(
80-
"{} has_extension = {}, ExistedChecker::is_valid() = {ret}",
81-
path.display(),
82-
path.extension().is_some()
83-
);
84-
ret
42+
impl<TSys: Sys> ExistedChecker<TSys> {
43+
pub fn new(sys: TSys) -> Self {
44+
Self { sys }
8545
}
46+
}
8647

87-
#[cfg(not(target_os = "windows"))]
48+
impl<TSys: Sys> Checker for ExistedChecker<TSys> {
8849
fn is_valid<F: NonFatalErrorHandler>(
8950
&self,
9051
path: &Path,
9152
nonfatal_error_handler: &mut F,
9253
) -> bool {
93-
let ret = fs::metadata(path).map(|metadata| metadata.is_file());
94-
#[cfg(feature = "tracing")]
95-
tracing::trace!("{} is_file() = {ret:?}", path.display());
96-
match ret {
97-
Ok(ret) => ret,
98-
Err(e) => {
99-
nonfatal_error_handler.handle(NonFatalError::Io(e));
100-
false
54+
if self.sys.is_windows() {
55+
let ret = self
56+
.sys
57+
.symlink_metadata(path)
58+
.map(|metadata| {
59+
#[cfg(feature = "tracing")]
60+
tracing::trace!(
61+
"{} is_file() = {}, is_symlink() = {}",
62+
path.display(),
63+
metadata.is_file(),
64+
metadata.is_symlink()
65+
);
66+
metadata.is_file() || metadata.is_symlink()
67+
})
68+
.map_err(|e| {
69+
nonfatal_error_handler.handle(NonFatalError::Io(e));
70+
})
71+
.unwrap_or(false);
72+
#[cfg(feature = "tracing")]
73+
tracing::trace!(
74+
"{} has_extension = {}, ExistedChecker::is_valid() = {ret}",
75+
path.display(),
76+
path.extension().is_some()
77+
);
78+
ret
79+
} else {
80+
let ret = self.sys.metadata(path).map(|metadata| metadata.is_file());
81+
#[cfg(feature = "tracing")]
82+
tracing::trace!("{} is_file() = {ret:?}", path.display());
83+
match ret {
84+
Ok(ret) => ret,
85+
Err(e) => {
86+
nonfatal_error_handler.handle(NonFatalError::Io(e));
87+
false
88+
}
10189
}
10290
}
10391
}
10492
}
10593

106-
#[cfg(target_os = "windows")]
107-
fn matches_arch<F: NonFatalErrorHandler>(path: &Path, nonfatal_error_handler: &mut F) -> bool {
108-
use std::io;
109-
110-
let ret = winsafe::GetBinaryType(&path.display().to_string())
111-
.map_err(|e| {
112-
nonfatal_error_handler.handle(NonFatalError::Io(io::Error::from_raw_os_error(
113-
e.raw() as i32
114-
)))
115-
})
116-
.is_ok();
117-
#[cfg(feature = "tracing")]
118-
tracing::trace!("{} matches_arch() = {ret}", path.display());
119-
ret
120-
}
121-
122-
pub struct CompositeChecker {
123-
existed_checker: ExistedChecker,
124-
executable_checker: ExecutableChecker,
94+
pub struct CompositeChecker<TSys: Sys> {
95+
existed_checker: ExistedChecker<TSys>,
96+
executable_checker: ExecutableChecker<TSys>,
12597
}
12698

127-
impl CompositeChecker {
128-
pub fn new() -> CompositeChecker {
99+
impl<TSys: Sys> CompositeChecker<TSys> {
100+
pub fn new(sys: TSys) -> Self {
129101
CompositeChecker {
130-
executable_checker: ExecutableChecker::new(),
131-
existed_checker: ExistedChecker::new(),
102+
executable_checker: ExecutableChecker::new(sys.clone()),
103+
existed_checker: ExistedChecker::new(sys),
132104
}
133105
}
134106
}
135107

136-
impl Checker for CompositeChecker {
108+
impl<TSys: Sys> Checker for CompositeChecker<TSys> {
137109
fn is_valid<F: NonFatalErrorHandler>(
138110
&self,
139111
path: &Path,

0 commit comments

Comments
 (0)