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