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::env; 11use std::ffi::OsStr; 12#[cfg(any(feature = "regex", target_os = "windows"))] 13use std::fs; 14use std::iter; 15use std::path::{Path, PathBuf}; 16 17pub trait Checker { 18 fn is_valid(&self, path: &Path) -> bool; 19} 20 21trait PathExt { 22 fn has_separator(&self) -> bool; 23 24 fn to_absolute<P>(self, cwd: P) -> PathBuf 25 where 26 P: AsRef<Path>; 27} 28 29impl PathExt for PathBuf { 30 fn has_separator(&self) -> bool { 31 self.components().count() > 1 32 } 33 34 fn to_absolute<P>(self, cwd: P) -> PathBuf 35 where 36 P: AsRef<Path>, 37 { 38 if self.is_absolute() { 39 self 40 } else { 41 let mut new_path = PathBuf::from(cwd.as_ref()); 42 new_path.push(self); 43 new_path 44 } 45 } 46} 47 48pub struct Finder; 49 50impl Finder { 51 pub fn new() -> Finder { 52 Finder 53 } 54 55 pub fn find<T, U, V>( 56 &self, 57 binary_name: T, 58 paths: Option<U>, 59 cwd: Option<V>, 60 binary_checker: CompositeChecker, 61 ) -> Result<impl Iterator<Item = PathBuf>> 62 where 63 T: AsRef<OsStr>, 64 U: AsRef<OsStr>, 65 V: AsRef<Path>, 66 { 67 let path = PathBuf::from(&binary_name); 68 69 let binary_path_candidates = match cwd { 70 Some(cwd) if path.has_separator() => { 71 // Search binary in cwd if the path have a path separator. 72 Either::Left(Self::cwd_search_candidates(path, cwd).into_iter()) 73 } 74 _ => { 75 // Search binary in PATHs(defined in environment variable). 76 let p = paths.ok_or(Error::CannotFindBinaryPath)?; 77 let paths: Vec<_> = env::split_paths(&p).collect(); 78 79 Either::Right(Self::path_search_candidates(path, paths).into_iter()) 80 } 81 }; 82 83 Ok(binary_path_candidates 84 .filter(move |p| binary_checker.is_valid(p)) 85 .map(correct_casing)) 86 } 87 88 #[cfg(feature = "regex")] 89 pub fn find_re<T>( 90 &self, 91 binary_regex: impl Borrow<Regex>, 92 paths: Option<T>, 93 binary_checker: CompositeChecker, 94 ) -> Result<impl Iterator<Item = PathBuf>> 95 where 96 T: AsRef<OsStr>, 97 { 98 let p = paths.ok_or(Error::CannotFindBinaryPath)?; 99 // Collect needs to happen in order to not have to 100 // change the API to borrow on `paths`. 101 #[allow(clippy::needless_collect)] 102 let paths: Vec<_> = env::split_paths(&p).collect(); 103 104 let matching_re = paths 105 .into_iter() 106 .flat_map(fs::read_dir) 107 .flatten() 108 .flatten() 109 .map(|e| e.path()) 110 .filter(move |p| { 111 if let Some(unicode_file_name) = p.file_name().unwrap().to_str() { 112 binary_regex.borrow().is_match(unicode_file_name) 113 } else { 114 false 115 } 116 }) 117 .filter(move |p| binary_checker.is_valid(p)); 118 119 Ok(matching_re) 120 } 121 122 fn cwd_search_candidates<C>(binary_name: PathBuf, cwd: C) -> impl IntoIterator<Item = PathBuf> 123 where 124 C: AsRef<Path>, 125 { 126 let path = binary_name.to_absolute(cwd); 127 128 Self::append_extension(iter::once(path)) 129 } 130 131 fn path_search_candidates<P>( 132 binary_name: PathBuf, 133 paths: P, 134 ) -> impl IntoIterator<Item = PathBuf> 135 where 136 P: IntoIterator<Item = PathBuf>, 137 { 138 let new_paths = paths.into_iter().map(move |p| p.join(binary_name.clone())); 139 140 Self::append_extension(new_paths) 141 } 142 143 #[cfg(unix)] 144 fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf> 145 where 146 P: IntoIterator<Item = PathBuf>, 147 { 148 paths 149 } 150 151 #[cfg(windows)] 152 fn append_extension<P>(paths: P) -> impl IntoIterator<Item = PathBuf> 153 where 154 P: IntoIterator<Item = PathBuf>, 155 { 156 use once_cell::sync::Lazy; 157 158 // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC 159 // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …]. 160 // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it; 161 // hence its retention.) 162 static PATH_EXTENSIONS: Lazy<Vec<String>> = Lazy::new(|| { 163 env::var("PATHEXT") 164 .map(|pathext| { 165 pathext 166 .split(';') 167 .filter_map(|s| { 168 if s.as_bytes().first() == Some(&b'.') { 169 Some(s.to_owned()) 170 } else { 171 // Invalid segment; just ignore it. 172 None 173 } 174 }) 175 .collect() 176 }) 177 // PATHEXT not being set or not being a proper Unicode string is exceedingly 178 // improbable and would probably break Windows badly. Still, don't crash: 179 .unwrap_or_default() 180 }); 181 182 paths 183 .into_iter() 184 .flat_map(move |p| -> Box<dyn Iterator<Item = _>> { 185 // Check if path already have executable extension 186 if has_executable_extension(&p, &PATH_EXTENSIONS) { 187 Box::new(iter::once(p)) 188 } else { 189 let bare_file = p.extension().map(|_| p.clone()); 190 // Appended paths with windows executable extensions. 191 // e.g. path `c:/windows/bin[.ext]` will expand to: 192 // [c:/windows/bin.ext] 193 // c:/windows/bin[.ext].COM 194 // c:/windows/bin[.ext].EXE 195 // c:/windows/bin[.ext].CMD 196 // ... 197 Box::new( 198 bare_file 199 .into_iter() 200 .chain(PATH_EXTENSIONS.iter().map(move |e| { 201 // Append the extension. 202 let mut p = p.clone().into_os_string(); 203 p.push(e); 204 205 PathBuf::from(p) 206 })), 207 ) 208 } 209 }) 210 } 211} 212 213#[cfg(target_os = "windows")] 214fn correct_casing(mut p: PathBuf) -> PathBuf { 215 if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) { 216 if let Ok(iter) = fs::read_dir(parent) { 217 for e in iter.filter_map(std::result::Result::ok) { 218 if e.file_name().eq_ignore_ascii_case(file_name) { 219 p.pop(); 220 p.push(e.file_name()); 221 break; 222 } 223 } 224 } 225 } 226 p 227} 228 229#[cfg(not(target_os = "windows"))] 230fn correct_casing(p: PathBuf) -> PathBuf { 231 p 232} 233