which/
finder.rs

1use crate::checker::CompositeChecker;
2use crate::error::*;
3#[cfg(windows)]
4use crate::helper::has_executable_extension;
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(&self, path: &Path) -> bool;
29}
30
31trait PathExt {
32    fn has_separator(&self) -> bool;
33
34    fn to_absolute<P>(self, cwd: P) -> PathBuf
35    where
36        P: AsRef<Path>;
37}
38
39impl PathExt for PathBuf {
40    fn has_separator(&self) -> bool {
41        self.components().count() > 1
42    }
43
44    fn to_absolute<P>(self, cwd: P) -> PathBuf
45    where
46        P: AsRef<Path>,
47    {
48        if self.is_absolute() {
49            self
50        } else {
51            let mut new_path = PathBuf::from(cwd.as_ref());
52            new_path.push(self);
53            new_path
54        }
55    }
56}
57
58pub struct Finder;
59
60impl Finder {
61    pub fn new() -> Finder {
62        Finder
63    }
64
65    pub fn find<T, U, V>(
66        &self,
67        binary_name: T,
68        paths: Option<U>,
69        cwd: Option<V>,
70        binary_checker: CompositeChecker,
71    ) -> Result<impl Iterator<Item = PathBuf>>
72    where
73        T: AsRef<OsStr>,
74        U: AsRef<OsStr>,
75        V: AsRef<Path>,
76    {
77        let path = PathBuf::from(&binary_name);
78
79        let binary_path_candidates = match cwd {
80            Some(cwd) if path.has_separator() => {
81                // Search binary in cwd if the path have a path separator.
82                Either::Left(Self::cwd_search_candidates(path, cwd).into_iter())
83            }
84            _ => {
85                // Search binary in PATHs(defined in environment variable).
86                let paths =
87                    env::split_paths(&paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?)
88                        .collect::<Vec<_>>();
89                if paths.is_empty() {
90                    return Err(Error::CannotGetCurrentDirAndPathListEmpty);
91                }
92
93                Either::Right(Self::path_search_candidates(path, paths).into_iter())
94            }
95        };
96
97        Ok(binary_path_candidates
98            .filter(move |p| binary_checker.is_valid(p))
99            .map(correct_casing))
100    }
101
102    #[cfg(feature = "regex")]
103    pub fn find_re<T>(
104        &self,
105        binary_regex: impl Borrow<Regex>,
106        paths: Option<T>,
107        binary_checker: CompositeChecker,
108    ) -> Result<impl Iterator<Item = PathBuf>>
109    where
110        T: AsRef<OsStr>,
111    {
112        let p = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
113        // Collect needs to happen in order to not have to
114        // change the API to borrow on `paths`.
115        #[allow(clippy::needless_collect)]
116        let paths: Vec<_> = env::split_paths(&p).collect();
117
118        let matching_re = paths
119            .into_iter()
120            .flat_map(fs::read_dir)
121            .flatten()
122            .flatten()
123            .map(|e| e.path())
124            .filter(move |p| {
125                if let Some(unicode_file_name) = p.file_name().unwrap().to_str() {
126                    binary_regex.borrow().is_match(unicode_file_name)
127                } else {
128                    false
129                }
130            })
131            .filter(move |p| binary_checker.is_valid(p));
132
133        Ok(matching_re)
134    }
135
136    fn cwd_search_candidates<C>(binary_name: PathBuf, cwd: C) -> impl IntoIterator<Item = PathBuf>
137    where
138        C: AsRef<Path>,
139    {
140        let path = binary_name.to_absolute(cwd);
141
142        Self::append_extension(iter::once(path))
143    }
144
145    fn path_search_candidates<P>(
146        binary_name: PathBuf,
147        paths: P,
148    ) -> impl IntoIterator<Item = PathBuf>
149    where
150        P: IntoIterator<Item = PathBuf>,
151    {
152        let new_paths = paths
153            .into_iter()
154            .map(move |p| tilde_expansion(&p).join(binary_name.clone()));
155
156        Self::append_extension(new_paths)
157    }
158
159    #[cfg(not(windows))]
160    fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf>
161    where
162        P: IntoIterator<Item = PathBuf>,
163    {
164        paths
165    }
166
167    #[cfg(windows)]
168    fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf>
169    where
170        P: IntoIterator<Item = PathBuf>,
171    {
172        use once_cell::sync::Lazy;
173
174        // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
175        // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …].
176        // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it;
177        // hence its retention.)
178        static PATH_EXTENSIONS: Lazy<Vec<String>> = Lazy::new(|| {
179            env::var("PATHEXT")
180                .map(|pathext| {
181                    pathext
182                        .split(';')
183                        .filter_map(|s| {
184                            if s.as_bytes().first() == Some(&b'.') {
185                                Some(s.to_owned())
186                            } else {
187                                // Invalid segment; just ignore it.
188                                None
189                            }
190                        })
191                        .collect()
192                })
193                // PATHEXT not being set or not being a proper Unicode string is exceedingly
194                // improbable and would probably break Windows badly. Still, don't crash:
195                .unwrap_or_default()
196        });
197
198        paths
199            .into_iter()
200            .flat_map(move |p| -> Box<dyn Iterator<Item = _>> {
201                // Check if path already have executable extension
202                if has_executable_extension(&p, &PATH_EXTENSIONS) {
203                    Box::new(iter::once(p))
204                } else {
205                    // Appended paths with windows executable extensions.
206                    // e.g. path `c:/windows/bin[.ext]` will expand to:
207                    // [c:/windows/bin.ext]
208                    // c:/windows/bin[.ext].COM
209                    // c:/windows/bin[.ext].EXE
210                    // c:/windows/bin[.ext].CMD
211                    // ...
212                    Box::new(
213                        iter::once(p.clone()).chain(PATH_EXTENSIONS.iter().map(move |e| {
214                            // Append the extension.
215                            let mut p = p.clone().into_os_string();
216                            p.push(e);
217
218                            PathBuf::from(p)
219                        })),
220                    )
221                }
222            })
223    }
224}
225
226fn tilde_expansion(p: &PathBuf) -> Cow<'_, PathBuf> {
227    let mut component_iter = p.components();
228    if let Some(Component::Normal(o)) = component_iter.next() {
229        if o == "~" {
230            let mut new_path = home_dir().unwrap_or_default();
231            new_path.extend(component_iter);
232            Cow::Owned(new_path)
233        } else {
234            Cow::Borrowed(p)
235        }
236    } else {
237        Cow::Borrowed(p)
238    }
239}
240
241#[cfg(target_os = "windows")]
242fn correct_casing(mut p: PathBuf) -> PathBuf {
243    if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) {
244        if let Ok(iter) = fs::read_dir(parent) {
245            for e in iter.filter_map(std::result::Result::ok) {
246                if e.file_name().eq_ignore_ascii_case(file_name) {
247                    p.pop();
248                    p.push(e.file_name());
249                    break;
250                }
251            }
252        }
253    }
254    p
255}
256
257#[cfg(not(target_os = "windows"))]
258fn correct_casing(p: PathBuf) -> PathBuf {
259    p
260}