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::borrow::Cow;
11use std::env;
12use std::ffi::OsStr;
13#[cfg(any(feature = "regex", target_os = "windows"))]
14use std::fs;
15use std::iter;
16use 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"))]
20use home::home_dir;
21
22#[cfg(not(any(windows, unix, target_os = "redox")))]
23fn home_dir() -> Option<std::path::PathBuf> {
24 None
25}
26
27pub trait Checker {
28 fn is_valid(&self, path: &Path) -> bool;
29}
30
31trait 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
39impl 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
58pub struct Finder;
59
60impl 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
226fn 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")]
242fn 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"))]
258fn correct_casing(p: PathBuf) -> PathBuf {
259 p
260}
261