Skip to main content

shrub_core/sys/unix/
fs.rs

1//! Filesystem utilities.
2
3use 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")]
11// _PATH_DEFPATH in https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/include/paths.h
12const 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
75/// Splits a platform-specific PATH-like value into individual paths.
76///
77/// On Unix, this delegates to [`std::env::split_paths`].
78pub 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        // standard hard-coded defaults for executable search path
90        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
101/// Retrieves the platform-specific set of paths that should contain standard
102/// system utilities. Used by `command -p`, for example.
103pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
104    //
105    // Try to call confstr(_CS_PATH). If that fails, can't find a string value,
106    // or finds an empty string, then we'll fall back to hard-coded
107    // defaults.
108    //
109
110    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        // standard hard-coded defaults
123        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/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH
154/// variable which indicates where all the POSIX.2 standard utilities can be
155/// found.
156///
157/// N.B. We would strongly prefer to use a safe API exposed (in an idiomatic
158/// way) by nix or similar. Until that exists, we accept the need to make the
159/// unsafe call directly.
160#[cfg(not(target_os = "android"))]
161fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
162    // SAFETY:
163    // Calling `confstr` with a null pointer and size 0 is a documented way to
164    // query the required size of the buffer to hold the value associated
165    // with `name`. It should not end up causing any undefined behavior.
166    let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
167
168    // When confstr returns 0, it either means there's no value associated with
169    // _CS_PATH, or _CS_PATH is considered invalid (and not present) on this
170    // platform. In both cases, we treat it as a non-existent value and
171    // return None.
172    if required_size == 0 {
173        return Ok(None);
174    }
175
176    let mut buffer = Vec::<u8>::with_capacity(required_size);
177
178    // SAFETY:
179    // We are calling `confstr` with a valid pointer and size that we obtained
180    // from the allocated buffer. Writing `c_char` (i8 or u8 depending on
181    // the platform) into `Vec<u8>` is fine, as i8 and u8 have compatible
182    // representations, and Rust does not support platforms where `c_char`
183    // is not 8-bit wide.
184    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    // Per the docs on `confstr`, it *may* return a size larger than the
192    // provided buffer. In our usage we wouldn't expect to see this, as
193    // we've first queried the required size. However, we defensively check
194    // for this case and return an error if it happens.
195    if final_size > buffer.capacity() {
196        return Err(std::io::Error::other(
197            "confstr needed more space than advertised",
198        ));
199    }
200
201    // SAFETY:
202    // We are trusting `confstr` to have written exactly `final_size` bytes into
203    // the buffer. We have checked above that it didn't return a value
204    // *larger* than the capacity of the buffer, and also checked for known
205    // error cases. Note that the returned length should include the null
206    // terminator.
207    unsafe { buffer.set_len(final_size) };
208
209    // The last byte is a null terminator. We assert that it is.
210    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
219/// Opens a null file that will discard all I/O.
220///
221/// # Errors
222///
223/// Returns [`crate::error::ErrorKind::IoError`] if `/dev/null` cannot be opened
224/// for reading and writing.
225pub 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
234/// Gives the platform an opportunity to handle a special file path (e.g.
235/// `/dev/null`).
236///
237/// # Errors
238///
239/// This implementation never returns `Some`, so it reports no error.
240pub const fn try_open_special_file(_path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
241    None
242}
243
244/// Returns the path to the system-wide shell profile script.
245pub fn get_system_profile_path() -> Option<&'static Path> {
246    Some(Path::new("/etc/profile"))
247}
248
249/// Returns the path to the system-wide shell rc script.
250pub fn get_system_rc_path() -> Option<&'static Path> {
251    Some(Path::new("/etc/bash.bashrc"))
252}
253
254/// Returns true if the string contains a path separator character.
255///
256/// On Unix, only `/` is considered a path separator.
257pub fn contains_path_separator(s: &str) -> bool {
258    s.contains('/')
259}
260
261/// Returns true if the string ends with a path separator character.
262///
263/// On Unix, only `/` is considered a path separator.
264pub fn ends_with_path_separator(s: &str) -> bool {
265    s.ends_with('/')
266}
267
268/// Returns the string with a trailing path separator removed, if present.
269///
270/// On Unix, only `/` is considered a path separator.
271pub fn strip_path_separator_suffix(s: &str) -> &str {
272    s.strip_suffix('/').unwrap_or(s)
273}
274
275/// Returns the platform default for case-insensitive pathname expansion.
276///
277/// On Unix, filesystems are typically case-sensitive, so this returns `false`.
278pub const fn default_case_insensitive_path_expansion() -> bool {
279    false
280}
281
282/// Finds the byte index of the last path separator in the string.
283///
284/// On Unix, only `/` is considered a path separator.
285pub fn rfind_path_separator(s: &str) -> Option<usize> {
286    s.rfind('/')
287}
288
289/// Splits a string on path separator characters, returning an iterator of
290/// components.
291///
292/// On Unix, only `/` is used as a separator.
293pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
294    s.split('/')
295}
296
297/// Returns the root path for an absolute pattern, if the first component
298/// indicates one.
299///
300/// On Unix, an empty first component (from splitting a path like `/foo`)
301/// indicates an absolute path rooted at `/`.
302pub 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
310/// Pushes a component onto a path for pattern expansion.
311///
312/// On Unix, this delegates directly to `PathBuf::push`.
313pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
314    path.push(component);
315}
316
317/// Normalizes path separators for shell output.
318///
319/// On Unix, this is a no-op since paths already use `/`.
320pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
321    std::borrow::Cow::Borrowed(s)
322}
323
324/// Resolves an owned path to the actual on-disk executable file, if any.
325///
326/// On Unix this is a straight passthrough: if the path is executable, the
327/// path is returned unchanged (no clone). This keeps `pathsearch::next`
328/// allocation-free on the happy path.
329///
330/// On Windows this function may append a `PATHEXT` extension and return a
331/// possibly-different `PathBuf`.
332pub 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
341/// Resolves a path to a potential executable.
342///
343/// Unix does not need PATHEXT resolution, so the path is left unchanged.
344pub(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        // Backslashes are not separators on Unix.
357        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        // Backslashes are not split on Unix.
380        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        // Drive-letter syntax is not recognized on Unix.
393        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        // /bin/sh exists and is executable on every supported Unix host.
420        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        // /etc/hostname (or similar) is a regular file but not executable.
434        // Use /etc/passwd which is universally present and not executable.
435        let path = PathBuf::from("/etc/passwd");
436        assert!(resolve_executable(path).is_none());
437    }
438}