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