1use crate::checker::is_valid;
2use crate::helper::has_executable_extension;
3use crate::sys::Sys;
4use crate::sys::SysReadDirEntry;
5use crate::{error::*, NonFatalErrorHandler};
6#[cfg(feature = "regex")]
7use regex::Regex;
8#[cfg(feature = "regex")]
9use std::borrow::Borrow;
10use std::borrow::Cow;
11use std::ffi::OsStr;
12#[cfg(feature = "regex")]
13use std::io;
14use std::path::{Component, Path, PathBuf};
15use std::vec;
16
17trait PathExt {
18 fn has_separator(&self) -> bool;
19
20 fn to_absolute<P>(self, cwd: P) -> PathBuf
21 where
22 P: AsRef<Path>;
23}
24
25impl PathExt for PathBuf {
26 fn has_separator(&self) -> bool {
27 self.components().count() > 1
28 }
29
30 fn to_absolute<P>(self, cwd: P) -> PathBuf
31 where
32 P: AsRef<Path>,
33 {
34 if self.is_absolute() {
35 self
36 } else {
37 let mut new_path = PathBuf::from(cwd.as_ref());
38 new_path.extend(
39 self.components()
40 .skip_while(|c| matches!(c, Component::CurDir)),
41 );
42 new_path
43 }
44 }
45}
46
47pub struct Finder<TSys: Sys> {
48 sys: TSys,
49}
50
51impl<TSys: Sys> Finder<TSys> {
52 pub fn new(sys: TSys) -> Self {
53 Finder { sys }
54 }
55
56 pub fn find<'a, T, U, V, F: NonFatalErrorHandler + 'a>(
57 self,
58 binary_name: T,
59 paths: Option<U>,
60 cwd: Option<V>,
61 nonfatal_error_handler: F,
62 ) -> Result<impl Iterator<Item = PathBuf> + 'a>
63 where
64 TSys: 'a,
65 T: AsRef<OsStr>,
66 U: AsRef<OsStr>,
67 V: AsRef<Path> + 'a,
68 {
69 let path = PathBuf::from(&binary_name);
70
71 #[cfg(feature = "tracing")]
72 tracing::debug!(
73 "query binary_name = {:?}, paths = {:?}, cwd = {:?}",
74 binary_name.as_ref().to_string_lossy(),
75 paths.as_ref().map(|p| p.as_ref().to_string_lossy()),
76 cwd.as_ref().map(|p| p.as_ref().display())
77 );
78
79 let ret = match cwd {
80 Some(cwd) if path.has_separator() => {
81 WhichFindIterator::new_cwd(path, cwd.as_ref(), self.sys, nonfatal_error_handler)
82 }
83 _ => {
84 #[cfg(feature = "tracing")]
85 tracing::trace!("{} has no path seperators, so only paths in PATH environment variable will be searched.", path.display());
86 let paths = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
88 let paths = self.sys.env_split_paths(paths.as_ref());
89 if paths.is_empty() {
90 return Err(Error::CannotGetCurrentDirAndPathListEmpty);
91 }
92 WhichFindIterator::new_paths(path, paths, self.sys, nonfatal_error_handler)
93 }
94 };
95 #[cfg(feature = "tracing")]
96 let ret = ret.inspect(|p| {
97 tracing::debug!("found path {}", p.display());
98 });
99 Ok(ret)
100 }
101
102 #[cfg(feature = "regex")]
103 pub fn find_re<T, F: NonFatalErrorHandler>(
104 self,
105 binary_regex: impl std::borrow::Borrow<Regex>,
106 paths: Option<T>,
107 nonfatal_error_handler: F,
108 ) -> Result<impl Iterator<Item = PathBuf>>
109 where
110 T: AsRef<OsStr>,
111 {
112 WhichFindRegexIter::new(self.sys, paths, binary_regex, nonfatal_error_handler)
113 }
114}
115
116struct WhichFindIterator<TSys: Sys, F: NonFatalErrorHandler> {
117 sys: TSys,
118 paths: PathsIter<vec::IntoIter<PathBuf>>,
119 nonfatal_error_handler: F,
120}
121
122impl<TSys: Sys, F: NonFatalErrorHandler> WhichFindIterator<TSys, F> {
123 pub fn new_cwd(binary_name: PathBuf, cwd: &Path, sys: TSys, nonfatal_error_handler: F) -> Self {
124 let path_extensions = if sys.is_windows() {
125 sys.env_windows_path_ext()
126 } else {
127 Cow::Borrowed(Default::default())
128 };
129 Self {
130 sys,
131 paths: PathsIter {
132 paths: vec![binary_name.to_absolute(cwd)].into_iter(),
133 current_path_with_index: None,
134 path_extensions,
135 },
136 nonfatal_error_handler,
137 }
138 }
139
140 pub fn new_paths(
141 binary_name: PathBuf,
142 paths: Vec<PathBuf>,
143 sys: TSys,
144 nonfatal_error_handler: F,
145 ) -> Self {
146 let path_extensions = if sys.is_windows() {
147 sys.env_windows_path_ext()
148 } else {
149 Cow::Borrowed(Default::default())
150 };
151 let paths = paths
152 .iter()
153 .map(|p| tilde_expansion(&sys, p).join(&binary_name))
154 .collect::<Vec<_>>();
155 Self {
156 sys,
157 paths: PathsIter {
158 paths: paths.into_iter(),
159 current_path_with_index: None,
160 path_extensions,
161 },
162 nonfatal_error_handler,
163 }
164 }
165}
166
167impl<TSys: Sys, F: NonFatalErrorHandler> Iterator for WhichFindIterator<TSys, F> {
168 type Item = PathBuf;
169
170 fn next(&mut self) -> Option<Self::Item> {
171 for path in &mut self.paths {
172 if is_valid(&self.sys, &path, &mut self.nonfatal_error_handler) {
173 return Some(correct_casing(
174 &self.sys,
175 path,
176 &mut self.nonfatal_error_handler,
177 ));
178 }
179 }
180 None
181 }
182}
183
184struct PathsIter<P>
185where
186 P: Iterator<Item = PathBuf>,
187{
188 paths: P,
189 current_path_with_index: Option<(PathBuf, usize)>,
190 path_extensions: Cow<'static, [String]>,
191}
192
193impl<P> Iterator for PathsIter<P>
194where
195 P: Iterator<Item = PathBuf>,
196{
197 type Item = PathBuf;
198
199 fn next(&mut self) -> Option<Self::Item> {
200 if self.path_extensions.is_empty() {
201 self.paths.next()
202 } else if let Some((p, index)) = self.current_path_with_index.take() {
203 let next_index = index + 1;
204 if next_index < self.path_extensions.len() {
205 self.current_path_with_index = Some((p.clone(), next_index));
206 }
207 let mut p = p.into_os_string();
209 p.push(&self.path_extensions[index]);
210 let ret = PathBuf::from(p);
211 #[cfg(feature = "tracing")]
212 tracing::trace!("possible extension: {}", ret.display());
213 Some(ret)
214 } else {
215 let p = self.paths.next()?;
216 if has_executable_extension(&p, &self.path_extensions) {
217 #[cfg(feature = "tracing")]
218 tracing::trace!(
219 "{} already has an executable extension, not modifying it further",
220 p.display()
221 );
222 } else {
223 #[cfg(feature = "tracing")]
224 tracing::trace!(
225 "{} has no extension, using PATHEXT environment variable to infer one",
226 p.display()
227 );
228 self.current_path_with_index = Some((p.clone(), 0));
236 }
237 Some(p)
238 }
239 }
240}
241
242fn tilde_expansion<TSys: Sys>(sys: TSys, p: &Path) -> Cow<'_, Path> {
243 let mut component_iter = p.components();
244 if let Some(Component::Normal(o)) = component_iter.next() {
245 if o == "~" {
246 let new_path = sys.home_dir();
247 if let Some(mut new_path) = new_path {
248 new_path.extend(component_iter);
249 #[cfg(feature = "tracing")]
250 tracing::trace!(
251 "found tilde, substituting in user's home directory to get {}",
252 new_path.display()
253 );
254 return Cow::Owned(new_path);
255 } else {
256 #[cfg(feature = "tracing")]
257 tracing::trace!("found tilde in path, but user's home directory couldn't be found");
258 }
259 }
260 }
261 Cow::Borrowed(p)
262}
263
264fn correct_casing<TSys: Sys, F: NonFatalErrorHandler>(
265 sys: TSys,
266 mut p: PathBuf,
267 nonfatal_error_handler: &mut F,
268) -> PathBuf {
269 if sys.is_windows() {
270 if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) {
271 if let Ok(iter) = sys.read_dir(parent) {
272 for e in iter {
273 match e {
274 Ok(e) => {
275 if e.file_name().eq_ignore_ascii_case(file_name) {
276 p.pop();
277 p.push(e.file_name());
278 break;
279 }
280 }
281 Err(e) => {
282 nonfatal_error_handler.handle(NonFatalError::Io(e));
283 }
284 }
285 }
286 }
287 }
288 }
289 p
290}
291
292#[cfg(feature = "regex")]
293struct WhichFindRegexIter<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> {
294 sys: TSys,
295 re: B,
296 paths: vec::IntoIter<PathBuf>,
297 nonfatal_error_handler: F,
298 current_read_dir_iter: Option<Box<dyn Iterator<Item = io::Result<TSys::ReadDirEntry>>>>,
299}
300
301#[cfg(feature = "regex")]
302impl<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> WhichFindRegexIter<TSys, B, F> {
303 pub fn new<T: AsRef<OsStr>>(
304 sys: TSys,
305 paths: Option<T>,
306 re: B,
307 nonfatal_error_handler: F,
308 ) -> Result<Self> {
309 let p = paths.ok_or(Error::CannotGetCurrentDirAndPathListEmpty)?;
310 let paths = sys.env_split_paths(p.as_ref());
311 Ok(WhichFindRegexIter {
312 sys,
313 re,
314 paths: paths.into_iter(),
315 nonfatal_error_handler,
316 current_read_dir_iter: None,
317 })
318 }
319}
320
321#[cfg(feature = "regex")]
322impl<TSys: Sys, B: Borrow<Regex>, F: NonFatalErrorHandler> Iterator
323 for WhichFindRegexIter<TSys, B, F>
324{
325 type Item = PathBuf;
326
327 fn next(&mut self) -> Option<Self::Item> {
328 loop {
329 if let Some(iter) = &mut self.current_read_dir_iter {
330 match iter.next() {
331 Some(Ok(path)) => {
332 if let Some(unicode_file_name) = path.file_name().to_str() {
333 if self.re.borrow().is_match(unicode_file_name) {
334 return Some(path.path());
335 } else {
336 #[cfg(feature = "tracing")]
337 tracing::debug!("regex filtered out {}", unicode_file_name);
338 }
339 } else {
340 #[cfg(feature = "tracing")]
341 tracing::debug!("regex unable to evaluate filename as it's not valid unicode. Lossy filename conversion: {}", path.file_name().to_string_lossy());
342 }
343 }
344 Some(Err(e)) => {
345 self.nonfatal_error_handler.handle(NonFatalError::Io(e));
346 }
347 None => {
348 self.current_read_dir_iter = None;
349 }
350 }
351 } else {
352 let path = self.paths.next();
353 if let Some(path) = path {
354 match self.sys.read_dir(&path) {
355 Ok(new_read_dir_iter) => {
356 self.current_read_dir_iter = Some(new_read_dir_iter);
357 }
358 Err(e) => {
359 self.nonfatal_error_handler.handle(NonFatalError::Io(e));
360 }
361 }
362 } else {
363 return None;
364 }
365 }
366 }
367 }
368}