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 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 |
|
226 | fn tilde_expansion(p: &PathBuf) -> Cow<'_, PathBuf> {
|
227 | let mut component_iter: Components<'_> = p.components();
|
228 | if let Some(Component::Normal(o: &OsStr)) = component_iter.next() {
|
229 | if o == "~" {
|
230 | let mut new_path: PathBuf = 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" )]
|
242 | fn 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" ))]
|
258 | fn correct_casing(p: PathBuf) -> PathBuf {
|
259 | p
|
260 | }
|
261 | |