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 #[cfg(any(windows, unix, target_os = "redox"))]
148 {
149 env_home::env_home_dir()
150 }
151 #[cfg(not(any(windows, unix, target_os = "redox")))]
152 {
153 None
154 }
155 }
156
157 #[inline]
158 fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
159 #[allow(clippy::disallowed_methods)] std::env::split_paths(paths).collect()
161 }
162
163 fn env_windows_path_ext(&self) -> Cow<'static, [String]> {
164 use std::sync::OnceLock;
165
166 static PATH_EXTENSIONS: OnceLock<Vec<String>> = OnceLock::new();
171 let path_extensions = PATH_EXTENSIONS.get_or_init(|| parse_path_ext(self.env_path_ext()));
172 Cow::Borrowed(path_extensions)
173 }
174
175 #[inline]
176 fn env_path(&self) -> Option<OsString> {
177 #[allow(clippy::disallowed_methods)] std::env::var_os("PATH")
179 }
180
181 #[inline]
182 fn env_path_ext(&self) -> Option<OsString> {
183 #[allow(clippy::disallowed_methods)] std::env::var_os("PATHEXT")
185 }
186
187 #[inline]
188 fn read_dir(
189 &self,
190 path: &Path,
191 ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
192 #[allow(clippy::disallowed_methods)] let iter = std::fs::read_dir(path)?;
194 Ok(Box::new(iter))
195 }
196
197 #[inline]
198 fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
199 #[allow(clippy::disallowed_methods)] std::fs::metadata(path)
201 }
202
203 #[inline]
204 fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
205 #[allow(clippy::disallowed_methods)] std::fs::symlink_metadata(path)
207 }
208
209 #[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
210 fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
211 use rustix::fs as rfs;
212 rfs::access(path, rfs::Access::EXEC_OK)
213 .map(|_| true)
214 .map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
215 }
216
217 #[cfg(windows)]
218 fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
219 winsafe::GetBinaryType(&path.display().to_string())
220 .map(|_| true)
221 .map_err(|e| io::Error::from_raw_os_error(e.raw() as i32))
222 }
223}
224
225impl<T> Sys for &T
226where
227 T: Sys,
228{
229 type ReadDirEntry = T::ReadDirEntry;
230
231 type Metadata = T::Metadata;
232
233 fn is_windows(&self) -> bool {
234 (*self).is_windows()
235 }
236
237 fn current_dir(&self) -> io::Result<PathBuf> {
238 (*self).current_dir()
239 }
240
241 fn home_dir(&self) -> Option<PathBuf> {
242 (*self).home_dir()
243 }
244
245 fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
246 (*self).env_split_paths(paths)
247 }
248
249 fn env_path(&self) -> Option<OsString> {
250 (*self).env_path()
251 }
252
253 fn env_path_ext(&self) -> Option<OsString> {
254 (*self).env_path_ext()
255 }
256
257 fn metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
258 (*self).metadata(path)
259 }
260
261 fn symlink_metadata(&self, path: &Path) -> io::Result<Self::Metadata> {
262 (*self).symlink_metadata(path)
263 }
264
265 fn read_dir(
266 &self,
267 path: &Path,
268 ) -> io::Result<Box<dyn Iterator<Item = io::Result<Self::ReadDirEntry>>>> {
269 (*self).read_dir(path)
270 }
271
272 fn is_valid_executable(&self, path: &Path) -> io::Result<bool> {
273 (*self).is_valid_executable(path)
274 }
275}
276
277fn parse_path_ext(pathext: Option<OsString>) -> Vec<String> {
278 pathext
279 .and_then(|pathext| {
280 #[allow(clippy::manual_ok_err)]
282 match pathext.into_string() {
283 Ok(pathext) => Some(pathext),
284 Err(_) => {
285 #[cfg(feature = "tracing")]
286 tracing::error!("pathext is not valid unicode");
287 None
288 }
289 }
290 })
291 .map(|pathext| {
292 pathext
293 .split(';')
294 .filter_map(|s| {
295 if s.as_bytes().first() == Some(&b'.') {
296 Some(s.to_owned())
297 } else {
298 #[cfg(feature = "tracing")]
300 tracing::debug!("PATHEXT segment \"{s}\" missing leading dot, ignoring");
301 None
302 }
303 })
304 .collect()
305 })
306 .unwrap_or_default()
309}