1use crate::checker::CompositeChecker;
2#[cfg(windows)]
3use crate::helper::has_executable_extension;
4use crate::{error::*, NonFatalErrorHandler};
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#[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<F: NonFatalErrorHandler>(
29 &self,
30 path: &Path,
31 nonfatal_error_handler: &mut F,
32 ) -> bool;
33}
34
35trait PathExt {
36 fn has_separator(&self) -> bool;
37
38 fn to_absolute<P>(self, cwd: P) -> PathBuf
39 where
40 P: AsRef<Path>;
41}
42
43impl PathExt for PathBuf {
44 fn has_separator(&self) -> bool {
45 self.components().count() > 1
46 }
47
48 fn to_absolute<P>(self, cwd: P) -> PathBuf
49 where
50 P: AsRef<Path>,
51 {
52 if self.is_absolute() {
53 self
54 } else {
55 let mut new_path = PathBuf::from(cwd.as_ref());
56 new_path.push(self);
57 new_path
58 }
59 }
60}
61
62pub struct Finder;
63
64impl Finder {
65 pub fn new() -> Finder {
66 Finder
67 }
68
69 pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>(
70 &self,
71 binary_name: T,
72 paths: Option<U>,
73 cwd: Option<V>,
74 binary_checker: CompositeChecker,
75 mut nonfatal_error_handler: F,
76 ) -> Result<impl Iterator<Item = PathBuf> + 'a>
77 where
78 T: AsRef<OsStr>,
79 U: AsRef<OsStr>,
80 V: AsRef<Path> + 'a,
81 {
82 let path = PathBuf::from(&binary_name);
83
84 #[cfg(feature = "tracing")]
85 tracing::debug!(
86 "query binary_name = {:?}, paths = {:?}, cwd = {:?}",
87 binary_name.as_ref().to_string_lossy(),
88 paths.as_ref().map(|p| p.as_ref().to_string_lossy()),
89 cwd.as_ref().map(|p| p.as_ref().display())
90 );
91
92 let binary_path_candidates = match cwd {
93 Some(cwd) if path.has_separator() => {
94 #[cfg(feature = "tracing")]
95 tracing::trace!(
96 "{} has a path seperator, so only CWD will be searched.",
97 path.display()
98 );
99 Either::Left(Self::cwd_search_candidates(path, cwd))
101 }
102 _ => {
103 #[cfg(feature = "tracing")]
104 tracing::trace!("{} has no path seperators, so only paths in PATH environment variable will be searched.", path.display());
105 let paths = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
107 let paths = env::split_paths(&paths).collect::<Vec<_>>();
108 if paths.is_empty() {
109 return Err(Error::CannotGetCurrentDirAndPathListEmpty);
110 }
111
112 Either::Right(Self::path_search_candidates(path, paths))
113 }
114 };
115 let ret = binary_path_candidates.into_iter().filter_map(move |p| {
116 binary_checker
117 .is_valid(&p, &mut nonfatal_error_handler)
118 .then(|| correct_casing(p, &mut nonfatal_error_handler))
119 });
120 #[cfg(feature = "tracing")]
121 let ret = ret.inspect(|p| {
122 tracing::debug!("found path {}", p.display());
123 });
124 Ok(ret)
125 }
126
127 #[cfg(feature = "regex")]
128 pub fn find_re<T, F: NonFatalErrorHandler>(
129 &self,
130 binary_regex: impl Borrow<Regex>,
131 paths: Option<T>,
132 binary_checker: CompositeChecker,
133 mut nonfatal_error_handler: F,
134 ) -> Result<impl Iterator<Item = PathBuf>>
135 where
136 T: AsRef<OsStr>,
137 {
138 let p = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
139 #[allow(clippy::needless_collect)]
142 let paths: Vec<_> = env::split_paths(&p).collect();
143
144 let matching_re = paths
145 .into_iter()
146 .flat_map(fs::read_dir)
147 .flatten()
148 .flatten()
149 .map(|e| e.path())
150 .filter(move |p| {
151 if let Some(unicode_file_name) = p.file_name().unwrap().to_str() {
152 binary_regex.borrow().is_match(unicode_file_name)
153 } else {
154 false
155 }
156 })
157 .filter(move |p| binary_checker.is_valid(p, &mut nonfatal_error_handler));
158
159 Ok(matching_re)
160 }
161
162 fn cwd_search_candidates<C>(binary_name: PathBuf, cwd: C) -> impl IntoIterator<Item = PathBuf>
163 where
164 C: AsRef<Path>,
165 {
166 let path = binary_name.to_absolute(cwd);
167
168 Self::append_extension(iter::once(path))
169 }
170
171 fn path_search_candidates<P>(
172 binary_name: PathBuf,
173 paths: P,
174 ) -> impl IntoIterator<Item = PathBuf>
175 where
176 P: IntoIterator<Item = PathBuf>,
177 {
178 let new_paths = paths
179 .into_iter()
180 .map(move |p| tilde_expansion(&p).join(binary_name.clone()));
181
182 Self::append_extension(new_paths)
183 }
184
185 #[cfg(not(windows))]
186 fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf>
187 where
188 P: IntoIterator<Item = PathBuf>,
189 {
190 paths
191 }
192
193 #[cfg(windows)]
194 fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf>
195 where
196 P: IntoIterator<Item = PathBuf>,
197 {
198 use std::sync::OnceLock;
199
200 static PATH_EXTENSIONS: OnceLock<Vec<String>> = OnceLock::new();
205
206 paths
207 .into_iter()
208 .flat_map(move |p| -> Box<dyn Iterator<Item = _>> {
209 let path_extensions = PATH_EXTENSIONS.get_or_init(|| {
210 env::var("PATHEXT")
211 .map(|pathext| {
212 pathext
213 .split(';')
214 .filter_map(|s| {
215 if s.as_bytes().first() == Some(&b'.') {
216 Some(s.to_owned())
217 } else {
218 None
220 }
221 })
222 .collect()
223 })
224 .unwrap_or_default()
227 });
228 if has_executable_extension(&p, path_extensions) {
230 #[cfg(feature = "tracing")]
231 tracing::trace!(
232 "{} already has an executable extension, not modifying it further",
233 p.display()
234 );
235 Box::new(iter::once(p))
236 } else {
237 #[cfg(feature = "tracing")]
238 tracing::trace!(
239 "{} has no extension, using PATHEXT environment variable to infer one",
240 p.display()
241 );
242 Box::new(
250 iter::once(p.clone()).chain(path_extensions.iter().map(move |e| {
251 let mut p = p.clone().into_os_string();
253 p.push(e);
254 let ret = PathBuf::from(p);
255 #[cfg(feature = "tracing")]
256 tracing::trace!("possible extension: {}", ret.display());
257 ret
258 })),
259 )
260 }
261 })
262 }
263}
264
265fn tilde_expansion(p: &PathBuf) -> Cow<'_, PathBuf> {
266 let mut component_iter = p.components();
267 if let Some(Component::Normal(o)) = component_iter.next() {
268 if o == "~" {
269 let mut new_path = home_dir().unwrap_or_default();
270 new_path.extend(component_iter);
271 #[cfg(feature = "tracing")]
272 tracing::trace!(
273 "found tilde, substituting in user's home directory to get {}",
274 new_path.display()
275 );
276 Cow::Owned(new_path)
277 } else {
278 Cow::Borrowed(p)
279 }
280 } else {
281 Cow::Borrowed(p)
282 }
283}
284
285#[cfg(target_os = "windows")]
286fn correct_casing<F: NonFatalErrorHandler>(
287 mut p: PathBuf,
288 nonfatal_error_handler: &mut F,
289) -> PathBuf {
290 if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) {
291 if let Ok(iter) = fs::read_dir(parent) {
292 for e in iter {
293 match e {
294 Ok(e) => {
295 if e.file_name().eq_ignore_ascii_case(file_name) {
296 p.pop();
297 p.push(e.file_name());
298 break;
299 }
300 }
301 Err(e) => {
302 nonfatal_error_handler.handle(NonFatalError::Io(e));
303 }
304 }
305 }
306 }
307 }
308 p
309}
310
311#[cfg(not(target_os = "windows"))]
312fn correct_casing<F: NonFatalErrorHandler>(p: PathBuf, _nonfatal_error_handler: &mut F) -> PathBuf {
313 p
314}