1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::ffi::OsString;
4use std::io;
5use std::path::Path;
6use std::path::PathBuf;
7
8pub trait SysReadDirEntry {
9 fn file_name(&self) -> OsString;
11 fn path(&self) -> PathBuf;
13}
14
15pub trait SysMetadata {
16 fn is_symlink(&self) -> bool;
18 fn is_file(&self) -> bool;
20}
21
22pub trait Sys {
51 type ReadDirEntry: SysReadDirEntry;
52 type Metadata: SysMetadata;
53
54 fn is_windows(&self) -> bool;
59 fn current_dir(&self) -> io::Result<PathBuf>;
61 fn home_dir(&self) -> Option<PathBuf>;
63 fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf>;
65 fn env_path(&self) -> Option<OsString>;
67 fn env_path_ext(&self) -> Option<OsString>;
69 fn env_windows_path_ext(&self) -> Cow<'static, [String]> {
77 Cow::Owned(parse_path_ext(self.env_path_ext()))
78 }
79 fn metadata(&self, path: &Path) -> io::Result<Self::Metadata>;
81 fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata>;
83 fn read_dir(
85 &self,
86 path: &Path,
87 ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>>;
88 fn is_valid_executable(&self, path: &Path) -> io::Result<bool>;
90}
91
92impl SysReadDirEntry for std::fs::DirEntry {
93 fn file_name(&self) -> OsString {
94 self.file_name()
95 }
96
97 fn path(&self) -> PathBuf {
98 self.path()
99 }
100}
101
102impl SysMetadata for std::fs::Metadata {
103 fn is_symlink(&self) -> bool {
104 self.file_type().is_symlink()
105 }
106
107 fn is_file(&self) -> bool {
108 self.file_type().is_file()
109 }
110}
111
112#[cfg(feature = "real-sys")]
113#[derive(Default, Clone, Copy)]
114pub struct RealSys;
115
116#[cfg(feature = "real-sys")]
117impl RealSys {
118 #[inline]
119 pub(crate) fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
120 #[allow(clippy::disallowed_methods)] std::fs::canonicalize(path)
122 }
123}
124
125#[cfg(feature = "real-sys")]
126impl Sys for RealSys {
127 type ReadDirEntry = std::fs::DirEntry;
128 type Metadata = std::fs::Metadata;
129
130 #[inline]
131 fn is_windows(&self) -> bool {
132 cfg!(windows)
136 }
137
138 #[inline]
139 fn current_dir(&self) -> io::Result<PathBuf> {
140 #[allow(clippy::disallowed_methods)] std::env::current_dir()
142 }
143
144 #[inline]
145 fn home_dir(&self) -> Option<PathBuf> {
146 #[allow(clippy::disallowed_methods)] #[allow(deprecated)] std::env::home_dir()
149 }
150
151 #[inline]
152 fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
153 #[allow(clippy::disallowed_methods)] std::env::split_paths(paths).collect()
155 }
156
157 fn env_windows_path_ext(&self) -> Cow<'static, [String]> {
158 use std::sync::OnceLock;
159
160 static PATH_EXTENSIONS: OnceLock<Vec<String>> = OnceLock::new();
165 let path_extensions = PATH_EXTENSIONS.get_or_init(|| parse_path_ext(self.env_path_ext()));
166 Cow::Borrowed(path_extensions)
167 }
168
169 #[inline]
170 fn env_path(&self) -> Option<OsString> {
171 #[allow(clippy::disallowed_methods)] std::env::var_os("PATH")
173 }
174
175 #[inline]
176 fn env_path_ext(&self) -> Option<OsString> {
177 #[allow(clippy::disallowed_methods)] std::env::var_os("PATHEXT")
179 }
180
181 #[inline]
182 fn read_dir(
183 &self,
184 path: &Path,
185 ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
186 #[allow(clippy::disallowed_methods)] let iter = std::fs::read_dir(path)?;
188 Ok(Box::new(iter))
189 }
190
191 #[inline]
192 fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
193 #[allow(clippy::disallowed_methods)] std::fs::metadata(path)
195 }
196
197 #[inline]
198 fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
199 #[allow(clippy::disallowed_methods)] std::fs::symlink_metadata(path)
201 }
202
203 #[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
204 fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
205 use std::ffi::CString;
206 #[cfg(any(unix, target_os = "redox"))]
207 use std::os::unix::ffi::OsStrExt;
208 #[cfg(target_os = "wasi")]
209 use std::os::wasi::ffi::OsStrExt;
210
211 let path = CString::new(path.as_os_str().as_bytes())?;
212 if unsafe { libc::access(path.as_ptr(), libc::X_OK) } == 0 {
213 Ok(true)
214 } else {
215 Err(io::Error::last_os_error())
216 }
217 }
218
219 #[cfg(windows)]
220 fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
221 use std::os::windows::ffi::OsStrExt;
222
223 use crate::win_ffi;
224 let w_str: Vec<u16> = path
225 .as_os_str()
226 .encode_wide()
227 .chain(std::iter::once(0))
228 .collect();
229 let mut binary_type = 0u32;
230 unsafe {
231 let success = win_ffi::GetBinaryTypeW(w_str.as_ptr(), &mut binary_type);
232 if success != 0 {
233 Ok(true)
234 } else {
235 Err(io::Error::from_raw_os_error(win_ffi::GetLastError() as i32))
236 }
237 }
238 }
239
240 #[cfg(not(any(unix, windows, target_os = "wasi", target_os = "redox")))]
241 fn is_valid_executable(&self, _path: &Path) -> io::Result<bool> {
242 Ok(false)
243 }
244}
245
246impl<T> Sys for &T
247where
248 T: Sys,
249{
250 type ReadDirEntry = T::ReadDirEntry;
251
252 type Metadata = T::Metadata;
253
254 fn is_windows(&self) -> bool {
255 (*self).is_windows()
256 }
257
258 fn current_dir(&self) -> io::Result<PathBuf> {
259 (*self).current_dir()
260 }
261
262 fn home_dir(&self) -> Option<PathBuf> {
263 (*self).home_dir()
264 }
265
266 fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
267 (*self).env_split_paths(paths)
268 }
269
270 fn env_path(&self) -> Option<OsString> {
271 (*self).env_path()
272 }
273
274 fn env_path_ext(&self) -> Option<OsString> {
275 (*self).env_path_ext()
276 }
277
278 fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
279 (*self).metadata(path)
280 }
281
282 fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
283 (*self).symlink_metadata(path)
284 }
285
286 fn read_dir(
287 &self,
288 path: &Path,
289 ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
290 (*self).read_dir(path)
291 }
292
293 fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
294 (*self).is_valid_executable(path)
295 }
296}
297
298fn parse_path_ext(pathext: Option<OsString>) -> Vec<String> {
299 pathext
300 .and_then(|pathext| {
301 #[allow(clippy::manual_ok_err)]
303 match pathext.into_string() {
304 Ok(pathext) => Some(pathext),
305 Err(_) => {
306 #[cfg(feature = "tracing")]
307 tracing::error!("pathext is not valid unicode");
308 None
309 }
310 }
311 })
312 .map(|pathext| {
313 pathext
314 .split(';')
315 .filter_map(|s| {
316 if s.as_bytes().first() == Some(&b'.') {
317 Some(s.to_owned())
318 } else {
319 #[cfg(feature = "tracing")]
321 tracing::debug!("PATHEXT segment \"{s}\" missing leading dot, ignoring");
322 None
323 }
324 })
325 .collect()
326 })
327 .unwrap_or_default()
330}