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