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