use crate::fs::OpenOptions; use ambient_authority::AmbientAuthority; use rustix::fs::OFlags; use std::ffi::{OsStr, OsString}; use std::ops::Deref; #[cfg(unix)] use std::os::unix::{ ffi::{OsStrExt, OsStringExt}, fs::OpenOptionsExt, }; #[cfg(target_os = "wasi")] use std::os::wasi::{ ffi::{OsStrExt, OsStringExt}, fs::OpenOptionsExt, }; use std::path::{Path, PathBuf}; use std::{fs, io}; /// Rust's `Path` implicitly strips redundant slashes, however they aren't /// redundant in one case: at the end of a path they indicate that a path is /// expected to name a directory. pub(crate) fn path_requires_dir(path: &Path) -> bool { let bytes = path.as_os_str().as_bytes(); // If a path ends with '/' or '.', it's a directory. These aren't the only // cases, but they are the only cases that Rust's `Path` implicitly // normalizes away. bytes.ends_with(b"/") || bytes.ends_with(b"/.") } /// Rust's `Path` implicitly strips trailing `.` components, however they /// aren't redundant in one case: at the end of a path they are the final path /// component, which has different path lookup behavior. pub(crate) fn path_has_trailing_dot(path: &Path) -> bool { let mut bytes = path.as_os_str().as_bytes(); // If a path ends with '.' followed by any number of '/'s, it's a trailing dot. while let Some((last, rest)) = bytes.split_last() { if *last == b'/' { bytes = rest; } else { break; } } bytes.ends_with(b"/.") || bytes == b"." } /// Rust's `Path` implicitly strips trailing `/`s, however they aren't /// redundant in one case: at the end of a path they are the final path /// component, which has different path lookup behavior. pub(crate) fn path_has_trailing_slash(path: &Path) -> bool { let bytes = path.as_os_str().as_bytes(); bytes.ends_with(b"/") } /// Append a trailing `/`. This can be used to require that the given `path` /// names a directory. pub(crate) fn append_dir_suffix(path: PathBuf) -> PathBuf { let mut bytes = path.into_os_string().into_vec(); bytes.push(b'/'); OsString::from_vec(bytes).into() } /// Strip trailing `/`s, unless this reduces `path` to `/` itself. This is /// used by `create_dir` and others to prevent paths like `foo/` from /// canonicalizing to `foo/.` since these syscalls treat these differently. #[allow(clippy::indexing_slicing)] pub(crate) fn strip_dir_suffix(path: &Path) -> impl Deref + '_ { let mut bytes = path.as_os_str().as_bytes(); while bytes.len() > 1 && *bytes.last().unwrap() == b'/' { bytes = &bytes[..bytes.len() - 1]; } OsStr::from_bytes(bytes).as_ref() } /// Return an `OpenOptions` for opening directories. pub(crate) fn dir_options() -> OpenOptions { OpenOptions::new().read(true).dir_required(true).clone() } /// Like `dir_options`, but additionally request the ability to read the /// directory entries. pub(crate) fn readdir_options() -> OpenOptions { OpenOptions::new() .read(true) .dir_required(true) .readdir_required(true) .clone() } /// Return an `OpenOptions` for canonicalizing paths. pub(crate) fn canonicalize_options() -> OpenOptions { OpenOptions::new().read(true).clone() } /// Open a directory named by a bare path, using the host process' ambient /// authority. /// /// # Ambient Authority /// /// This function is not sandboxed and may trivially access any path that the /// host process has access to. pub(crate) fn open_ambient_dir_impl( path: &Path, ambient_authority: AmbientAuthority, ) -> io::Result { let _ = ambient_authority; let mut options = fs::OpenOptions::new(); options.read(true); #[cfg(not(target_os = "wasi"))] // This is for `std::fs`, so we don't have `dir_required`, so set // `O_DIRECTORY` manually. options.custom_flags((OFlags::DIRECTORY | target_o_path()).bits() as i32); #[cfg(target_os = "wasi")] options.directory(true); options.open(path) } /// Use `O_PATH` on platforms which have it, or none otherwise. #[inline] pub(crate) const fn target_o_path() -> OFlags { #[cfg(any( target_os = "android", target_os = "emscripten", target_os = "freebsd", target_os = "fuchsia", target_os = "linux", target_os = "redox", ))] { OFlags::PATH } #[cfg(any( target_os = "dragonfly", target_os = "ios", target_os = "macos", target_os = "netbsd", target_os = "openbsd", target_os = "wasi", target_os = "illumos", target_os = "solaris", ))] { OFlags::empty() } } #[cfg(racy_asserts)] #[test] fn append_dir_suffix_tests() { assert!(append_dir_suffix(Path::new("foo").to_path_buf()) .display() .to_string() .ends_with('/')); } #[test] fn strip_dir_suffix_tests() { assert_eq!(&*strip_dir_suffix(Path::new("/foo//")), Path::new("/foo")); assert_eq!(&*strip_dir_suffix(Path::new("/foo/")), Path::new("/foo")); assert_eq!(&*strip_dir_suffix(Path::new("foo/")), Path::new("foo")); assert_eq!(&*strip_dir_suffix(Path::new("foo")), Path::new("foo")); assert_eq!(&*strip_dir_suffix(Path::new("/")), Path::new("/")); assert_eq!(&*strip_dir_suffix(Path::new("//")), Path::new("/")); assert_eq!(&*strip_dir_suffix(Path::new("/.")), Path::new("/.")); assert_eq!(&*strip_dir_suffix(Path::new("//.")), Path::new("/.")); assert_eq!(&*strip_dir_suffix(Path::new(".")), Path::new(".")); assert_eq!(&*strip_dir_suffix(Path::new("foo/.")), Path::new("foo/.")); } #[test] fn test_path_requires_dir() { assert!(!path_requires_dir(Path::new("."))); assert!(path_requires_dir(Path::new("/"))); assert!(path_requires_dir(Path::new("//"))); assert!(path_requires_dir(Path::new("/./."))); assert!(path_requires_dir(Path::new("foo/"))); assert!(path_requires_dir(Path::new("foo//"))); assert!(path_requires_dir(Path::new("foo//."))); assert!(path_requires_dir(Path::new("foo/./."))); assert!(path_requires_dir(Path::new("foo/./"))); assert!(path_requires_dir(Path::new("foo/.//"))); } #[test] fn test_path_has_trailing_dot() { assert!(!path_has_trailing_dot(Path::new("foo"))); assert!(!path_has_trailing_dot(Path::new("foo."))); assert!(!path_has_trailing_dot(Path::new("/./foo"))); assert!(!path_has_trailing_dot(Path::new(".."))); assert!(!path_has_trailing_dot(Path::new("/.."))); assert!(!path_has_trailing_dot(Path::new("/"))); assert!(!path_has_trailing_dot(Path::new("//"))); assert!(!path_has_trailing_dot(Path::new("foo//"))); assert!(!path_has_trailing_dot(Path::new("foo/"))); assert!(path_has_trailing_dot(Path::new("."))); assert!(path_has_trailing_dot(Path::new("/./."))); assert!(path_has_trailing_dot(Path::new("foo//."))); assert!(path_has_trailing_dot(Path::new("foo/./."))); assert!(path_has_trailing_dot(Path::new("foo/./"))); assert!(path_has_trailing_dot(Path::new("foo/.//"))); } #[test] fn test_path_has_trailing_slash() { assert!(path_has_trailing_slash(Path::new("/"))); assert!(path_has_trailing_slash(Path::new("//"))); assert!(path_has_trailing_slash(Path::new("foo//"))); assert!(path_has_trailing_slash(Path::new("foo/"))); assert!(path_has_trailing_slash(Path::new("foo/./"))); assert!(path_has_trailing_slash(Path::new("foo/.//"))); assert!(!path_has_trailing_slash(Path::new("foo"))); assert!(!path_has_trailing_slash(Path::new("foo."))); assert!(!path_has_trailing_slash(Path::new("/./foo"))); assert!(!path_has_trailing_slash(Path::new(".."))); assert!(!path_has_trailing_slash(Path::new("/.."))); assert!(!path_has_trailing_slash(Path::new("."))); assert!(!path_has_trailing_slash(Path::new("/./."))); assert!(!path_has_trailing_slash(Path::new("foo//."))); assert!(!path_has_trailing_slash(Path::new("foo/./."))); }