shrub_core/sys/unix/
fs.rs1use std::os::unix::ffi::OsStringExt;
13use std::os::unix::fs::FileTypeExt;
14pub use std::os::unix::fs::MetadataExt;
15use std::path::{Path, PathBuf};
16
17use crate::error;
18
19#[cfg(target_os = "android")]
20const 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";
22
23impl crate::sys::fs::PathExt for Path {
24 fn readable(&self) -> bool {
25 nix::unistd::access(self, nix::unistd::AccessFlags::R_OK).is_ok()
26 }
27
28 fn writable(&self) -> bool {
29 nix::unistd::access(self, nix::unistd::AccessFlags::W_OK).is_ok()
30 }
31
32 fn executable(&self) -> bool {
33 nix::unistd::access(self, nix::unistd::AccessFlags::X_OK).is_ok()
34 }
35
36 fn exists_and_is_block_device(&self) -> bool {
37 try_get_file_type(self).is_some_and(|ft| ft.is_block_device())
38 }
39
40 fn exists_and_is_char_device(&self) -> bool {
41 try_get_file_type(self).is_some_and(|ft| ft.is_char_device())
42 }
43
44 fn exists_and_is_fifo(&self) -> bool {
45 try_get_file_type(self).is_some_and(|ft: std::fs::FileType| ft.is_fifo())
46 }
47
48 fn exists_and_is_socket(&self) -> bool {
49 try_get_file_type(self).is_some_and(|ft| ft.is_socket())
50 }
51
52 fn exists_and_is_setgid(&self) -> bool {
53 const S_ISGID: u32 = 0o2000;
54 let file_mode = try_get_file_mode(self);
55 file_mode.is_some_and(|mode| mode & S_ISGID != 0)
56 }
57
58 fn exists_and_is_setuid(&self) -> bool {
59 const S_ISUID: u32 = 0o4000;
60 let file_mode = try_get_file_mode(self);
61 file_mode.is_some_and(|mode| mode & S_ISUID != 0)
62 }
63
64 fn exists_and_is_sticky_bit(&self) -> bool {
65 const S_ISVTX: u32 = 0o1000;
66 let file_mode = try_get_file_mode(self);
67 file_mode.is_some_and(|mode| mode & S_ISVTX != 0)
68 }
69
70 fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
71 let metadata = self.metadata()?;
72 Ok((metadata.dev(), metadata.ino()))
73 }
74}
75
76fn try_get_file_type(path: &Path) -> Option<std::fs::FileType> {
77 path.metadata().map(|metadata| metadata.file_type()).ok()
78}
79
80fn try_get_file_mode(path: &Path) -> Option<u32> {
81 path.metadata().map(|metadata| metadata.mode()).ok()
82}
83
84pub fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(s: &T) -> std::env::SplitPaths<'_> {
88 std::env::split_paths(s)
89}
90
91pub(crate) fn get_default_executable_search_paths() -> Vec<PathBuf> {
92 #[cfg(target_os = "android")]
93 {
94 std::env::split_paths(ANDROID_DEFPATH).collect()
95 }
96 #[cfg(not(target_os = "android"))]
97 {
98 vec![
100 "/usr/local/sbin".into(),
101 "/usr/local/bin".into(),
102 "/usr/sbin".into(),
103 "/usr/bin".into(),
104 "/sbin".into(),
105 "/bin".into(),
106 ]
107 }
108}
109
110pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
113 if let Ok(Some(cs_path)) = confstr_cs_path()
120 && !cs_path.as_os_str().is_empty()
121 {
122 return split_paths(&cs_path).collect();
123 }
124
125 #[cfg(target_os = "android")]
126 {
127 std::env::split_paths(ANDROID_DEFPATH).collect()
128 }
129 #[cfg(not(target_os = "android"))]
130 {
131 vec![
133 "/bin".into(),
134 "/usr/bin".into(),
135 "/sbin".into(),
136 "/usr/sbin".into(),
137 "/etc".into(),
138 "/usr/etc".into(),
139 ]
140 }
141}
142
143#[allow(clippy::unnecessary_wraps)]
144fn confstr_cs_path() -> Result<Option<PathBuf>, std::io::Error> {
145 #[cfg(target_os = "android")]
146 {
147 Ok(Some(PathBuf::from(ANDROID_DEFPATH)))
148 }
149 #[cfg(not(target_os = "android"))]
150 {
151 let value = confstr(nix::libc::_CS_PATH)?;
152
153 if let Some(value) = value {
154 let value_str = PathBuf::from(value);
155 Ok(Some(value_str))
156 } else {
157 Ok(None)
158 }
159 }
160}
161
162#[cfg(not(target_os = "android"))]
170fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
171 let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
175
176 if required_size == 0 {
181 return Ok(None);
182 }
183
184 let mut buffer = Vec::<u8>::with_capacity(required_size);
185
186 let final_size =
191 unsafe { nix::libc::confstr(name, buffer.as_mut_ptr().cast(), buffer.capacity()) };
192
193 if final_size == 0 {
194 return Err(std::io::Error::last_os_error());
195 }
196
197 if final_size > buffer.capacity() {
202 return Err(std::io::Error::other(
203 "confstr needed more space than advertised",
204 ));
205 }
206
207 unsafe { buffer.set_len(final_size) };
212
213 if !matches!(buffer.pop(), Some(0)) {
215 return Err(std::io::Error::other(
216 "confstr did not null-terminate the returned string",
217 ));
218 }
219
220 Ok(Some(std::ffi::OsString::from_vec(buffer)))
221}
222
223pub fn open_null_file() -> Result<std::fs::File, error::Error> {
230 let f = std::fs::File::options()
231 .read(true)
232 .write(true)
233 .open("/dev/null")?;
234
235 Ok(f)
236}
237
238pub const fn try_open_special_file(_path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
245 None
246}
247
248pub fn get_system_profile_path() -> Option<&'static Path> {
250 Some(Path::new("/etc/profile"))
251}
252
253pub fn get_system_rc_path() -> Option<&'static Path> {
255 Some(Path::new("/etc/bash.bashrc"))
256}
257
258pub fn contains_path_separator(s: &str) -> bool {
262 s.contains('/')
263}
264
265pub fn ends_with_path_separator(s: &str) -> bool {
269 s.ends_with('/')
270}
271
272pub fn strip_path_separator_suffix(s: &str) -> &str {
276 s.strip_suffix('/').unwrap_or(s)
277}
278
279pub const fn default_case_insensitive_path_expansion() -> bool {
283 false
284}
285
286pub fn rfind_path_separator(s: &str) -> Option<usize> {
290 s.rfind('/')
291}
292
293pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
298 s.split('/')
299}
300
301pub fn pattern_path_root(first_component: &str) -> Option<PathBuf> {
307 if first_component.is_empty() {
308 Some(PathBuf::from("/"))
309 } else {
310 None
311 }
312}
313
314pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
318 path.push(component);
319}
320
321pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
325 std::borrow::Cow::Borrowed(s)
326}
327
328pub fn resolve_executable(path: PathBuf) -> Option<PathBuf> {
337 use crate::sys::fs::PathExt;
338 if path.as_path().executable() {
339 Some(path)
340 } else {
341 None
342 }
343}
344
345pub(crate) const fn resolve_executable_path(path: PathBuf, _working_dir: &Path) -> PathBuf {
349 path
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn path_separator_helpers() {
358 assert!(contains_path_separator("foo/bar"));
359 assert!(!contains_path_separator("foobar"));
360 assert!(!contains_path_separator(r"foo\bar"));
362
363 assert!(ends_with_path_separator("foo/"));
364 assert!(!ends_with_path_separator("foo"));
365 assert!(!ends_with_path_separator(r"foo\"));
366
367 assert_eq!(strip_path_separator_suffix("foo/"), "foo");
368 assert_eq!(strip_path_separator_suffix("foo"), "foo");
369 assert_eq!(strip_path_separator_suffix(r"foo\"), r"foo\");
370
371 assert_eq!(rfind_path_separator("a/b/c"), Some(3));
372 assert_eq!(rfind_path_separator("abc"), None);
373 }
374
375 #[test]
376 fn split_path_for_pattern_basic() {
377 let parts: Vec<_> = split_path_for_pattern("a/b/c").collect();
378 assert_eq!(parts, vec!["a", "b", "c"]);
379
380 let parts: Vec<_> = split_path_for_pattern("/a/b").collect();
381 assert_eq!(parts, vec!["", "a", "b"]);
382
383 let parts: Vec<_> = split_path_for_pattern(r"a\b").collect();
385 assert_eq!(parts, vec![r"a\b"]);
386 }
387
388 #[test]
389 fn pattern_path_root_absolute() {
390 assert_eq!(pattern_path_root(""), Some(PathBuf::from("/")));
391 }
392
393 #[test]
394 fn pattern_path_root_relative() {
395 assert_eq!(pattern_path_root("foo"), None);
396 assert_eq!(pattern_path_root("c:"), None);
398 }
399
400 #[test]
401 fn push_path_for_pattern_appends_child() {
402 let mut p = PathBuf::from("/home/reuben");
403 push_path_for_pattern(&mut p, "foo");
404 assert_eq!(p, PathBuf::from("/home/reuben/foo"));
405 }
406
407 #[test]
408 fn normalize_path_separators_is_noop() {
409 use std::borrow::Cow;
410 assert!(matches!(
411 normalize_path_separators("/foo/bar"),
412 Cow::Borrowed("/foo/bar")
413 ));
414 }
415
416 #[test]
417 fn default_case_insensitive_is_false() {
418 assert!(!default_case_insensitive_path_expansion());
419 }
420
421 #[test]
422 fn resolve_executable_returns_input_unchanged() {
423 let path = PathBuf::from("/bin/sh");
425 let resolved = resolve_executable(path.clone());
426 assert_eq!(resolved.as_deref(), Some(path.as_path()));
427 }
428
429 #[test]
430 fn resolve_executable_returns_none_for_nonexistent() {
431 let path = PathBuf::from("/this/path/should/not/exist/shrub-test");
432 assert!(resolve_executable(path).is_none());
433 }
434
435 #[test]
436 fn resolve_executable_returns_none_for_non_executable() {
437 let path = PathBuf::from("/etc/passwd");
440 assert!(resolve_executable(path).is_none());
441 }
442}