which/
finder.rs

1use crate::checker::is_valid;
2use crate::helper::has_executable_extension;
3use crate::sys::Sys;
4use crate::sys::SysReadDirEntry;
5use crate::{error::*, NonFatalErrorHandler};
6#[cfg(feature = "regex")]
7use regex::Regex;
8#[cfg(feature = "regex")]
9use std::borrow::Borrow;
10use std::borrow::Cow;
11use std::ffi::OsStr;
12#[cfg(feature = "regex")]
13use std::io;
14use std::path::{Component, Path, PathBuf};
15use std::vec;
16
17trait PathExt {
18    fn has_separator(&self) -> bool;
19
20    fn to_absolute<P>(self, cwd: P) -> PathBuf
21    where
22        P: AsRef<Path>;
23}
24
25impl PathExt for PathBuf {
26    fn has_separator(&self) -> bool {
27        self.components().count() > 1
28    }
29
30    fn to_absolute<P>(self, cwd: P) -> PathBuf
31    where
32        P: AsRef<Path>,
33    {
34        if self.is_absolute() {
35            self
36        } else {
37            let mut new_path = PathBuf::from(cwd.as_ref());
38            new_path.extend(
39                self.components()
40                    .skip_while(|c| matches!(c, Component::CurDir)),
41            );
42            new_path
43        }
44    }
45}
46
47pub struct Finder<TSys: Sys> {
48    sys: TSys,
49}
50
51impl<TSys: Sys> Finder<TSys> {
52    pub fn new(sys: TSys) -> Self {
53        Finder { sys }
54    }
55
56    pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>(
57        self,
58        binary_name: T,
59        paths: Option<U>,
60        cwd: Option<V>,
61        nonfatal_error_handler: F,
62    ) -> Result<impl Iterator<Item = PathBuf> + 'a>
63    where
64        TSys: 'a,
65        T: AsRef<OsStr>,
66        U: AsRef<OsStr>,
67        V: AsRef<Path> + 'a,
68    {
69        let path = PathBuf::from(&binary_name);
70
71        #[cfg(feature = "tracing")]
72        tracing::debug!(
73            "query binary_name = {:?}, paths = {:?}, cwd = {:?}",
74            binary_name.as_ref().to_string_lossy(),
75            paths.as_ref().map(|p| p.as_ref().to_string_lossy()),
76            cwd.as_ref().map(|p| p.as_ref().display())
77        );
78
79        let ret = match cwd {
80            Some(cwd) if path.has_separator() => {
81                WhichFindIterator::new_cwd(path, cwd.as_ref(), self.sys, nonfatal_error_handler)
82            }
83            _ => {
84                #[cfg(feature = "tracing")]
85                tracing::trace!("{} has no path seperators, so only paths in PATH environment variable will be searched.", path.display());
86                // Search binary in PATHs(defined in environment variable).
87                let paths = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
88                let paths = self.sys.env_split_paths(paths.as_ref());
89                if paths.is_empty() {
90                    return Err(Error::CannotGetCurrentDirAndPathListEmpty);
91                }
92                WhichFindIterator::new_paths(path, paths, self.sys, nonfatal_error_handler)
93            }
94        };
95        #[cfg(feature = "tracing")]
96        let ret = ret.inspect(|p| {
97            tracing::debug!("found path {}", p.display());
98        });
99        Ok(ret)
100    }
101
102    #[cfg(feature = "regex")]
103    pub fn find_re<T, F: NonFatalErrorHandler>(
104        self,
105        binary_regex: impl std::borrow::Borrow<Regex>,
106        paths: Option<T>,
107        nonfatal_error_handler: F,
108    ) -> Result<impl Iterator<Item = PathBuf>>
109    where
110        T: AsRef<OsStr>,
111    {
112        WhichFindRegexIter::new(self.sys, paths, binary_regex, nonfatal_error_handler)
113    }
114}
115
116struct WhichFindIterator<TSys: Sys, F: NonFatalErrorHandler> {
117    sys: TSys,
118    paths: PathsIter<vec::IntoIter<PathBuf>>,
119    nonfatal_error_handler: F,
120}
121
122impl<TSys: Sys, F: NonFatalErrorHandler> WhichFindIterator<TSys, F> {
123    pub fn new_cwd(binary_name: PathBuf, cwd: &Path, sys: TSys, nonfatal_error_handler: F) -> Self {
124        let path_extensions = if sys.is_windows() {
125            sys.env_windows_path_ext()
126        } else {
127            Cow::Borrowed(Default::default())
128        };
129        Self {
130            sys,
131            paths: PathsIter {
132                paths: vec![binary_name.to_absolute(cwd)].into_iter(),
133                current_path_with_index: None,
134                path_extensions,
135            },
136            nonfatal_error_handler,
137        }
138    }
139
140    pub fn new_paths(
141        binary_name: PathBuf,
142        paths: Vec<PathBuf>,
143        sys: TSys,
144        nonfatal_error_handler: F,
145    ) -> Self {
146        let path_extensions = if sys.is_windows() {
147            sys.env_windows_path_ext()
148        } else {
149            Cow::Borrowed(Default::default())
150        };
151        let paths = paths
152            .iter()
153            .map(|p| tilde_expansion(&sys, p).join(&binary_name))
154            .collect::<Vec<_>>();
155        Self {
156            sys,
157            paths: PathsIter {
158                paths: paths.into_iter(),
159                current_path_with_index: None,
160                path_extensions,
161            },
162            nonfatal_error_handler,
163        }
164    }
165}
166
167impl<TSys: Sys, F: NonFatalErrorHandler> Iterator for WhichFindIterator<TSys, F> {
168    type Item = PathBuf;
169
170    fn next(&mut self) -> Option<Self::Item> {
171        for path in &mut self.paths {
172            if is_valid(&self.sys, &path, &mut self.nonfatal_error_handler) {
173                return Some(correct_casing(
174                    &self.sys,
175                    path,
176                    &mut self.nonfatal_error_handler,
177                ));
178            }
179        }
180        None
181    }
182}
183
184struct PathsIter<P>
185where
186    P: Iterator<Item = PathBuf>,
187{
188    paths: P,
189    current_path_with_index: Option<(PathBuf, usize)>,
190    path_extensions: Cow<'static, [String]>,
191}
192
193impl<P> Iterator for PathsIter<P>
194where
195    P: Iterator<Item = PathBuf>,
196{
197    type Item = PathBuf;
198
199    fn next(&mut self) -> Option<Self::Item> {
200        if self.path_extensions.is_empty() {
201            self.paths.next()
202        } else if let Some((p, index)) = self.current_path_with_index.take() {
203            let next_index = index + 1;
204            if next_index < self.path_extensions.len() {
205                self.current_path_with_index = Some((p.clone(), next_index));
206            }
207            // Append the extension.
208            let mut p = p.into_os_string();
209            p.push(&self.path_extensions[index]);
210            let ret = PathBuf::from(p);
211            #[cfg(feature = "tracing")]
212            tracing::trace!("possible extension: {}", ret.display());
213            Some(ret)
214        } else {
215            let p = self.paths.next()?;
216            if has_executable_extension(&p, &self.path_extensions) {
217                #[cfg(feature = "tracing")]
218                tracing::trace!(
219                    "{} already has an executable extension, not modifying it further",
220                    p.display()
221                );
222            } else {
223                #[cfg(feature = "tracing")]
224                tracing::trace!(
225                    "{} has no extension, using PATHEXT environment variable to infer one",
226                    p.display()
227                );
228                // Appended paths with windows executable extensions.
229                // e.g. path `c:/windows/bin[.ext]` will expand to:
230                // [c:/windows/bin.ext]
231                // c:/windows/bin[.ext].COM
232                // c:/windows/bin[.ext].EXE
233                // c:/windows/bin[.ext].CMD
234                // ...
235                self.current_path_with_index = Some((p.clone(), 0));
236            }
237            Some(p)
238        }
239    }
240}
241
242fn tilde_expansion<TSys: Sys>(sys: TSys, p: &Path) -> Cow<'_, Path> {
243    let mut component_iter = p.components();
244    if let Some(Component::Normal(o)) = component_iter.next() {
245        if o == "~" {
246            let new_path = sys.home_dir();
247            if let Some(mut new_path) = new_path {
248                new_path.extend(component_iter);
249                #[cfg(feature = "tracing")]
250                tracing::trace!(
251                    "found tilde, substituting in user's home directory to get {}",
252                    new_path.display()
253                );
254                return Cow::Owned(new_path);
255            } else {
256                #[cfg(feature = "tracing")]
257                tracing::trace!("found tilde in path, but user's home directory couldn't be found");
258            }
259        }
260    }
261    Cow::Borrowed(p)
262}
263
264fn correct_casing<TSys: Sys, F: NonFatalErrorHandler>(
265    sys: TSys,
266    mut p: PathBuf,
267    nonfatal_error_handler: &mut F,
268) -> PathBuf {
269    if sys.is_windows() {
270        if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) {
271            if let Ok(iter) = sys.read_dir(parent) {
272                for e in iter {
273                    match e {
274                        Ok(e) => {
275                            if e.file_name().eq_ignore_ascii_case(file_name) {
276                                p.pop();
277                                p.push(e.file_name());
278                                break;
279                            }
280                        }
281                        Err(e) => {
282                            nonfatal_error_handler.handle(NonFatalError::Io(e));
283                        }
284                    }
285                }
286            }
287        }
288    }
289    p
290}
291
292#[cfg(feature = "regex")]
293struct WhichFindRegexIter<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> {
294    sys: TSys,
295    re: B,
296    paths: vec::IntoIter<PathBuf>,
297    nonfatal_error_handler: F,
298    current_read_dir_iter: Option<Box<dyn Iterator<Item = io::Result<TSys::ReadDirEntry>>>>,
299}
300
301#[cfg(feature = "regex")]
302impl<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> WhichFindRegexIter<TSys, B, F> {
303    pub fn new<T: AsRef<OsStr>>(
304        sys: TSys,
305        paths: Option<T>,
306        re: B,
307        nonfatal_error_handler: F,
308    ) -> Result<Self> {
309        let p = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
310        let paths = sys.env_split_paths(p.as_ref());
311        Ok(WhichFindRegexIter {
312            sys,
313            re,
314            paths: paths.into_iter(),
315            nonfatal_error_handler,
316            current_read_dir_iter: None,
317        })
318    }
319}
320
321#[cfg(feature = "regex")]
322impl<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> Iterator
323    for WhichFindRegexIter<TSys, B, F>
324{
325    type Item = PathBuf;
326
327    fn next(&mut self) -> Option<Self::Item> {
328        loop {
329            if let Some(iter) = &mut self.current_read_dir_iter {
330                match iter.next() {
331                    Some(Ok(path)) => {
332                        if let Some(unicode_file_name) = path.file_name().to_str() {
333                            if self.re.borrow().is_match(unicode_file_name) {
334                                return Some(path.path());
335                            } else {
336                                #[cfg(feature = "tracing")]
337                                tracing::debug!("regex filtered out {}", unicode_file_name);
338                            }
339                        } else {
340                            #[cfg(feature = "tracing")]
341                            tracing::debug!("regex unable to evaluate filename as it's not valid unicode. Lossy filename conversion: {}", path.file_name().to_string_lossy());
342                        }
343                    }
344                    Some(Err(e)) => {
345                        self.nonfatal_error_handler.handle(NonFatalError::Io(e));
346                    }
347                    None => {
348                        self.current_read_dir_iter = None;
349                    }
350                }
351            } else {
352                let path = self.paths.next();
353                if let Some(path) = path {
354                    match self.sys.read_dir(&path) {
355                        Ok(new_read_dir_iter) => {
356                            self.current_read_dir_iter = Some(new_read_dir_iter);
357                        }
358                        Err(e) => {
359                            self.nonfatal_error_handler.handle(NonFatalError::Io(e));
360                        }
361                    }
362                } else {
363                    return None;
364                }
365            }
366        }
367    }
368}