which/
sys.rs

1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::ffi::OsString;
4use std::io;
5use std::path::Path;
6use std::path::PathBuf;
7
8pub trait SysReadDirEntry {
9    /// Gets the file name of the directory entry, not the full path.
10    fn file_name(&self) -> OsString;
11    /// Gets the full path of the directory entry.
12    fn path(&self) -> PathBuf;
13}
14
15pub trait SysMetadata {
16    /// Gets if the path is a symlink.
17    fn is_symlink(&self) -> bool;
18    /// Gets if the path is a file.
19    fn is_file(&self) -> bool;
20}
21
22/// Represents the system that `which` interacts with to get information
23/// about the environment and file system.
24///
25/// ### How to use in Wasm without WASI
26///
27/// WebAssembly without WASI does not have a filesystem, but using this crate is possible in `wasm32-unknown-unknown` targets by disabling default features:
28///
29/// ```toml
30/// which = { version = "...", default-features = false }
31/// ```
32///
33// Then providing your own implementation of the `which::sys::Sys` trait:
34///
35/// ```rs
36/// use which::WhichConfig;
37///
38/// struct WasmSys;
39///
40/// impl which::sys::Sys for WasmSys {
41///     // it is up to you to implement this trait based on the
42///     // environment you are running WebAssembly in
43/// }
44///
45/// let paths = WhichConfig::new_with_sys(WasmSys)
46///     .all_results()
47///     .unwrap()
48///     .collect::<Vec<_>>();
49/// ```
50pub trait Sys {
51    type ReadDirEntry: SysReadDirEntry;
52    type Metadata: SysMetadata;
53
54    /// Check if the current platform is Windows.
55    ///
56    /// This can be set to true in wasm32-unknown-unknown targets that
57    /// are running on Windows systems.
58    fn is_windows(&self) -> bool;
59    /// Gets the current working directory.
60    fn current_dir(&self) -> io::Result<PathBuf>;
61    /// Gets the home directory of the current user.
62    fn home_dir(&self) -> Option<PathBuf>;
63    /// Splits a platform-specific PATH variable into a list of paths.
64    fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf>;
65    /// Gets the value of the PATH environment variable.
66    fn env_path(&self) -> Option<OsString>;
67    /// Gets the value of the PATHEXT environment variable. If not on Windows, simply return None.
68    fn env_path_ext(&self) -> Option<OsString>;
69    /// Gets and parses the PATHEXT environment variable on Windows.
70    ///
71    /// Override this to enable caching the parsed PATHEXT.
72    ///
73    /// Note: This will only be called when `is_windows()` returns `true`
74    /// and isn't conditionally compiled with `#[cfg(windows)]` so that it
75    /// can work in Wasm.
76    fn env_windows_path_ext(&self) -> Cow<'static, [String]> {
77        Cow::Owned(parse_path_ext(self.env_path_ext()))
78    }
79    /// Gets the metadata of the provided path, following symlinks.
80    fn metadata(&self, path: &Path) -> io::Result<Self::Metadata>;
81    /// Gets the metadata of the provided path, not following symlinks.
82    fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata>;
83    /// Reads the directory entries of the provided path.
84    fn read_dir(
85        &self,
86        path: &Path,
87    ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>>;
88    /// Checks if the provided path is a valid executable.
89    fn is_valid_executable(&self, path: &Path) -> io::Result<bool>;
90}
91
92impl SysReadDirEntry for std::fs::DirEntry {
93    fn file_name(&self) -> OsString {
94        self.file_name()
95    }
96
97    fn path(&self) -> PathBuf {
98        self.path()
99    }
100}
101
102impl SysMetadata for std::fs::Metadata {
103    fn is_symlink(&self) -> bool {
104        self.file_type().is_symlink()
105    }
106
107    fn is_file(&self) -> bool {
108        self.file_type().is_file()
109    }
110}
111
112#[cfg(feature = "real-sys")]
113#[derive(Default, Clone, Copy)]
114pub struct RealSys;
115
116#[cfg(feature = "real-sys")]
117impl RealSys {
118    #[inline]
119    pub(crate) fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
120        #[allow(clippy::disallowed_methods)] // ok, sys implementation
121        std::fs::canonicalize(path)
122    }
123}
124
125#[cfg(feature = "real-sys")]
126impl Sys for RealSys {
127    type ReadDirEntry = std::fs::DirEntry;
128    type Metadata = std::fs::Metadata;
129
130    #[inline]
131    fn is_windows(&self) -> bool {
132        // Again, do not change the code to directly use `#[cfg(windows)]`
133        // because we want to allow people to implement this code in Wasm
134        // and then tell at runtime if running on a Windows system.
135        cfg!(windows)
136    }
137
138    #[inline]
139    fn current_dir(&self) -> io::Result<PathBuf> {
140        #[allow(clippy::disallowed_methods)] // ok, sys implementation
141        std::env::current_dir()
142    }
143
144    #[inline]
145    fn home_dir(&self) -> Option<PathBuf> {
146        // Home dir shim, use env_home crate when possible. Otherwise, return None
147        #[cfg(any(windows, unix, target_os = "redox"))]
148        {
149            env_home::env_home_dir()
150        }
151        #[cfg(not(any(windows, unix, target_os = "redox")))]
152        {
153            None
154        }
155    }
156
157    #[inline]
158    fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
159        #[allow(clippy::disallowed_methods)] // ok, sys implementation
160        std::env::split_paths(paths).collect()
161    }
162
163    fn env_windows_path_ext(&self) -> Cow<'static, [String]> {
164        use std::sync::OnceLock;
165
166        // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
167        // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …].
168        // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it;
169        // hence its retention.)
170        static PATH_EXTENSIONS: OnceLock<Vec<String>> = OnceLock::new();
171        let path_extensions = PATH_EXTENSIONS.get_or_init(|| parse_path_ext(self.env_path_ext()));
172        Cow::Borrowed(path_extensions)
173    }
174
175    #[inline]
176    fn env_path(&self) -> Option<OsString> {
177        #[allow(clippy::disallowed_methods)] // ok, sys implementation
178        std::env::var_os("PATH")
179    }
180
181    #[inline]
182    fn env_path_ext(&self) -> Option<OsString> {
183        #[allow(clippy::disallowed_methods)] // ok, sys implementation
184        std::env::var_os("PATHEXT")
185    }
186
187    #[inline]
188    fn read_dir(
189        &self,
190        path: &Path,
191    ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
192        #[allow(clippy::disallowed_methods)] // ok, sys implementation
193        let iter = std::fs::read_dir(path)?;
194        Ok(Box::new(iter))
195    }
196
197    #[inline]
198    fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
199        #[allow(clippy::disallowed_methods)] // ok, sys implementation
200        std::fs::metadata(path)
201    }
202
203    #[inline]
204    fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
205        #[allow(clippy::disallowed_methods)] // ok, sys implementation
206        std::fs::symlink_metadata(path)
207    }
208
209    #[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
210    fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
211        use rustix::fs as rfs;
212        rfs::access(path, rfs::Access::EXEC_OK)
213            .map(|_| true)
214            .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
215    }
216
217    #[cfg(windows)]
218    fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
219        winsafe::GetBinaryType(&path.display().to_string())
220            .map(|_| true)
221            .map_err(|e| io::Error::from_raw_os_error(e.raw() as i32))
222    }
223}
224
225impl<T> Sys for &T
226where
227    T: Sys,
228{
229    type ReadDirEntry = T::ReadDirEntry;
230
231    type Metadata = T::Metadata;
232
233    fn is_windows(&self) -> bool {
234        (*self).is_windows()
235    }
236
237    fn current_dir(&self) -> io::Result<PathBuf> {
238        (*self).current_dir()
239    }
240
241    fn home_dir(&self) -> Option<PathBuf> {
242        (*self).home_dir()
243    }
244
245    fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
246        (*self).env_split_paths(paths)
247    }
248
249    fn env_path(&self) -> Option<OsString> {
250        (*self).env_path()
251    }
252
253    fn env_path_ext(&self) -> Option<OsString> {
254        (*self).env_path_ext()
255    }
256
257    fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
258        (*self).metadata(path)
259    }
260
261    fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
262        (*self).symlink_metadata(path)
263    }
264
265    fn read_dir(
266        &self,
267        path: &Path,
268    ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
269        (*self).read_dir(path)
270    }
271
272    fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
273        (*self).is_valid_executable(path)
274    }
275}
276
277fn parse_path_ext(pathext: Option<OsString>) -> Vec<String> {
278    pathext
279        .and_then(|pathext| {
280            // If tracing feature enabled then this lint is incorrect, so disable it.
281            #[allow(clippy::manual_ok_err)]
282            match pathext.into_string() {
283                Ok(pathext) => Some(pathext),
284                Err(_) => {
285                    #[cfg(feature = "tracing")]
286                    tracing::error!("pathext is not valid unicode");
287                    None
288                }
289            }
290        })
291        .map(|pathext| {
292            pathext
293                .split(';')
294                .filter_map(|s| {
295                    if s.as_bytes().first() == Some(&b'.') {
296                        Some(s.to_owned())
297                    } else {
298                        // Invalid segment; just ignore it.
299                        #[cfg(feature = "tracing")]
300                        tracing::debug!("PATHEXT segment \"{s}\" missing leading dot, ignoring");
301                        None
302                    }
303                })
304                .collect()
305        })
306        // PATHEXT not being set or not being a proper Unicode string is exceedingly
307        // improbable and would probably break Windows badly. Still, don't crash:
308        .unwrap_or_default()
309}