Skip to main content

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        #[allow(clippy::disallowed_methods)] // ok, sys implementation
147        #[allow(deprecated)] // only deprecated <1.85
148        std::env::home_dir()
149    }
150
151    #[inline]
152    fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
153        #[allow(clippy::disallowed_methods)] // ok, sys implementation
154        std::env::split_paths(paths).collect()
155    }
156
157    fn env_windows_path_ext(&self) -> Cow<'static, [String]> {
158        use std::sync::OnceLock;
159
160        // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
161        // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …].
162        // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it;
163        // hence its retention.)
164        static PATH_EXTENSIONS: OnceLock<Vec<String>> = OnceLock::new();
165        let path_extensions = PATH_EXTENSIONS.get_or_init(|| parse_path_ext(self.env_path_ext()));
166        Cow::Borrowed(path_extensions)
167    }
168
169    #[inline]
170    fn env_path(&self) -> Option<OsString> {
171        #[allow(clippy::disallowed_methods)] // ok, sys implementation
172        std::env::var_os("PATH")
173    }
174
175    #[inline]
176    fn env_path_ext(&self) -> Option<OsString> {
177        #[allow(clippy::disallowed_methods)] // ok, sys implementation
178        std::env::var_os("PATHEXT")
179    }
180
181    #[inline]
182    fn read_dir(
183        &self,
184        path: &Path,
185    ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
186        #[allow(clippy::disallowed_methods)] // ok, sys implementation
187        let iter = std::fs::read_dir(path)?;
188        Ok(Box::new(iter))
189    }
190
191    #[inline]
192    fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
193        #[allow(clippy::disallowed_methods)] // ok, sys implementation
194        std::fs::metadata(path)
195    }
196
197    #[inline]
198    fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
199        #[allow(clippy::disallowed_methods)] // ok, sys implementation
200        std::fs::symlink_metadata(path)
201    }
202
203    #[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
204    fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
205        use std::ffi::CString;
206        #[cfg(any(unix, target_os = "redox"))]
207        use std::os::unix::ffi::OsStrExt;
208        #[cfg(target_os = "wasi")]
209        use std::os::wasi::ffi::OsStrExt;
210
211        let path = CString::new(path.as_os_str().as_bytes())?;
212        if unsafe { libc::access(path.as_ptr(), libc::X_OK) } == 0 {
213            Ok(true)
214        } else {
215            Err(io::Error::last_os_error())
216        }
217    }
218
219    #[cfg(windows)]
220    fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
221        use std::os::windows::ffi::OsStrExt;
222
223        use crate::win_ffi;
224        let w_str: Vec<u16> = path
225            .as_os_str()
226            .encode_wide()
227            .chain(std::iter::once(0))
228            .collect();
229        let mut binary_type = 0u32;
230        unsafe {
231            let success = win_ffi::GetBinaryTypeW(w_str.as_ptr(), &mut binary_type);
232            if success != 0 {
233                Ok(true)
234            } else {
235                Err(io::Error::from_raw_os_error(win_ffi::GetLastError() as i32))
236            }
237        }
238    }
239}
240
241impl<T> Sys for &T
242where
243    T: Sys,
244{
245    type ReadDirEntry = T::ReadDirEntry;
246
247    type Metadata = T::Metadata;
248
249    fn is_windows(&self) -> bool {
250        (*self).is_windows()
251    }
252
253    fn current_dir(&self) -> io::Result<PathBuf> {
254        (*self).current_dir()
255    }
256
257    fn home_dir(&self) -> Option<PathBuf> {
258        (*self).home_dir()
259    }
260
261    fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
262        (*self).env_split_paths(paths)
263    }
264
265    fn env_path(&self) -> Option<OsString> {
266        (*self).env_path()
267    }
268
269    fn env_path_ext(&self) -> Option<OsString> {
270        (*self).env_path_ext()
271    }
272
273    fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
274        (*self).metadata(path)
275    }
276
277    fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
278        (*self).symlink_metadata(path)
279    }
280
281    fn read_dir(
282        &self,
283        path: &Path,
284    ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
285        (*self).read_dir(path)
286    }
287
288    fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
289        (*self).is_valid_executable(path)
290    }
291}
292
293fn parse_path_ext(pathext: Option<OsString>) -> Vec<String> {
294    pathext
295        .and_then(|pathext| {
296            // If tracing feature enabled then this lint is incorrect, so disable it.
297            #[allow(clippy::manual_ok_err)]
298            match pathext.into_string() {
299                Ok(pathext) => Some(pathext),
300                Err(_) => {
301                    #[cfg(feature = "tracing")]
302                    tracing::error!("pathext is not valid unicode");
303                    None
304                }
305            }
306        })
307        .map(|pathext| {
308            pathext
309                .split(';')
310                .filter_map(|s| {
311                    if s.as_bytes().first() == Some(&b'.') {
312                        Some(s.to_owned())
313                    } else {
314                        // Invalid segment; just ignore it.
315                        #[cfg(feature = "tracing")]
316                        tracing::debug!("PATHEXT segment \"{s}\" missing leading dot, ignoring");
317                        None
318                    }
319                })
320                .collect()
321        })
322        // PATHEXT not being set or not being a proper Unicode string is exceedingly
323        // improbable and would probably break Windows badly. Still, don't crash:
324        .unwrap_or_default()
325}