Skip to main content

shrub_core/sys/unix/
fs.rs

1//! Filesystem utilities.
2//!
3//! # Why this module is unsafe
4//!
5//! 1. Safe code cannot do it. Outside Android, the default `PATH` comes from
6//!    `confstr(_CS_PATH)`, which neither the standard library nor nix wraps, so
7//!    `confstr` calls libc's function, which writes through a raw pointer.
8//! 2. Speed is not the reason.
9//! 3. The unsafe operations are the two `libc::confstr` calls and the
10//!    `Vec::set_len` that records what the second one wrote, in `confstr`.
11
12use 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")]
20// _PATH_DEFPATH in https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/include/paths.h
21const 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
84/// Splits a platform-specific PATH-like value into individual paths.
85///
86/// On Unix, this delegates to [`std::env::split_paths`].
87pub 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        // standard hard-coded defaults for executable search path
99        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
110/// Retrieves the platform-specific set of paths that should contain standard
111/// system utilities. Used by `command -p`, for example.
112pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
113    //
114    // Try to call confstr(_CS_PATH). If that fails, can't find a string value,
115    // or finds an empty string, then we'll fall back to hard-coded
116    // defaults.
117    //
118
119    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        // standard hard-coded defaults
132        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/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH
163/// variable which indicates where all the POSIX.2 standard utilities can be
164/// found.
165///
166/// N.B. We would strongly prefer to use a safe API exposed (in an idiomatic
167/// way) by nix or similar. Until that exists, we accept the need to make the
168/// unsafe call directly.
169#[cfg(not(target_os = "android"))]
170fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
171    // SAFETY: `confstr` writes at most `len` bytes to `buf`. With `len` 0 it
172    // writes nothing, so the null `buf` is never written; it returns the size
173    // the value needs, terminator included (POSIX `confstr`).
174    let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
175
176    // When confstr returns 0, it either means there's no value associated with
177    // _CS_PATH, or _CS_PATH is considered invalid (and not present) on this
178    // platform. In both cases, we treat it as a non-existent value and
179    // return None.
180    if required_size == 0 {
181        return Ok(None);
182    }
183
184    let mut buffer = Vec::<u8>::with_capacity(required_size);
185
186    // SAFETY: `confstr` writes at most `len` bytes to `buf`. `buf` points to
187    // `buffer`'s allocation, which holds `buffer.capacity()` bytes and lives
188    // across the call, and `len` is that capacity. `c_char` is one byte on
189    // every platform Rust supports, so the bytes it writes are valid `u8`s.
190    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    // Per the docs on `confstr`, it *may* return a size larger than the
198    // provided buffer. In our usage we wouldn't expect to see this, as
199    // we've first queried the required size. However, we defensively check
200    // for this case and return an error if it happens.
201    if final_size > buffer.capacity() {
202        return Err(std::io::Error::other(
203            "confstr needed more space than advertised",
204        ));
205    }
206
207    // SAFETY: `set_len` requires a length at most the capacity, with every
208    // byte below it initialized. `final_size` is at most `buffer.capacity()`,
209    // checked above, and `confstr` wrote the value and its terminator, which
210    // is `final_size` bytes when they fit in `len`.
211    unsafe { buffer.set_len(final_size) };
212
213    // The last byte is a null terminator. We assert that it is.
214    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
223/// Opens a null file that will discard all I/O.
224///
225/// # Errors
226///
227/// Returns [`crate::error::ErrorKind::IoError`] if `/dev/null` cannot be opened
228/// for reading and writing.
229pub 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
238/// Gives the platform an opportunity to handle a special file path (e.g.
239/// `/dev/null`).
240///
241/// # Errors
242///
243/// This implementation never returns `Some`, so it reports no error.
244pub const fn try_open_special_file(_path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
245    None
246}
247
248/// Returns the path to the system-wide shell profile script.
249pub fn get_system_profile_path() -> Option<&'static Path> {
250    Some(Path::new("/etc/profile"))
251}
252
253/// Returns the path to the system-wide shell rc script.
254pub fn get_system_rc_path() -> Option<&'static Path> {
255    Some(Path::new("/etc/bash.bashrc"))
256}
257
258/// Returns true if the string contains a path separator character.
259///
260/// On Unix, only `/` is considered a path separator.
261pub fn contains_path_separator(s: &str) -> bool {
262    s.contains('/')
263}
264
265/// Returns true if the string ends with a path separator character.
266///
267/// On Unix, only `/` is considered a path separator.
268pub fn ends_with_path_separator(s: &str) -> bool {
269    s.ends_with('/')
270}
271
272/// Returns the string with a trailing path separator removed, if present.
273///
274/// On Unix, only `/` is considered a path separator.
275pub fn strip_path_separator_suffix(s: &str) -> &str {
276    s.strip_suffix('/').unwrap_or(s)
277}
278
279/// Returns the platform default for case-insensitive pathname expansion.
280///
281/// On Unix, filesystems are typically case-sensitive, so this returns `false`.
282pub const fn default_case_insensitive_path_expansion() -> bool {
283    false
284}
285
286/// Finds the byte index of the last path separator in the string.
287///
288/// On Unix, only `/` is considered a path separator.
289pub fn rfind_path_separator(s: &str) -> Option<usize> {
290    s.rfind('/')
291}
292
293/// Splits a string on path separator characters, returning an iterator of
294/// components.
295///
296/// On Unix, only `/` is used as a separator.
297pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
298    s.split('/')
299}
300
301/// Returns the root path for an absolute pattern, if the first component
302/// indicates one.
303///
304/// On Unix, an empty first component (from splitting a path like `/foo`)
305/// indicates an absolute path rooted at `/`.
306pub 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
314/// Pushes a component onto a path for pattern expansion.
315///
316/// On Unix, this delegates directly to `PathBuf::push`.
317pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
318    path.push(component);
319}
320
321/// Normalizes path separators for shell output.
322///
323/// On Unix, this is a no-op since paths already use `/`.
324pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
325    std::borrow::Cow::Borrowed(s)
326}
327
328/// Resolves an owned path to the actual on-disk executable file, if any.
329///
330/// On Unix this is a straight passthrough: if the path is executable, the
331/// path is returned unchanged (no clone). This keeps `pathsearch::next`
332/// allocation-free on the happy path.
333///
334/// On Windows this function may append a `PATHEXT` extension and return a
335/// possibly-different `PathBuf`.
336pub 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
345/// Resolves a path to a potential executable.
346///
347/// Unix does not need PATHEXT resolution, so the path is left unchanged.
348pub(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        // Backslashes are not separators on Unix.
361        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        // Backslashes are not split on Unix.
384        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        // Drive-letter syntax is not recognized on Unix.
397        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        // /bin/sh exists and is executable on every supported Unix host.
424        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        // /etc/hostname (or similar) is a regular file but not executable.
438        // Use /etc/passwd which is universally present and not executable.
439        let path = PathBuf::from("/etc/passwd");
440        assert!(resolve_executable(path).is_none());
441    }
442}