which/
finder.rs

1use crate::checker::CompositeChecker;
2#[cfg(windows)]
3use crate::helper::has_executable_extension;
4use crate::{error::*, NonFatalErrorHandler};
5use either::Either;
6#[cfg(feature = "regex")]
7use regex::Regex;
8#[cfg(feature = "regex")]
9use std::borrow::Borrow;
10use std::borrow::Cow;
11use std::env;
12use std::ffi::OsStr;
13#[cfg(any(feature = "regex", target_os = "windows"))]
14use std::fs;
15use std::iter;
16use std::path::{Component, Path, PathBuf};
17
18// Home dir shim, use home crate when possible. Otherwise, return None
19#[cfg(any(windows, unix, target_os = "redox"))]
20use home::home_dir;
21
22#[cfg(not(any(windows, unix, target_os = "redox")))]
23fn home_dir() -> Option<std::path::PathBuf> {
24    None
25}
26
27pub trait Checker {
28    fn is_valid<F: NonFatalErrorHandler>(
29        &self,
30        path: &Path,
31        nonfatal_error_handler: &mut F,
32    ) -> bool;
33}
34
35trait PathExt {
36    fn has_separator(&self) -> bool;
37
38    fn to_absolute<P>(self, cwd: P) -> PathBuf
39    where
40        P: AsRef<Path>;
41}
42
43impl PathExt for PathBuf {
44    fn has_separator(&self) -> bool {
45        self.components().count() > 1
46    }
47
48    fn to_absolute<P>(self, cwd: P) -> PathBuf
49    where
50        P: AsRef<Path>,
51    {
52        if self.is_absolute() {
53            self
54        } else {
55            let mut new_path = PathBuf::from(cwd.as_ref());
56            new_path.push(self);
57            new_path
58        }
59    }
60}
61
62pub struct Finder;
63
64impl Finder {
65    pub fn new() -> Finder {
66        Finder
67    }
68
69    pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>(
70        &self,
71        binary_name: T,
72        paths: Option<U>,
73        cwd: Option<V>,
74        binary_checker: CompositeChecker,
75        mut nonfatal_error_handler: F,
76    ) -> Result<impl Iterator<Item = PathBuf> + 'a>
77    where
78        T: AsRef<OsStr>,
79        U: AsRef<OsStr>,
80        V: AsRef<Path> + 'a,
81    {
82        let path = PathBuf::from(&binary_name);
83
84        #[cfg(feature = "tracing")]
85        tracing::debug!(
86            "query binary_name = {:?}, paths = {:?}, cwd = {:?}",
87            binary_name.as_ref().to_string_lossy(),
88            paths.as_ref().map(|p| p.as_ref().to_string_lossy()),
89            cwd.as_ref().map(|p| p.as_ref().display())
90        );
91
92        let binary_path_candidates = match cwd {
93            Some(cwd) if path.has_separator() => {
94                #[cfg(feature = "tracing")]
95                tracing::trace!(
96                    "{} has a path seperator, so only CWD will be searched.",
97                    path.display()
98                );
99                // Search binary in cwd if the path have a path separator.
100                Either::Left(Self::cwd_search_candidates(path, cwd))
101            }
102            _ => {
103                #[cfg(feature = "tracing")]
104                tracing::trace!("{} has no path seperators, so only paths in PATH environment variable will be searched.", path.display());
105                // Search binary in PATHs(defined in environment variable).
106                let paths = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
107                let paths = env::split_paths(&paths).collect::<Vec<_>>();
108                if paths.is_empty() {
109                    return Err(Error::CannotGetCurrentDirAndPathListEmpty);
110                }
111
112                Either::Right(Self::path_search_candidates(path, paths))
113            }
114        };
115        let ret = binary_path_candidates.into_iter().filter_map(move |p| {
116            binary_checker
117                .is_valid(&p, &mut nonfatal_error_handler)
118                .then(|| correct_casing(p, &mut nonfatal_error_handler))
119        });
120        #[cfg(feature = "tracing")]
121        let ret = ret.inspect(|p| {
122            tracing::debug!("found path {}", p.display());
123        });
124        Ok(ret)
125    }
126
127    #[cfg(feature = "regex")]
128    pub fn find_re<T, F: NonFatalErrorHandler>(
129        &self,
130        binary_regex: impl Borrow<Regex>,
131        paths: Option<T>,
132        binary_checker: CompositeChecker,
133        mut nonfatal_error_handler: F,
134    ) -> Result<impl Iterator<Item = PathBuf>>
135    where
136        T: AsRef<OsStr>,
137    {
138        let p = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
139        // Collect needs to happen in order to not have to
140        // change the API to borrow on `paths`.
141        #[allow(clippy::needless_collect)]
142        let paths: Vec<_> = env::split_paths(&p).collect();
143
144        let matching_re = paths
145            .into_iter()
146            .flat_map(fs::read_dir)
147            .flatten()
148            .flatten()
149            .map(|e| e.path())
150            .filter(move |p| {
151                if let Some(unicode_file_name) = p.file_name().unwrap().to_str() {
152                    binary_regex.borrow().is_match(unicode_file_name)
153                } else {
154                    false
155                }
156            })
157            .filter(move |p| binary_checker.is_valid(p, &mut nonfatal_error_handler));
158
159        Ok(matching_re)
160    }
161
162    fn cwd_search_candidates<C>(binary_name: PathBuf, cwd: C) -> impl IntoIterator<Item = PathBuf>
163    where
164        C: AsRef<Path>,
165    {
166        let path = binary_name.to_absolute(cwd);
167
168        Self::append_extension(iter::once(path))
169    }
170
171    fn path_search_candidates<P>(
172        binary_name: PathBuf,
173        paths: P,
174    ) -> impl IntoIterator<Item = PathBuf>
175    where
176        P: IntoIterator<Item = PathBuf>,
177    {
178        let new_paths = paths
179            .into_iter()
180            .map(move |p| tilde_expansion(&p).join(binary_name.clone()));
181
182        Self::append_extension(new_paths)
183    }
184
185    #[cfg(not(windows))]
186    fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf>
187    where
188        P: IntoIterator<Item = PathBuf>,
189    {
190        paths
191    }
192
193    #[cfg(windows)]
194    fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf>
195    where
196        P: IntoIterator<Item = PathBuf>,
197    {
198        use std::sync::OnceLock;
199
200        // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
201        // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …].
202        // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it;
203        // hence its retention.)
204        static PATH_EXTENSIONS: OnceLock<Vec<String>> = OnceLock::new();
205
206        paths
207            .into_iter()
208            .flat_map(move |p| -> Box<dyn Iterator<Item = _>> {
209                let path_extensions = PATH_EXTENSIONS.get_or_init(|| {
210                    env::var("PATHEXT")
211                        .map(|pathext| {
212                            pathext
213                                .split(';')
214                                .filter_map(|s| {
215                                    if s.as_bytes().first() == Some(&b'.') {
216                                        Some(s.to_owned())
217                                    } else {
218                                        // Invalid segment; just ignore it.
219                                        None
220                                    }
221                                })
222                                .collect()
223                        })
224                        // PATHEXT not being set or not being a proper Unicode string is exceedingly
225                        // improbable and would probably break Windows badly. Still, don't crash:
226                        .unwrap_or_default()
227                });
228                // Check if path already have executable extension
229                if has_executable_extension(&p, path_extensions) {
230                    #[cfg(feature = "tracing")]
231                    tracing::trace!(
232                        "{} already has an executable extension, not modifying it further",
233                        p.display()
234                    );
235                    Box::new(iter::once(p))
236                } else {
237                    #[cfg(feature = "tracing")]
238                    tracing::trace!(
239                        "{} has no extension, using PATHEXT environment variable to infer one",
240                        p.display()
241                    );
242                    // Appended paths with windows executable extensions.
243                    // e.g. path `c:/windows/bin[.ext]` will expand to:
244                    // [c:/windows/bin.ext]
245                    // c:/windows/bin[.ext].COM
246                    // c:/windows/bin[.ext].EXE
247                    // c:/windows/bin[.ext].CMD
248                    // ...
249                    Box::new(
250                        iter::once(p.clone()).chain(path_extensions.iter().map(move |e| {
251                            // Append the extension.
252                            let mut p = p.clone().into_os_string();
253                            p.push(e);
254                            let ret = PathBuf::from(p);
255                            #[cfg(feature = "tracing")]
256                            tracing::trace!("possible extension: {}", ret.display());
257                            ret
258                        })),
259                    )
260                }
261            })
262    }
263}
264
265fn tilde_expansion(p: &PathBuf) -> Cow<'_, PathBuf> {
266    let mut component_iter = p.components();
267    if let Some(Component::Normal(o)) = component_iter.next() {
268        if o == "~" {
269            let mut new_path = home_dir().unwrap_or_default();
270            new_path.extend(component_iter);
271            #[cfg(feature = "tracing")]
272            tracing::trace!(
273                "found tilde, substituting in user's home directory to get {}",
274                new_path.display()
275            );
276            Cow::Owned(new_path)
277        } else {
278            Cow::Borrowed(p)
279        }
280    } else {
281        Cow::Borrowed(p)
282    }
283}
284
285#[cfg(target_os = "windows")]
286fn correct_casing<F: NonFatalErrorHandler>(
287    mut p: PathBuf,
288    nonfatal_error_handler: &mut F,
289) -> PathBuf {
290    if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) {
291        if let Ok(iter) = fs::read_dir(parent) {
292            for e in iter {
293                match e {
294                    Ok(e) => {
295                        if e.file_name().eq_ignore_ascii_case(file_name) {
296                            p.pop();
297                            p.push(e.file_name());
298                            break;
299                        }
300                    }
301                    Err(e) => {
302                        nonfatal_error_handler.handle(NonFatalError::Io(e));
303                    }
304                }
305            }
306        }
307    }
308    p
309}
310
311#[cfg(not(target_os = "windows"))]
312fn correct_casing<F: NonFatalErrorHandler>(p: PathBuf, _nonfatal_error_handler: &mut F) -> PathBuf {
313    p
314}