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
241impl<T> Sys for &T
242where
243 T: Sys,
244{
245 type ReadDirEntry = T::ReadDirEntry;
246
247 type Metadata = T::Metadata;
248
249 fn is_windows(&self) -> bool {
250 (*self).is_windows()
251 }
252
253 fn current_dir(&self) -> io::Result<PathBuf> {
254 (*self).current_dir()
255 }
256
257 fn home_dir(&self) -> Option<PathBuf> {
258 (*self).home_dir()
259 }
260
261 fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
262 (*self).env_split_paths(paths)
263 }
264
265 fn env_path(&self) -> Option<OsString> {
266 (*self).env_path()
267 }
268
269 fn env_path_ext(&self) -> Option<OsString> {
270 (*self).env_path_ext()
271 }
272
273 fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
274 (*self).metadata(path)
275 }
276
277 fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
278 (*self).symlink_metadata(path)
279 }
280
281 fn read_dir(
282 &self,
283 path: &Path,
284 ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
285 (*self).read_dir(path)
286 }
287
288 fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
289 (*self).is_valid_executable(path)
290 }
291}
292
293fn parse_path_ext(pathext: Option<OsString>) -> Vec<String> {
294 pathext
295 .and_then(|pathext| {
296 #[allow(clippy::manual_ok_err)]
298 match pathext.into_string() {
299 Ok(pathext) => Some(pathext),
300 Err(_) => {
301 #[cfg(feature = "tracing")]
302 tracing::error!("pathext is not valid unicode");
303 None
304 }
305 }
306 })
307 .map(|pathext| {
308 pathext
309 .split(';')
310 .filter_map(|s| {
311 if s.as_bytes().first() == Some(&b'.') {
312 Some(s.to_owned())
313 } else {
314 #[cfg(feature = "tracing")]
316 tracing::debug!("PATHEXT segment \"{s}\" missing leading dot, ignoring");
317 None
318 }
319 })
320 .collect()
321 })
322 .unwrap_or_default()
325}