cap_primitives/rustix/fs/
dir_utils.rs

1use crate::fs::OpenOptions;
2use ambient_authority::AmbientAuthority;
3use rustix::fs::OFlags;
4use std::ffi::{OsStr, OsString};
5use std::ops::Deref;
6#[cfg(unix)]
7use std::os::unix::{
8    ffi::{OsStrExt, OsStringExt},
9    fs::OpenOptionsExt,
10};
11#[cfg(target_os = "wasi")]
12use std::os::wasi::{
13    ffi::{OsStrExt, OsStringExt},
14    fs::OpenOptionsExt,
15};
16use std::path::{Path, PathBuf};
17use std::{fs, io};
18
19/// Rust's `Path` implicitly strips redundant slashes, however they aren't
20/// redundant in one case: at the end of a path they indicate that a path is
21/// expected to name a directory.
22pub(crate) fn path_requires_dir(path: &Path) -> bool {
23    let bytes = path.as_os_str().as_bytes();
24
25    // If a path ends with '/' or '.', it's a directory. These aren't the only
26    // cases, but they are the only cases that Rust's `Path` implicitly
27    // normalizes away.
28    bytes.ends_with(b"/") || bytes.ends_with(b"/.")
29}
30
31/// Rust's `Path` implicitly strips trailing `.` components, however they
32/// aren't redundant in one case: at the end of a path they are the final path
33/// component, which has different path lookup behavior.
34pub(crate) fn path_has_trailing_dot(path: &Path) -> bool {
35    let mut bytes = path.as_os_str().as_bytes();
36
37    // If a path ends with '.' followed by any number of '/'s, it's a trailing dot.
38    while let Some((last, rest)) = bytes.split_last() {
39        if *last == b'/' {
40            bytes = rest;
41        } else {
42            break;
43        }
44    }
45
46    bytes.ends_with(b"/.") || bytes == b"."
47}
48
49/// Rust's `Path` implicitly strips trailing `/`s, however they aren't
50/// redundant in one case: at the end of a path they are the final path
51/// component, which has different path lookup behavior.
52pub(crate) fn path_has_trailing_slash(path: &Path) -> bool {
53    let bytes = path.as_os_str().as_bytes();
54
55    bytes.ends_with(b"/")
56}
57
58/// Append a trailing `/`. This can be used to require that the given `path`
59/// names a directory.
60pub(crate) fn append_dir_suffix(path: PathBuf) -> PathBuf {
61    let mut bytes = path.into_os_string().into_vec();
62    bytes.push(b'/');
63    OsString::from_vec(bytes).into()
64}
65
66/// Strip trailing `/`s, unless this reduces `path` to `/` itself. This is
67/// used by `create_dir` and others to prevent paths like `foo/` from
68/// canonicalizing to `foo/.` since these syscalls treat these differently.
69#[allow(clippy::indexing_slicing)]
70pub(crate) fn strip_dir_suffix(path: &Path) -> impl Deref<Target = Path> + '_ {
71    let mut bytes = path.as_os_str().as_bytes();
72    while bytes.len() > 1 && *bytes.last().unwrap() == b'/' {
73        bytes = &bytes[..bytes.len() - 1];
74    }
75    OsStr::from_bytes(bytes).as_ref()
76}
77
78/// Return an `OpenOptions` for opening directories.
79pub(crate) fn dir_options() -> OpenOptions {
80    OpenOptions::new().read(true).dir_required(true).clone()
81}
82
83/// Like `dir_options`, but additionally request the ability to read the
84/// directory entries.
85pub(crate) fn readdir_options() -> OpenOptions {
86    OpenOptions::new()
87        .read(true)
88        .dir_required(true)
89        .readdir_required(true)
90        .clone()
91}
92
93/// Return an `OpenOptions` for canonicalizing paths.
94pub(crate) fn canonicalize_options() -> OpenOptions {
95    OpenOptions::new().read(true).clone()
96}
97
98/// Open a directory named by a bare path, using the host process' ambient
99/// authority.
100///
101/// # Ambient Authority
102///
103/// This function is not sandboxed and may trivially access any path that the
104/// host process has access to.
105pub(crate) fn open_ambient_dir_impl(
106    path: &Path,
107    ambient_authority: AmbientAuthority,
108) -> io::Result<fs::File> {
109    let _ = ambient_authority;
110
111    let mut options = fs::OpenOptions::new();
112    options.read(true);
113
114    #[cfg(not(target_os = "wasi"))]
115    // This is for `std::fs`, so we don't have `dir_required`, so set
116    // `O_DIRECTORY` manually.
117    options.custom_flags((OFlags::DIRECTORY | target_o_path()).bits() as i32);
118    #[cfg(target_os = "wasi")]
119    options.directory(true);
120
121    options.open(path)
122}
123
124/// Use `O_PATH` on platforms which have it, or none otherwise.
125#[inline]
126pub(crate) const fn target_o_path() -> OFlags {
127    #[cfg(any(
128        target_os = "android",
129        target_os = "emscripten",
130        target_os = "freebsd",
131        target_os = "fuchsia",
132        target_os = "linux",
133        target_os = "redox",
134    ))]
135    {
136        OFlags::PATH
137    }
138
139    #[cfg(any(
140        target_os = "dragonfly",
141        target_os = "ios",
142        target_os = "macos",
143        target_os = "tvos",
144        target_os = "watchos",
145        target_os = "visionos",
146        target_os = "netbsd",
147        target_os = "openbsd",
148        target_os = "wasi",
149        target_os = "illumos",
150        target_os = "solaris",
151    ))]
152    {
153        OFlags::empty()
154    }
155}
156
157#[cfg(racy_asserts)]
158#[test]
159fn append_dir_suffix_tests() {
160    assert!(append_dir_suffix(Path::new("foo").to_path_buf())
161        .display()
162        .to_string()
163        .ends_with('/'));
164}
165
166#[test]
167fn strip_dir_suffix_tests() {
168    assert_eq!(&*strip_dir_suffix(Path::new("/foo//")), Path::new("/foo"));
169    assert_eq!(&*strip_dir_suffix(Path::new("/foo/")), Path::new("/foo"));
170    assert_eq!(&*strip_dir_suffix(Path::new("foo/")), Path::new("foo"));
171    assert_eq!(&*strip_dir_suffix(Path::new("foo")), Path::new("foo"));
172    assert_eq!(&*strip_dir_suffix(Path::new("/")), Path::new("/"));
173    assert_eq!(&*strip_dir_suffix(Path::new("//")), Path::new("/"));
174    assert_eq!(&*strip_dir_suffix(Path::new("/.")), Path::new("/."));
175    assert_eq!(&*strip_dir_suffix(Path::new("//.")), Path::new("/."));
176    assert_eq!(&*strip_dir_suffix(Path::new(".")), Path::new("."));
177    assert_eq!(&*strip_dir_suffix(Path::new("foo/.")), Path::new("foo/."));
178}
179
180#[test]
181fn test_path_requires_dir() {
182    assert!(!path_requires_dir(Path::new(".")));
183    assert!(path_requires_dir(Path::new("/")));
184    assert!(path_requires_dir(Path::new("//")));
185    assert!(path_requires_dir(Path::new("/./.")));
186    assert!(path_requires_dir(Path::new("foo/")));
187    assert!(path_requires_dir(Path::new("foo//")));
188    assert!(path_requires_dir(Path::new("foo//.")));
189    assert!(path_requires_dir(Path::new("foo/./.")));
190    assert!(path_requires_dir(Path::new("foo/./")));
191    assert!(path_requires_dir(Path::new("foo/.//")));
192}
193
194#[test]
195fn test_path_has_trailing_dot() {
196    assert!(!path_has_trailing_dot(Path::new("foo")));
197    assert!(!path_has_trailing_dot(Path::new("foo.")));
198
199    assert!(!path_has_trailing_dot(Path::new("/./foo")));
200    assert!(!path_has_trailing_dot(Path::new("..")));
201    assert!(!path_has_trailing_dot(Path::new("/..")));
202
203    assert!(!path_has_trailing_dot(Path::new("/")));
204    assert!(!path_has_trailing_dot(Path::new("//")));
205    assert!(!path_has_trailing_dot(Path::new("foo//")));
206    assert!(!path_has_trailing_dot(Path::new("foo/")));
207
208    assert!(path_has_trailing_dot(Path::new(".")));
209
210    assert!(path_has_trailing_dot(Path::new("/./.")));
211    assert!(path_has_trailing_dot(Path::new("foo//.")));
212    assert!(path_has_trailing_dot(Path::new("foo/./.")));
213    assert!(path_has_trailing_dot(Path::new("foo/./")));
214    assert!(path_has_trailing_dot(Path::new("foo/.//")));
215}
216
217#[test]
218fn test_path_has_trailing_slash() {
219    assert!(path_has_trailing_slash(Path::new("/")));
220    assert!(path_has_trailing_slash(Path::new("//")));
221    assert!(path_has_trailing_slash(Path::new("foo//")));
222    assert!(path_has_trailing_slash(Path::new("foo/")));
223    assert!(path_has_trailing_slash(Path::new("foo/./")));
224    assert!(path_has_trailing_slash(Path::new("foo/.//")));
225
226    assert!(!path_has_trailing_slash(Path::new("foo")));
227    assert!(!path_has_trailing_slash(Path::new("foo.")));
228    assert!(!path_has_trailing_slash(Path::new("/./foo")));
229    assert!(!path_has_trailing_slash(Path::new("..")));
230    assert!(!path_has_trailing_slash(Path::new("/..")));
231    assert!(!path_has_trailing_slash(Path::new(".")));
232    assert!(!path_has_trailing_slash(Path::new("/./.")));
233    assert!(!path_has_trailing_slash(Path::new("foo//.")));
234    assert!(!path_has_trailing_slash(Path::new("foo/./.")));
235}