1 use crate::checker::CompositeChecker;
2 use crate::error::*;
3 #[cfg(windows)]
4 use crate::helper::has_executable_extension;
5 use either::Either;
6 #[cfg(feature = "regex")]
7 use regex::Regex;
8 #[cfg(feature = "regex")]
9 use std::borrow::Borrow;
10 use std::env;
11 use std::ffi::OsStr;
12 #[cfg(any(feature = "regex", target_os = "windows"))]
13 use std::fs;
14 use std::iter;
15 use std::path::{Path, PathBuf};
16
17 pub trait Checker {
is_valid(&self, path: &Path) -> bool18 fn is_valid(&self, path: &Path) -> bool;
19 }
20
21 trait PathExt {
has_separator(&self) -> bool22 fn has_separator(&self) -> bool;
23
to_absolute<P>(self, cwd: P) -> PathBuf where P: AsRef<Path>24 fn to_absolute<P>(self, cwd: P) -> PathBuf
25 where
26 P: AsRef<Path>;
27 }
28
29 impl PathExt for PathBuf {
has_separator(&self) -> bool30 fn has_separator(&self) -> bool {
31 self.components().count() > 1
32 }
33
to_absolute<P>(self, cwd: P) -> PathBuf where P: AsRef<Path>,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
48 pub struct Finder;
49
50 impl Finder {
new() -> Finder51 pub fn new() -> Finder {
52 Finder
53 }
54
find<T, U, V>( &self, binary_name: T, paths: Option<U>, cwd: Option<V>, binary_checker: CompositeChecker, ) -> Result<impl Iterator<Item = PathBuf>> where T: AsRef<OsStr>, U: AsRef<OsStr>, V: AsRef<Path>,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")]
find_re<T>( &self, binary_regex: impl Borrow<Regex>, paths: Option<T>, binary_checker: CompositeChecker, ) -> Result<impl Iterator<Item = PathBuf>> where T: AsRef<OsStr>,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
cwd_search_candidates<C>(binary_name: PathBuf, cwd: C) -> impl IntoIterator<Item = PathBuf> where C: AsRef<Path>,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
path_search_candidates<P>( binary_name: PathBuf, paths: P, ) -> impl IntoIterator<Item = PathBuf> where P: IntoIterator<Item = PathBuf>,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)]
append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf> where P: IntoIterator<Item = PathBuf>,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)]
append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf> where P: IntoIterator<Item = PathBuf>,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")]
correct_casing(mut p: PathBuf) -> PathBuf214 fn 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"))]
correct_casing(p: PathBuf) -> PathBuf230 fn correct_casing(p: PathBuf) -> PathBuf {
231 p
232 }
233