From 4fc505d09491609e5a61fc5fe1c771af6bb14a17 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 4 Aug 2026 19:05:06 -0400 Subject: [PATCH] add dir operations --- library/std/src/fs.rs | 150 ++++++++++++++++++++++++++ library/std/src/fs/tests.rs | 35 ++++++ library/std/src/sys/fs/common.rs | 20 +++- library/std/src/sys/fs/unix/dir.rs | 67 +++++++++--- library/std/src/sys/fs/windows/dir.rs | 24 +++++ 5 files changed, 281 insertions(+), 15 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 4c5cd0e0c9e6a..433e0bbf57448 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -1570,6 +1570,63 @@ impl Dir { .map(|inner| Self { inner }) } + /// Attempts to open a directory at `path` according to `opts`. + /// + /// This function opens a directory. To open a file instead, see [`File::open`]. + /// + /// # Errors + /// + /// This function will return an error if `path` does not point to an existing directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::{Dir, OpenOptions}, io}; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::open_with("foo", &OpenOptions::new().read(true))?; + /// let mut f = dir.open_file("bar.txt")?; + /// let contents = io::read_to_string(f)?; + /// assert_eq!(contents, "Hello, world!"); + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_with>(path: P, opts: &OpenOptions) -> io::Result { + fs_imp::Dir::open(path.as_ref(), &opts.0).map(|inner| Self { inner }) + } + + /// Attempts to open a directory at `path` with the minimum permissions for traversal. + /// + /// The permissions requested by this function are guaranteed to be sufficient to open a child + /// file or folder, but not necessarily to list all children. + /// + /// # Errors + /// + /// This function may return an error according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::Dir, io}; + /// + /// fn main() -> std::io::Result<()> { + /// let foo = Dir::open_for_traversal("foo")?; + /// let foobar = foo.open_dir("bar")?; + /// let mut foobarbaz = foobar.open_file("baz")?; + /// let contents = io::read_to_string(foobarbaz)?; + /// assert_eq!(contents, "Hello, world!"); + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_for_traversal>(path: P) -> io::Result { + fs_imp::Dir::open_for_traversal(path.as_ref()).map(|inner| Self { inner }) + } + /// Queries metadata about the underlying directory. /// /// # Examples @@ -1711,6 +1768,99 @@ impl Dir { ) -> io::Result<()> { self.inner.rename(from.as_ref(), &to_dir.inner, to.as_ref()) } + + /// Attempts to create a directory relative to this directory. + /// + /// This function interprets `path` relative to the directory provided by `self`. To create a directory + /// relative to the current working directory, or at an absolute path, see + /// [`fs::create_dir`][crate::fs::create_dir]. + #[unstable(feature = "dirfd", issue = "120426")] + pub fn create_dir>(&self, path: P) -> io::Result<()> { + self.inner.create_dir(path.as_ref()) + } + + /// Attempts to open a directory in read-only mode relative to this directory. + /// + /// This function interprets `path` relative to the directory provided by `self`. To open a directory + /// relative to the current working directory, or at an absolute path, see [`Dir::open`]. + /// + /// # Errors + /// + /// This function will return an error if `path` does not point to an existing directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::Dir}; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::open("foo")?; + /// let foobar = dir.open_dir("bar")?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_dir>(&self, path: P) -> io::Result { + self.inner + .open_dir(path.as_ref(), &OpenOptions::new().read(true).0) + .map(|inner| Self { inner }) + } + + /// Attempts to open a directory relative to this directory according to `opts`. + /// + /// This function interprets `path` relative to the directory provided by `self`. To open a directory + /// relative to the current working directory, or at an absolute path, see [`Dir::open`]. + /// + /// # Errors + /// + /// This function will return errors according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::fs::{Dir, OpenOptions}; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::open("foo")?; + /// let foobar_w = dir.open_dir_with("bar", &OpenOptions::new().write(true))?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_dir_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + self.inner.open_dir(path.as_ref(), &opts.0).map(|inner| Self { inner }) + } + + /// Attempts to remove a directory relative to this directory. + /// + /// This function interprets `path` relative to the directory provided by `self`. To remove a directory + /// relative to the current working directory, or at an absolute path, see + /// [`fs::remove_dir`][crate::fs::remove_dir]. + /// + /// # Errors + /// + /// This function will return an error if `path` does not point to an existing directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::Dir}; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::open("foo")?; + /// dir.remove_dir("bar")?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn remove_dir>(&self, path: P) -> io::Result<()> { + self.inner.remove_dir(path.as_ref()) + } } impl AsInner for Dir { diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index f0dbe3e76984a..5ae36cb362c0b 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -2711,3 +2711,38 @@ fn test_dir_rename_file() { check!(f.read_exact(&mut buf)); assert_eq!(b"bar", &buf); } + +#[test] +fn test_dir_remove_dir() { + let tmpdir = tmpdir(); + check!(fs::create_dir(tmpdir.join("foo"))); + let dir = check!(Dir::open(tmpdir.path())); + check!(dir.remove_dir("foo")); + assert!(!matches!(exists(tmpdir.join("foo")), Ok(true))); +} + +#[test] +fn test_dir_create_dir() { + let tmpdir = tmpdir(); + let dir = check!(Dir::open(tmpdir.path())); + check!(dir.create_dir("foo")); + check!(Dir::open(tmpdir.join("foo"))); +} + +#[test] +fn test_dir_open_dir() { + let tmpdir = tmpdir(); + let dir1 = check!(Dir::open(tmpdir.path())); + check!(dir1.create_dir("foo")); + let dir2 = check!(Dir::open(tmpdir.path().join("foo"))); + let mut f = + check!(dir2.open_file_with("bar.txt", &OpenOptions::new().create(true).write(true))); + check!(f.write(b"baz")); + check!(f.flush()); + drop(f); + let dir3 = check!(dir1.open_dir("foo")); + let mut f = check!(dir3.open_file("bar.txt")); + let mut buf = [0u8; 3]; + check!(f.read_exact(&mut buf)); + assert_eq!(b"baz", &buf); +} diff --git a/library/std/src/sys/fs/common.rs b/library/std/src/sys/fs/common.rs index 68aed39d1dcdf..8345d8e214a1c 100644 --- a/library/std/src/sys/fs/common.rs +++ b/library/std/src/sys/fs/common.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] // not used on all platforms -use crate::fs::{remove_file, rename}; +use crate::fs::{create_dir, remove_dir, remove_file, rename}; use crate::io::{self, Error, ErrorKind}; use crate::path::{Path, PathBuf}; use crate::sys::IntoInner; @@ -71,6 +71,12 @@ impl Dir { path.canonicalize().map(|path| Self { path }) } + pub fn open_for_traversal(path: &Path) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + Self::open(path, &opts) + } + pub fn open_file(&self, path: &Path, opts: &OpenOptions) -> io::Result { File::open(&self.path.join(path), &opts) } @@ -86,6 +92,18 @@ impl Dir { pub fn rename(&self, from: &Path, to_dir: &Self, to: &Path) -> io::Result<()> { rename(self.path.join(from), to_dir.path.join(to)) } + + pub fn create_dir(&self, path: &Path) -> io::Result<()> { + create_dir(self.path.join(path)) + } + + pub fn open_dir(&self, path: &Path, opts: &OpenOptions) -> io::Result { + Self::open(&self.path.join(path), opts) + } + + pub fn remove_dir(&self, path: &Path) -> io::Result<()> { + remove_dir(path) + } } impl fmt::Debug for Dir { diff --git a/library/std/src/sys/fs/unix/dir.rs b/library/std/src/sys/fs/unix/dir.rs index f3f612a225ed1..87bd9ee74689d 100644 --- a/library/std/src/sys/fs/unix/dir.rs +++ b/library/std/src/sys/fs/unix/dir.rs @@ -1,14 +1,12 @@ -use libc::{c_int, renameat, unlinkat}; +use libc::{c_int, mkdirat, renameat, unlinkat}; cfg_select! { - not( - any( - all(target_os = "linux", not(target_env = "musl")), - target_os = "l4re", - target_os = "android", - target_os = "hurd", - ) - ) => { + not(any( + all(target_os = "linux", not(target_env = "musl")), + target_os = "l4re", + target_os = "android", + target_os = "hurd", + )) => { use libc::{open as open64, openat as openat64}; } _ => { @@ -30,6 +28,13 @@ use crate::sys::helpers::run_path_with_cstr; use crate::sys::{AsInner, FromInner, IntoInner, cvt, cvt_r}; use crate::{fmt, fs, io}; +const TRAVERSE_DIRECTORY: i32 = cfg_select! { + any(target_os = "freebsd", target_os = "aix") => libc::O_EXEC, + any(target_os = "linux", target_os = "android", target_os = "l4re") => libc::O_PATH, + target_os = "illumos" => libc::O_SEARCH, + _ => libc::O_RDONLY, +}; + pub struct Dir(OwnedFd); impl Dir { @@ -37,8 +42,14 @@ impl Dir { run_path_with_cstr(path, &|path| Self::open_with_c(path, opts)) } + pub fn open_for_traversal(path: &Path) -> io::Result { + run_path_with_cstr(path, &|path| Self::open_traversal_c(path)) + } + pub fn open_file(&self, path: &Path, opts: &OpenOptions) -> io::Result { - run_path_with_cstr(path.as_ref(), &|path| self.open_file_c(path, &opts)) + run_path_with_cstr(path.as_ref(), &|path| self.open_file_c(path, &opts, 0)) + .map(|fd| FileDesc::from_inner(fd)) + .map(File) } pub fn metadata(&self) -> io::Result { @@ -61,7 +72,19 @@ impl Dir { }) } - pub fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result { + pub fn open_dir(&self, path: &Path, opts: &OpenOptions) -> io::Result { + run_path_with_cstr(path, &|path| self.open_file_c(path, &opts, libc::O_DIRECTORY)).map(Self) + } + + pub fn create_dir(&self, path: &Path) -> io::Result<()> { + run_path_with_cstr(path.as_ref(), &|path| self.create_dir_c(path)) + } + + pub fn remove_dir(&self, path: &Path) -> io::Result<()> { + run_path_with_cstr(path, &|path| self.remove_c(path, true)) + } + + fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result { let flags = libc::O_CLOEXEC | libc::O_DIRECTORY | opts.get_access_mode()? @@ -71,15 +94,27 @@ impl Dir { Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) } - fn open_file_c(&self, path: &CStr, opts: &OpenOptions) -> io::Result { + fn open_traversal_c(path: &CStr) -> io::Result { + let flags = libc::O_CLOEXEC | libc::O_DIRECTORY | TRAVERSE_DIRECTORY; + let fd = cvt_r(|| unsafe { open64(path.as_ptr(), flags, 0) })?; + Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) + } + + fn open_file_c( + &self, + path: &CStr, + opts: &OpenOptions, + extra_flags: c_int, + ) -> io::Result { let flags = libc::O_CLOEXEC | opts.get_access_mode()? | opts.get_creation_mode()? - | (opts.custom_flags as c_int & !libc::O_ACCMODE); + | (opts.custom_flags as c_int & !libc::O_ACCMODE) + | extra_flags; let fd = cvt_r(|| unsafe { openat64(self.0.as_raw_fd(), path.as_ptr(), flags, opts.mode as c_int) })?; - Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) + Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } fn remove_c(&self, path: &CStr, remove_dir: bool) -> io::Result<()> { @@ -99,6 +134,10 @@ impl Dir { }) .map(|_| ()) } + + fn create_dir_c(&self, path: &CStr) -> io::Result<()> { + cvt(unsafe { mkdirat(self.0.as_raw_fd(), path.as_ptr(), 0o777) }).map(|_| ()) + } } impl fmt::Debug for Dir { diff --git a/library/std/src/sys/fs/windows/dir.rs b/library/std/src/sys/fs/windows/dir.rs index 5e69515b66599..4fe0062af821b 100644 --- a/library/std/src/sys/fs/windows/dir.rs +++ b/library/std/src/sys/fs/windows/dir.rs @@ -66,6 +66,12 @@ impl Dir { with_native_path(path, &|path| Self::open_with_native(path, opts)) } + pub fn open_for_traversal(path: &Path) -> io::Result { + let mut opts = OpenOptions::new(); + opts.access_mode(c::FILE_TRAVERSE); + with_native_path(path, &|path| Self::open_with_native(path, &opts)) + } + pub fn open_file(&self, path: &Path, opts: &OpenOptions) -> io::Result { // NtCreateFile will fail if given an absolute path and a non-null RootDirectory if path.is_absolute() { @@ -87,6 +93,24 @@ impl Dir { self.rename_native(&from, to_dir, &to, is_dir) } + pub fn create_dir(&self, path: &Path) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.read(true); + opts.write(true); + opts.create_new(true); + self.open_dir(path, &opts).map(|_| ()) + } + + pub fn open_dir(&self, path: &Path, opts: &OpenOptions) -> io::Result { + let path = to_u16s_without_nul(&path)?; + self.open_file_native(&path, &opts, true).map(|handle| Self { handle }) + } + + pub fn remove_dir(&self, path: &Path) -> io::Result<()> { + let path = to_u16s_without_nul(&path)?; + self.remove_native(&path, true) + } + fn open_with_native(path: &WCStr, opts: &OpenOptions) -> io::Result { let creation = opts.get_creation_mode()?; let sa = c::SECURITY_ATTRIBUTES {