brush_core/sys/unix/
fs.rs

1//! Filesystem utilities.
2
3use std::os::unix::ffi::OsStringExt;
4use std::os::unix::fs::FileTypeExt;
5use std::path::Path;
6
7use crate::error;
8
9pub use std::os::unix::fs::MetadataExt;
10
11const DEFAULT_EXECUTABLE_SEARCH_PATHS: &[&str] = &[
12    "/usr/local/sbin",
13    "/usr/local/bin",
14    "/usr/sbin",
15    "/usr/bin",
16    "/sbin",
17    "/bin",
18];
19
20const DEFAULT_STANDARD_UTILS_PATHS: &[&str] =
21    &["/bin", "/usr/bin", "/sbin", "/usr/sbin", "/etc", "/usr/etc"];
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(crate) fn get_default_executable_search_paths() -> Vec<String> {
85    DEFAULT_EXECUTABLE_SEARCH_PATHS
86        .iter()
87        .map(|s| (*s).to_owned())
88        .collect()
89}
90
91/// Retrieves the platform-specific set of paths that should contain standard system
92/// utilities. Used by `command -p`, for example.
93pub fn get_default_standard_utils_paths() -> Vec<String> {
94    //
95    // Try to call confstr(_CS_PATH). If that fails, can't find a string value, or
96    // finds an empty string, then we'll fall back to hard-coded defaults.
97    //
98
99    if let Ok(Some(cs_path)) = confstr_cs_path() {
100        if !cs_path.is_empty() {
101            return cs_path.split(':').map(|s| s.to_string()).collect();
102        }
103    }
104
105    DEFAULT_STANDARD_UTILS_PATHS
106        .iter()
107        .map(|s| (*s).to_owned())
108        .collect()
109}
110
111fn confstr_cs_path() -> Result<Option<String>, std::io::Error> {
112    let value = confstr(nix::libc::_CS_PATH)?;
113
114    if let Some(value) = value {
115        let value_str = value
116            .into_string()
117            .map_err(|_err| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid data"))?;
118        Ok(Some(value_str))
119    } else {
120        Ok(None)
121    }
122}
123
124/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH variable which
125/// indicates where all the POSIX.2 standard utilities can be found.
126///
127/// N.B. We would strongly prefer to use a safe API exposed (in an idiomatic way) by nix
128/// or similar. Until that exists, we accept the need to make the unsafe call directly.
129fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
130    // SAFETY:
131    // Calling `confstr` with a null pointer and size 0 is a documented way to query
132    // the required size of the buffer to hold the value associated with `name`. It
133    // should not end up causing any undefined behavior.
134    let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
135
136    // When confstr returns 0, it either means there's no value associated with _CS_PATH, or
137    // _CS_PATH is considered invalid (and not present) on this platform. In both cases, we
138    // treat it as a non-existent value and return None.
139    if required_size == 0 {
140        return Ok(None);
141    }
142
143    let mut buffer = Vec::<u8>::with_capacity(required_size);
144
145    // SAFETY:
146    // We are calling `confstr` with a valid pointer and size that we obtained from the
147    // allocated buffer. Writing `c_char` (i8 or u8 depending on the platform) into
148    // `Vec<u8>` is fine, as i8 and u8 have compatible representations, and Rust does
149    // not support platforms where `c_char` is not 8-bit wide.
150    let final_size =
151        unsafe { nix::libc::confstr(name, buffer.as_mut_ptr().cast(), buffer.capacity()) };
152
153    if final_size == 0 {
154        return Err(std::io::Error::last_os_error());
155    }
156
157    // Per the docs on `confstr`, it *may* return a size larger than the provided buffer.
158    // In our usage we wouldn't expect to see this, as we've first queried the required size.
159    // However, we defensively check for this case and return an error if it happens.
160    if final_size > buffer.capacity() {
161        return Err(std::io::Error::other(
162            "confstr needed more space than advertised",
163        ));
164    }
165
166    // SAFETY:
167    // We are trusting `confstr` to have written exactly `final_size` bytes into the buffer.
168    // We have checked above that it didn't return a value *larger* than the capacity of
169    // the buffer, and also checked for known error cases. Note that the returned length
170    // should include the null terminator.
171    unsafe { buffer.set_len(final_size) };
172
173    // The last byte is a null terminator. We assert that it is.
174    if !matches!(buffer.pop(), Some(0)) {
175        return Err(std::io::Error::other(
176            "confstr did not null-terminate the returned string",
177        ));
178    }
179
180    Ok(Some(std::ffi::OsString::from_vec(buffer)))
181}
182
183/// Opens a null file that will discard all I/O.
184pub fn open_null_file() -> Result<std::fs::File, error::Error> {
185    let f = std::fs::File::options()
186        .read(true)
187        .write(true)
188        .open("/dev/null")?;
189
190    Ok(f)
191}