shrub_core/sys/unix/
fs.rs1use std::os::unix::ffi::OsStringExt;
4use std::os::unix::fs::FileTypeExt;
5pub use std::os::unix::fs::MetadataExt;
6use std::path::{Path, PathBuf};
7
8use crate::error;
9
10#[cfg(target_os = "android")]
11const ANDROID_DEFPATH: &str = "/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/apex/com.android.virt/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin";
13
14impl crate::sys::fs::PathExt for Path {
15 fn readable(&self) -> bool {
16 nix::unistd::access(self, nix::unistd::AccessFlags::R_OK).is_ok()
17 }
18
19 fn writable(&self) -> bool {
20 nix::unistd::access(self, nix::unistd::AccessFlags::W_OK).is_ok()
21 }
22
23 fn executable(&self) -> bool {
24 nix::unistd::access(self, nix::unistd::AccessFlags::X_OK).is_ok()
25 }
26
27 fn exists_and_is_block_device(&self) -> bool {
28 try_get_file_type(self).is_some_and(|ft| ft.is_block_device())
29 }
30
31 fn exists_and_is_char_device(&self) -> bool {
32 try_get_file_type(self).is_some_and(|ft| ft.is_char_device())
33 }
34
35 fn exists_and_is_fifo(&self) -> bool {
36 try_get_file_type(self).is_some_and(|ft: std::fs::FileType| ft.is_fifo())
37 }
38
39 fn exists_and_is_socket(&self) -> bool {
40 try_get_file_type(self).is_some_and(|ft| ft.is_socket())
41 }
42
43 fn exists_and_is_setgid(&self) -> bool {
44 const S_ISGID: u32 = 0o2000;
45 let file_mode = try_get_file_mode(self);
46 file_mode.is_some_and(|mode| mode & S_ISGID != 0)
47 }
48
49 fn exists_and_is_setuid(&self) -> bool {
50 const S_ISUID: u32 = 0o4000;
51 let file_mode = try_get_file_mode(self);
52 file_mode.is_some_and(|mode| mode & S_ISUID != 0)
53 }
54
55 fn exists_and_is_sticky_bit(&self) -> bool {
56 const S_ISVTX: u32 = 0o1000;
57 let file_mode = try_get_file_mode(self);
58 file_mode.is_some_and(|mode| mode & S_ISVTX != 0)
59 }
60
61 fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
62 let metadata = self.metadata()?;
63 Ok((metadata.dev(), metadata.ino()))
64 }
65}
66
67fn try_get_file_type(path: &Path) -> Option<std::fs::FileType> {
68 path.metadata().map(|metadata| metadata.file_type()).ok()
69}
70
71fn try_get_file_mode(path: &Path) -> Option<u32> {
72 path.metadata().map(|metadata| metadata.mode()).ok()
73}
74
75pub fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(s: &T) -> std::env::SplitPaths<'_> {
79 std::env::split_paths(s)
80}
81
82pub(crate) fn get_default_executable_search_paths() -> Vec<PathBuf> {
83 #[cfg(target_os = "android")]
84 {
85 std::env::split_paths(ANDROID_DEFPATH).collect()
86 }
87 #[cfg(not(target_os = "android"))]
88 {
89 vec![
91 "/usr/local/sbin".into(),
92 "/usr/local/bin".into(),
93 "/usr/sbin".into(),
94 "/usr/bin".into(),
95 "/sbin".into(),
96 "/bin".into(),
97 ]
98 }
99}
100
101pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
104 if let Ok(Some(cs_path)) = confstr_cs_path()
111 && !cs_path.as_os_str().is_empty()
112 {
113 return split_paths(&cs_path).collect();
114 }
115
116 #[cfg(target_os = "android")]
117 {
118 std::env::split_paths(ANDROID_DEFPATH).collect()
119 }
120 #[cfg(not(target_os = "android"))]
121 {
122 vec![
124 "/bin".into(),
125 "/usr/bin".into(),
126 "/sbin".into(),
127 "/usr/sbin".into(),
128 "/etc".into(),
129 "/usr/etc".into(),
130 ]
131 }
132}
133
134#[allow(clippy::unnecessary_wraps)]
135fn confstr_cs_path() -> Result<Option<PathBuf>, std::io::Error> {
136 #[cfg(target_os = "android")]
137 {
138 Ok(Some(PathBuf::from(ANDROID_DEFPATH)))
139 }
140 #[cfg(not(target_os = "android"))]
141 {
142 let value = confstr(nix::libc::_CS_PATH)?;
143
144 if let Some(value) = value {
145 let value_str = PathBuf::from(value);
146 Ok(Some(value_str))
147 } else {
148 Ok(None)
149 }
150 }
151}
152
153#[cfg(not(target_os = "android"))]
161fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
162 let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
167
168 if required_size == 0 {
173 return Ok(None);
174 }
175
176 let mut buffer = Vec::<u8>::with_capacity(required_size);
177
178 let final_size =
185 unsafe { nix::libc::confstr(name, buffer.as_mut_ptr().cast(), buffer.capacity()) };
186
187 if final_size == 0 {
188 return Err(std::io::Error::last_os_error());
189 }
190
191 if final_size > buffer.capacity() {
196 return Err(std::io::Error::other(
197 "confstr needed more space than advertised",
198 ));
199 }
200
201 unsafe { buffer.set_len(final_size) };
208
209 if !matches!(buffer.pop(), Some(0)) {
211 return Err(std::io::Error::other(
212 "confstr did not null-terminate the returned string",
213 ));
214 }
215
216 Ok(Some(std::ffi::OsString::from_vec(buffer)))
217}
218
219pub fn open_null_file() -> Result<std::fs::File, error::Error> {
226 let f = std::fs::File::options()
227 .read(true)
228 .write(true)
229 .open("/dev/null")?;
230
231 Ok(f)
232}
233
234pub const fn try_open_special_file(_path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
241 None
242}
243
244pub fn get_system_profile_path() -> Option<&'static Path> {
246 Some(Path::new("/etc/profile"))
247}
248
249pub fn get_system_rc_path() -> Option<&'static Path> {
251 Some(Path::new("/etc/bash.bashrc"))
252}
253
254pub fn contains_path_separator(s: &str) -> bool {
258 s.contains('/')
259}
260
261pub fn ends_with_path_separator(s: &str) -> bool {
265 s.ends_with('/')
266}
267
268pub fn strip_path_separator_suffix(s: &str) -> &str {
272 s.strip_suffix('/').unwrap_or(s)
273}
274
275pub const fn default_case_insensitive_path_expansion() -> bool {
279 false
280}
281
282pub fn rfind_path_separator(s: &str) -> Option<usize> {
286 s.rfind('/')
287}
288
289pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
294 s.split('/')
295}
296
297pub fn pattern_path_root(first_component: &str) -> Option<PathBuf> {
303 if first_component.is_empty() {
304 Some(PathBuf::from("/"))
305 } else {
306 None
307 }
308}
309
310pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
314 path.push(component);
315}
316
317pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
321 std::borrow::Cow::Borrowed(s)
322}
323
324pub fn resolve_executable(path: PathBuf) -> Option<PathBuf> {
333 use crate::sys::fs::PathExt;
334 if path.as_path().executable() {
335 Some(path)
336 } else {
337 None
338 }
339}
340
341pub(crate) const fn resolve_executable_path(path: PathBuf, _working_dir: &Path) -> PathBuf {
345 path
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn path_separator_helpers() {
354 assert!(contains_path_separator("foo/bar"));
355 assert!(!contains_path_separator("foobar"));
356 assert!(!contains_path_separator(r"foo\bar"));
358
359 assert!(ends_with_path_separator("foo/"));
360 assert!(!ends_with_path_separator("foo"));
361 assert!(!ends_with_path_separator(r"foo\"));
362
363 assert_eq!(strip_path_separator_suffix("foo/"), "foo");
364 assert_eq!(strip_path_separator_suffix("foo"), "foo");
365 assert_eq!(strip_path_separator_suffix(r"foo\"), r"foo\");
366
367 assert_eq!(rfind_path_separator("a/b/c"), Some(3));
368 assert_eq!(rfind_path_separator("abc"), None);
369 }
370
371 #[test]
372 fn split_path_for_pattern_basic() {
373 let parts: Vec<_> = split_path_for_pattern("a/b/c").collect();
374 assert_eq!(parts, vec!["a", "b", "c"]);
375
376 let parts: Vec<_> = split_path_for_pattern("/a/b").collect();
377 assert_eq!(parts, vec!["", "a", "b"]);
378
379 let parts: Vec<_> = split_path_for_pattern(r"a\b").collect();
381 assert_eq!(parts, vec![r"a\b"]);
382 }
383
384 #[test]
385 fn pattern_path_root_absolute() {
386 assert_eq!(pattern_path_root(""), Some(PathBuf::from("/")));
387 }
388
389 #[test]
390 fn pattern_path_root_relative() {
391 assert_eq!(pattern_path_root("foo"), None);
392 assert_eq!(pattern_path_root("c:"), None);
394 }
395
396 #[test]
397 fn push_path_for_pattern_appends_child() {
398 let mut p = PathBuf::from("/home/reuben");
399 push_path_for_pattern(&mut p, "foo");
400 assert_eq!(p, PathBuf::from("/home/reuben/foo"));
401 }
402
403 #[test]
404 fn normalize_path_separators_is_noop() {
405 use std::borrow::Cow;
406 assert!(matches!(
407 normalize_path_separators("/foo/bar"),
408 Cow::Borrowed("/foo/bar")
409 ));
410 }
411
412 #[test]
413 fn default_case_insensitive_is_false() {
414 assert!(!default_case_insensitive_path_expansion());
415 }
416
417 #[test]
418 fn resolve_executable_returns_input_unchanged() {
419 let path = PathBuf::from("/bin/sh");
421 let resolved = resolve_executable(path.clone());
422 assert_eq!(resolved.as_deref(), Some(path.as_path()));
423 }
424
425 #[test]
426 fn resolve_executable_returns_none_for_nonexistent() {
427 let path = PathBuf::from("/this/path/should/not/exist/shrub-test");
428 assert!(resolve_executable(path).is_none());
429 }
430
431 #[test]
432 fn resolve_executable_returns_none_for_non_executable() {
433 let path = PathBuf::from("/etc/passwd");
436 assert!(resolve_executable(path).is_none());
437 }
438}